Year 2, Term 3, 2025

Networked Tic-Tac-Toe

A networked Tic-Tac-Toe server built on an unfinished course starting point, extended with win detection, concurrent matches and a working results flow.

  • Unity client, .NET console server
  • C#
  • Solo
  • One term
Networked Tic-Tac-Toe, Unity client, .NET console server

What it was, and what I owned

A two-player networked Tic-Tac-Toe with a standalone authoritative server, built on top of a deliberately unfinished course starting point. The login, lobby and game room skeleton, the ASerializable serialization layer and a handful of example protocol messages were provided as-is, along with an explicit list of what didn't work yet: only one game could ever run, that game had no win condition and never ended, and clients had no names.

My work was closing those gaps. GameRoom gained real win detection, checking the board after every move against the mover's ID, and a proper end state with a results screen, a concede path and a return to the lobby. Four protocol messages needed to exist for that to work and didn't: ConcedeRequest, CloseResultsRequest, OpenResults and PlayerNames. The single game room instance became a list (CreateGameRoom/RemoveGameRoom), so the server can run more than one match at a time instead of locking to whichever pair readied up first.

Player identity needed the same treatment: the login flow now rejects a duplicate name instead of silently accepting a second 'John Doe', and PlayerNames carries who's actually playing into the game room so the client can show it instead of two blank slots.

Attribution

The Packet, TcpMessageChannel and ASerializable serialization layer, and the room topology itself (LoginRoom, LobbyRoom, GameRoom, and the base Room class's member management and faulty-client cleanup), were provided by the course. My job was the protocol messages and networking interactions needed to make the game actually function: win detection, the concede and results flow, duplicate-name handling at login, and replacing the single game room with a list so multiple matches can run at once.

The code that makes the claim checkable

server/src/TCPGameServer.cs3 room types
private LoginRoom _loginRoom;	//this is the room every new user joins
private LobbyRoom _lobbyRoom;	//this is the room a user moves to after a successful 'login'
private List<GameRoom> _gameRooms;		//this is the room a user moves to when a game is succesfully started

//stores additional info for a player
private Dictionary<TcpMessageChannel, PlayerInfo> _playerInfo = new Dictionary<TcpMessageChannel, PlayerInfo>();

The server holds one login room, one lobby, and a list of game rooms. A client is a member of exactly one at a time, so 'where is this connection in its lifecycle' is answered by which room owns it rather than by state flags.

server/src/rooms/GameRoom.csauthoritative server
public void StartGame(TcpMessageChannel pPlayer1, TcpMessageChannel pPlayer2)
{
    if (IsGameInPlay) throw new Exception("Programmer error duuuude.");

    //Log.LogInfo("Starting game...", this);
    IsGameInPlay = true;
    addMember(pPlayer1);
    addMember(pPlayer2);

    //Send player names
    PlayerNames playerNames = new PlayerNames();
    playerNames.player1 = _server.GetPlayerInfo(pPlayer1).name;
    playerNames.player2 = _server.GetPlayerInfo(pPlayer2).name;
    sendToAll(playerNames);

    //Refresh board view so it doesn't display a previous match before a move is made.
    sendMakeMoveRequest(0);

    //Saving the player's so they can be moved to the lobby once the game has ended.
    _player1 = pPlayer1;
    _player2 = pPlayer2;
}

Starting a match moves both players out of the lobby and into a fresh game room, then broadcasts the pairing. Moves are validated server-side and the result is sent to both clients, so a modified client cannot claim a square it did not win.

Systems deep-dive

The course starting point could run exactly one game, and that game never ended, since GameRoom had no way to detect a win and no message to leave it with. CheckGameEnd runs after every MakeMoveRequest, checking the board's eight winning lines against the ID of whoever just moved. A win, a concede (ConcedeRequest) or a disconnect all funnel into the same EndGame, which records the result, opens a results screen, and waits for CloseResultsRequest to send both players back to the lobby.

The bigger structural fix was TCPGameServer holding a single GameRoom field. That meant a second pair readying up in the lobby had nowhere to go until the first match finished. Replacing it with a list and CreateGameRoom/RemoveGameRoom calls let the lobby spin up a fresh room per match, so concurrent games are possible rather than a queue of one.