Distributed Systems

ECE419, Winter 2026
University of Toronto
Instructor: Ashvin Goel

    Distributed Systems

Lab 1: Learning Go

Due date: Jan 25

In this assignment you will get started with programming in the Go language. You will learn how to compile, run, and debug a Go program. You will practice Go by implementing a distributed version of the game of Nim. In particular, you will implement the Nim client so that two instances of the client will play the game against each other. The two clients will connect to a server that we have provided.

Overview

Go (or golang, as it helps to search for it) is an imperative programming language designed to ease the development of distributed systems. In some ways, it is related to systems languages like C and Rust, in that programs are built using structs and functions. In other ways it is more similar to managed languages like C# and Java, in that it has a lot of "managed" features: it has a garbage collector, full runtime type information, and the design strives to have essentially no undefined behavior.

This assignment's objective is to help you become familiar with Go's basic features. You will learn: 1) how to use basic data structures like slices, 2) how to work with simple struct definitions and methods, and 3) the basics of serializing and deserializing Go data structures for client-server communication.

We have provided you several Go resources to familiarize yourself with the Go language. We suggest going over them before starting this lab.

Starter code

Update your local repository with our starter code.

The starter code for this lab is available in the nim directory. nim/client.go contains the initial client code, including a Clerk that you will use to implement client-side communication. nim/server.go contains the server code. The nim board, data request and reply format are defined in nim/common.go.

You only need to modify the nim/client.go file for this lab. You may modify or add any other files for testing but we will test your code with our starter code and your modified version of the nim/client.go file.

After you have completed your implementation, you can test your code as follows:

cd nim
go test

Implementation

In this lab, you will implement Nim, a two-player game. A game of Nim starts with a board that contains a fixed number of rows, and each row contains a set of coins. The two players take turns choosing a row and removing any non-zero number of coins from the row. A player wins when their last move takes the final coin from the board, so none of the rows have any coins left.

There are two kinds of nodes in the system: a client (nim/client.go) and a server (nim/server.go). You will implement Nim on the client side. One instance of your client will play the game against another instance of your client. Both instances will connect to our server, which serves as a proxy, forwarding messages received from one client to the other client during the game.

Clients communicate with the server using PlayArgs and PlayReply Go structs (see nim/common.go). The RecvRequest() function in nim/server.go waits to receive a serialized PlayArgs message from a client. The PlayArgs Go struct consists of a unique client ID and a MoveMessage struct that contains a unique game ID (GameID), a Nim board (Board), a row in the board from which coins were removed (MoveRow), and the number of coins that were removed from this row (MoveCount).

The board in the MoveMessage struct is a slice of unsigned bytes. It represents the state of the board after the move has been made. Each element of the slice represents one row of coins. For example, suppose a MoveMessage is {2, [6, 6, 4], 1, 2}. Then the game ID is 2 and the board consists of three rows. Rows 0 and 1 contain 6 coins and Row 2 contains 4 coins. In this move, the client generated this board by moving 2 coins from Row 1 (so the previous board was [6, 8, 4]).

The ProcessRequest() function in nim/server.go processes a received message from a client. Then the SendResponse() function in nim/server.go sends a serialized PlayReply message to a client. The PlayReply Go struct consists of a MoveMessage struct.

The implementation for this lab consists of two parts as described below.

Nim client with reliable network

Your first task is to implement a Nim client that works over a reliable network that does not drop, reorder or delay messages.

You are expected to implement the playMove() function in nim/client.go. This function plays one move at a time. Each move either 1) sends a move to the server using the SendRequest() function, or 2) receives a move from the server using the RecvResponse() function. See the comments at the top in labcomm/labcomm.go for more details about these functions.

The playMove() function returns the PlayStatus (see nim/common.go) and the message that the client sent to the server or received from the server. The caller (our tester) invokes the playMove() function repeatedly until it returns the game is over. The caller checks that the return values of the playMove() function satisfy the Nim game requirements.

The diagram below shows the Nim client-server protocol over a reliable network. Client messages sent using the SendRequest() function and received using the RecvResponse() function are shown as arrows between the two clients and the server. Each message shows the MoveMessage struct. The client also sends the client ID to the server (not shown in the diagram). The diagram also shows the status returned (in blue) by the PlayMove() function at the client.

Nim message protocol

The client side of the game proceeds in two phases, Start and Play. The Start phase performs a handshake between the two clients, ensuring they are the playing the same game. After that, the clients play the Nim game in the Play phase until the game is over.

The Start phase operates in two steps:

  1. The client sends a PlayArgs message with a nil board.

    The PlayMove() function returns the client's move and the SendMove status.

  2. The client waits to receive a PlayReply message. This message contains a game ID and the board to be played. Then, the client switches to the Play phase.

    The PlayMove() function returns the server's move and the RecvMove status.

The server generates the initial game board (e.g., board0 in the diagram above) and a unique game ID during the Start phase. It ensures that it sends the initial game board to exactly one client (e.g., Client 2 in the diagram above) so that the clients can continue to play the game on the same initial board. Clients must use the game ID returned by the server in all later messages.

The Play phase operates in two steps repeatedly until the game is over:

  1. The client decides on the next move to play based on the board it received and then sends a PlayArgs message with the updated board. The client also sends the row from which it removed the coins and the number of coins removed in MoveMessage. However, if the previously received message indicated that the other client won the game (e.g., when Client 1 receives the empty board [0, 0, 0] message in the diagram above), then the client sends a special last move (instead of the next move) PlayArgs message (see lastMove() function in nim/server.go).

    The PlayMove() function returns the client's move. If the client wins in this move (e.g., Client 2 wins in the diagram above), it returns the SendWin status. If the other client has already won, it returns the RecvWin status. Otherwise, it returns the SendMove status.

    The last move message lets the server know that the client has accepted that the other client won. In this case, the server sends a nil board to both clients indicating that the game is done.

  2. The client waits to receive a PlayReply message. This message contains either the board sent by the other client or the server's nil board. The client should verify that the received move is valid. A MoveMessage{_, Board, MoveRow, MoveCount} received from the client is valid with respect to the previously sent MoveMessage{_, PrevBoard, _, _} if Board is the state reached after removing MoveCount coins from row MoveRow in PrevBoard. A nil board message is valid if PrevBoard is empty. If the received move is not valid, then the client should resend its previous board next time.

    The PlayMove() function returns the server's move. If the client received a valid nil board, it returns the GameOver status. Otherwise, it returns the RecvMove status.

You have completed this task when your code passes the Reliable tests in the nim test suite.

$ cd nim
$ go test -run Reliable
Test: 2 clients (reliable network)...
  ... Passed --  time  0.0s #messages   44 #Ops   84
Test: 4 clients (reliable network)...
  ... Passed --  time  0.0s #messages  168 #Ops  168
PASS
ok      ece419/nim  0.006s

The numbers after each Passed are the real time in seconds, the number of messages sent and received by clients, and the number of operations executed (PlayMove() calls). In the second test, two separate games are played by 4 clients.

Nim client with unreliable network

Now you should implement the Nim client so that it works over an unreliable network that may drop, reorder or delay messages. When a client sends a move to the server using the SendRequest() function, this request can be arbitrarily delayed, reordered or dropped by the network. Similarly, when a client waits to receive a move using the RecvResponse() function, the move may be arbitrarily delayed or dropped by the network. While the SendRequest() function is guaranteed to return, the RecvResponse() function may never return if a request or reply is dropped by the network. Thus, instead of the RecvResponse() function, you should use the RecvResponseTimeout() function, which is guaranteed to return after a timeout. A reasonable timeout value is 200 ms. These functions return an ok status, which you should use to determine whether the calls failed.

To recover from message receive failures, the client should resend the previous message it sent, in both the Start and Play phases.

You can assume that the server does not fail and it implements the proxy logic correctly. You can also assume that messages are not corrupted.

You have completed this task when your code passes all the tests in the nim test suite.

$ cd nim
$ go test
Test: 2 clients (reliable network)...
  ... Passed --  time  0.0s #messages    36 #Ops   36
Test: 4 clients (reliable network)...
  ... Passed --  time  0.0s #messages   112 #Ops  112
Test: 2 clients (unreliable network)...
  ... Passed --  time 14.7s #messages   341 #Ops  341
Test: 4 clients (unreliable network)...
  ... Passed --  time 19.6s #messages   600 #Ops  600
PASS
ok      ece419/nim  34.310s

Note the much larger number of messages sent and received by clients, and the number of PlayMove() calls executed, when playing over the unreliable network.

Advice

Testing

If you wish to run individual tests, look for function names starting with Test, e.g., TestTwoReliable in nim_test.go. Then run the individual test as follows:

go test -run TestTwoReliable

Checking submission

We have provided you a tool that allows you to check your lab implementation and estimate your lab grade. After setting your path variable, you can run the tool in the nim directory as follows:

ece419-check lab1

You can check the output of the tool in the test.log file. Note that an individual test will fail if it takes more than 120 seconds.

Submission

We will use the following file from your remote repository for grading: nim/client.go.

Please see lab submission.

Acknowledgements

Prof. Ivan Beschastnikh and his team at UBC developed the original version of the Nim lab that used a single client to play with a server.