The vanilla UNO itself is a simple game to implement. Initially I thought of doing a completely command-line based interface, but that gets very unwieldy. So I quickly switched to termui.

Being built for dashboards, the UI library itself isn’t the most glamorous and I am contemplating moving to bubbletea. But that has to wait.

session with 4 players

In this post, I will note down a few of the stuff I have learned while implementing this prototype.

Typed channels

The UI is decoupled from the player client code itself and all communication happens via some channels. Since you can statically type your data in Go, you can have multiple channels like

clientChannels := client.ClientChannels{
		GeneralUICommandPushChan:       commChannels.GeneralUICommandChan,
		AskUserForDecisionPushChan:     commChannels.AskUIForUserTurnChan,
		NonDecisionReplCommandPullChan: commChannels.NonDecisionReplCommandsChan,
		LogWindowPushChan:              commChannels.LogWindowChan,
		GameEventPushChan:              commChannels.GameEventChan,
}

and immediately know what kind of data each of them are supposed to carry (unless you use interface{}). Lookin at you, Elixir. The above shows all the channels that the client code uses to communicate with the UI.

In big production apps and microservices, you have stuff like message-queues and pub-sub based frameworks. Go channels are like message queues in a single process… so… just threadsafe queues that are first class citizens of the language (since you can wait on them via select).

Player client

The admin program, right now, is really acting as a synchronization point for each player decision. Admin chooses a player and asks for that players turn → Player makes a turn (so-called “decision”) and sends to admin → Admin runs decision on its own table object and also sends the decision to all other player clients → All player clients run the decision on their own table object and wait for admin to choose the next player. Note that since all clients already has the entire state of the uno table, they already “know” which player’s turn is next, or if a player just won the game. But they still wait for admin to announce it - because the admin is the “leader” - shades of consensus protocols come to mind, but I ain’t crazy enough to put in a raft module in this. Or perhaps it will be interesting to play around with an admin-less version.

Server-sent events

The admin needs to push messages to the client. A dedicate socket connection is straightforward, but I had made the mistake of actually running an http.Server in the client itself that the admin can make requests to. Noob move. Now all players would need to make their computers accessible to the internet somehow - not something you can expect from clients with any sort of sanity.

One decent way to have the admin maintain an open connection to the client is via HTTP/1’s chunked response encoding. It really just amounts to having keep-alive, no-cache headers on the http request (and response). Rest of the stuff like “sending size value followed by two carriage-returns” is handled by http.Flusher so you don’t really need to care about the exact format of the chunked-response format. In HTTP/1 it will also add transfer-encoding: chunked header on the first payload. In fact, if you’re using HTTP/2, chunked encoding is not supported by the protcol, but http.Flusher will handle sending messages transparently using message frames.

When the client makes a request like POST /player, that HTTP connection becomes a long lived one that we can use io.Read on in a loop to get a constant stream of bytes. Of course you need to delimit your individual messages to be able to discern each individual message. I use line-delimited JSON messages (which means you cannot have “’\n’” character appearing in the JSON strings - works ok for my use-case).

There is however an issue with having long-lived HTTP connection like this. If you set any read timeouts on the http.Client that you for reading the stream, it may likely fail since it’s meant to be writing to the connection (i.e. not closing the connecting) for a relatively long time compared to your usual REST API calls. So it’s best to use a separate client object for this with a differnt configuration.

Go’s default http request handling

While http.Flusher takes care of flushing individual messages, a not so good of a news is that if you want to do this from a single “controller” goroutine which manages multiple such http.ResponseWriters and corresponding http.Flushers for multiple clients, you would need to keep the original handler invocation for each of the clients “alive”, by which I mean prevent returning from the handler function call because the http.Server will close (or mark the connection as idle for pooling) once the handler returns.

Now, http.Hijacker will prevent that but once you use that you will have to work with a TCP net.Conn, implementing the chunked protocol yourself.

So that leaves me to think that the “best” way is to prevent returning from the handler and keeping it “alive” for the duration of the controller goroutine.

More to come

I’m kinda excited with this project since it serves as a test-bench of sorts for me - a mental code-gym of sorts. Will be updating here when I make the next step. In particular I have to write a proper server that is capable of handling multiple game-sessions. Bit of a chore, but absolutely crucial for an MVP.