Multiplayer Tetris Game (part 5)

Posted on: May 25, 2025

I’m used to write code by hand and often paying too much attention to how the code itself looks. Let’s see whether we can make AI agents do exactly what we need and eventually build a multiplayer game that supports multiple devices. I’m using Github Copilot on Visual Studio Code. Let’s experiment with a few models available on Copilot and see whether they produce anything useful when asked to evaluate our system design and generate server side code.

Evaluating our design

Let’s see what AI agents “think” about our current architecture and whether they come up with anything interesting. The prompt I’d like to try is below and we will add all blog posts in this series, and ADRs and the latest code up to this point as context.

Prompt: As an expert software architecture consultant. Understand and evaluate the architecture of the multi player tetris game described on these blog posts, ADRs and proof of concept code. Explain your evaluation thought process and at the end suggest what can be improved in 3-5 sentences.

Is this a good prompt? I have no idea. I guess I’m just going with vibes. The question is quite open and we are not suggesting anything specific for adding support for multiple devices. Here are general suggestions from several models available on Github Copilot in Visual Studio Code.

GPT-4.1

Your architecture is robust for local multiplayer and well-structured for maintainability and extensibility. To improve, focus on abstracting input sources, serializing and synchronizing game state, and introducing a networking layer to support multi-device multiplayer. Adding integration tests and optimizing performance-critical sections will further future-proof the codebase. This will enable the game to scale from local to remote multiplayer with minimal disruption to the core logic. Full Response

Gemini 2.5 Pro

To improve, consider further modularizing the core game logic, potentially extracting state management into a more defined module to prepare for complexities like network synchronization. Abstracting input handling away from direct keyboard events will be beneficial for supporting different input methods or networked play. Finally, formalizing the game state into a serializable format will be essential if you plan to expand to multi-device multiplayer, enabling easier transmission and synchronization across clients. Full Response

Claude 3.7 Sonnet

The architecture is well-structured for a client-side game but could be improved by implementing a more robust state management system, centralizing configuration, providing better abstractions for input handling, and preparing for potential network play by separating client/server concerns. Additionally, optimizing performance for rendering and collision detection would be beneficial for scaling to more complex game scenarios. Full Response

Here’s a table from above summaries (generated using GPT-4.1).

Main PointGPT-4.1Gemini 2.5 ProClaude 3.7 Sonnet
Abstract input sources/handling away from direct keyboard events
Serialize/formalize game state for synchronization and network play
Introduce networking layer/separate client/server concerns for multi-device multiplayer
Optimize performance-critical sections (e.g., rendering, collision detection)

Just from a glance, we can see that all 3 models are recommending better abstraction for input and networking and a serializable game state. But does it make sense? do we really need to abstract some parts of the code to support player input from other devices or make any changes to support serialization? we’ll take a look at these suggestions in a follow-up blog post but let’s assume these are correct and continue.


Implementing these changes

Next, let’s see if the models are good enough to come up with a plan to implement these changes. We will be using GPT-4.1 from now on unless it mentions otherwise.

Prompt: As an expert software architecture consultant hired to add multiple device support to a split screen multi player game. Understand and evaluate the architecture of the multi player tetris game described on these blog posts, ADRs, architecture reviews and proof of concept code. List down the main components of the new system, their responsibilities and where would they live (eg: client or server). Come up with a step by step execution plan to implement this new requirement.

You can read the full response here and once again we are going to assume the response is correct and continue. Let’s see how far we can go on vibes.

| Component                 | Responsibility                                                           | Location      |
| ------------------------- | ------------------------------------------------------------------------ | ------------- |
| **Game State Manager**    | Holds the canonical game state, applies actions, serializes/deserializes | Server        |
| **Networking Layer**      | Handles communication (e.g., WebSocket) between clients and server       | Client/Server |
| **Input Handler**         | Captures and abstracts player input (keyboard, touch, etc.)              | Client        |
| **Game Renderer**         | Renders the game state to the screen                                     | Client        |
| **Synchronization Logic** | Ensures clients are in sync with server state, handles reconciliation    | Client        |
| **Session/Lobby Manager** | Manages player sessions, game rooms, and player assignment               | Server        |
| **Authentication (opt.)** | Identifies players and manages access                                    | Server/Client |

Let’s write an ADR that describe how we are going to split code across the client and the server.

Prompt: As an expert software architecture consultant hired to add multiple device support to a split screen multi player game. Carefully read given architecture review documents and proposed implementation steps. Write an ADR in markdown that describe how the components are split between the client and the server. Follow the style used by other ADRs in the app.

# 04. Client vs. server components

Date: 2025-05-25

## Status

Proposed

## Context

To support multiplayer gameplay across multiple devices, the architecture must be adapted to split responsibilities between a client (running on each player's device) and a server (coordinating game state and player actions). The current implementation is single-process and single-device, with all game logic, rendering, and input handling running in the browser. Supporting multiple devices requires a clear separation of concerns and communication protocol between client and server.

## Decision

- **Client responsibilities:**
  - Capture player input (keyboard, touch, etc.) and send actions to the server.
  - Render the game state as received from the server using PixiJS.
  - Display points, game over, and other UI elements.
  - Maintain minimal local state (only for rendering and input buffering).
- **Server responsibilities:**
  - Maintain the authoritative game state, including all players, shapes, and world rules.
  - Apply game rules, process player actions, and advance the game (e.g., via a server-side ticker).
  - Broadcast updated game state to all connected clients after each tick or action.
  - Handle player join/leave and synchronize new clients with the current game state.
- **Communication:**
  - Use WebSockets for real-time, bidirectional communication between clients and server.
  - Define a protocol for sending player actions (from client to server) and game state updates (from server to clients).

## Consequences

- The game logic and world state must be refactored to run on the server, with the client acting as a thin view/controller.
- The renderer and input modules remain on the client, but are decoupled from the game world logic.
- Network latency and synchronization must be considered; the server is the source of truth for all game state.
- Enables true multiplayer gameplay across devices, but increases complexity in state management and error handling.

Before going further into implementation, let’s describe how a user experience would look like. Perhaps we can use a model to generate this as well.

Prompt: As an expert computer game user experience consultant define how the user flow would look like for a multi player tetris game. From the point where the client loads the web page, creates or joins a game, start and play and eventually end the game. List down what pages should be there on the client ,what actions the user can do on each page. Write an ADR with findings following the same style as other ADRs.

# 06. Multiplayer Tetris User Experience Flow

Date: 2025-05-25

## Status

Accepted

## Context

With the transition to a true multiplayer architecture supporting multiple devices, the user experience must be reimagined to accommodate remote players joining, playing, and leaving games in a seamless and intuitive way. The client application should guide users from landing on the site, through joining or creating a game session, to playing and eventually seeing results or starting a new game. This ADR outlines the recommended user flow and the corresponding client-side pages/screens, as well as the actions available to users at each stage.

## Decision

The multiplayer Tetris client will consist of the following main pages/screens:

### 1. Landing Page / Home

- **Purpose:** Welcome users and provide entry points to create or join a game.
- **Actions:**
  - Enter a player name and (optionally) select a color/avatar.
  - Choose to "Create Game" (host) or "Join Game" (enter a room code or select from a list).
  - View basic instructions or a "How to Play" link.

### 2. Lobby / Waiting Room

- **Purpose:** Allow players to gather before the game starts.
- **Actions:**
  - See a list of connected players (names, colors).
  - Copy/share the game link or room code with friends.
  - Host can start the game when ready (if applicable).
  - Option to leave the lobby and return to Home.
  - See status messages (e.g., "Waiting for players...").

### 3. Game Screen

- **Purpose:** Main gameplay interface.
- **Actions:**
  - Play Tetris using keyboard/touch controls.
  - See own and (optionally) other players' boards/scores.
  - View current points, next piece, and other game info.
  - Receive real-time updates from the server (state sync).
  - Option to pause (if supported) or forfeit/leave the game.

### 4. Game Over / Results

- **Purpose:** Show results after the game ends.
- **Actions:**
  - View final scores and rankings.
  - Option to play again (restart with same players), return to lobby, or go back to Home.
  - Share results (optional).
  - See summary of actions (e.g., lines cleared, time played).

### 5. Error / Disconnection Screen (as needed)

- **Purpose:** Handle network errors, lost connections, or invalid room codes.
- **Actions:**
  - Display error messages and guidance.
  - Option to retry, return to Home, or contact support.

#### Optional: Spectator Mode

- Allow users to join as spectators and watch ongoing games.

## Consequences

- **Improved Onboarding:** Clear separation of joining/creating games and gameplay reduces confusion for new users.
- **Social Play:** Lobby enables players to coordinate before starting, improving the multiplayer experience.
- **Resilience:** Dedicated error handling screens improve robustness in the face of network issues.
- **Replayability:** Game Over screen encourages replay and sharing, increasing engagement.
- **Extensibility:** This flow supports future features such as matchmaking, chat, or tournaments.

## Example Flow

1. **User visits site →** enters name → chooses "Create Game" or "Join Game".
2. **Lobby:** Waits for friends to join, sees player list.
3. **Game starts:** Plays Tetris, receives real-time updates.
4. **Game ends:** Sees results, can play again or return home.

Based on this user flow, now we can design an API. The AI model seems to prefer sending the entire game state to the client all the time, perhaps we can suggest it to use the Renderer interface as the point where the server needs to send messages to the client and methods on the renderer interface can be some of the messages the server sends the client.

Prompt: As an expert software architecture consultant hired to add multiple device support to a split screen multi player game. Carefully read given architecture review documents and proposed implementation steps. Design a Websocket based API that can be used to send player actions to the server and state changes to the client. Write an ADR in markdown that describes the API. Follow the style used by other ADRs in the app.

It didn’t generate what I expected so let’s try changing the prompt to avoid the mistakes it did.

Prompt: As an expert software architecture consultant hired to add multiple device support to a split screen multi player game. Carefully read given architecture review documents and proposed implementation steps. Design a Websocket based API that can be used to send player actions to the server and state changes to the client. Use ADR#5 that describes the user flow and existing the client side code to determine what message types should be supported. For example: methods on the Renderer interface can be mapped to some of the messages the server needs to send the client so the client can render. Instead of sending the entire state, only send incremental changes using the renderer interface. There’s no need to send the full game state to the client. Renderer based messages are not optional, it’s the most important. Players should send a “ready” message to the server. When all players are ready, the server will send a “game_started” message to the client. This will make the client navigate to the game page and the game will start. Write an ADR in markdown that describes the API. Follow the style used by other ADRs in the app.

# 06. WebSocket Server API for Multiplayer Tetris

Date: 2025-05-28

## Status

Accepted

## Context

To enable real-time multiplayer gameplay across multiple devices, the architecture must support efficient, low-latency communication between clients and a central server. The server is authoritative for game state, while clients are responsible for input and rendering. The user flow (see ADR#5) includes joining/creating games, waiting in a lobby, playing, and seeing results. The client code is structured around a Renderer interface, which provides methods for updating the visual state. The WebSocket API must support incremental, renderer-driven updates rather than sending the full game state on every change.

## Decision

- **Protocol:** Use WebSockets for bidirectional, real-time communication between clients and server.
- **Message Format:** All messages are JSON objects with a `type` field. Additional fields depend on the message type.
- **Client-to-Server Messages:**
  - `join`: Sent when a client connects. Includes player info (name, color).
  - `ready`: Sent when a player is ready to start the game from the lobby.
  - `action`: Sent when a player performs an in-game action (move, rotate, drop, etc.).
  - `leave`: Sent when a player leaves the game or disconnects.
- **Server-to-Client Messages:**
  - `player_joined`: Notifies clients when a new player joins the lobby.
  - `player_left`: Notifies clients when a player leaves or disconnects.
  - `game_started`: Sent when all players are ready; instructs clients to navigate to the game screen and begin play.
  - **Renderer-based incremental updates:**
    - `insert_shape`, `update_shape`, `remove_shape`, `update_points`, `clear_lines`, etc. (maps directly to Renderer interface methods; only changes are sent, not the full state)
  - `game_over`: Notifies clients that the game has ended and provides results.
  - `error`: Communicates errors (e.g., invalid action, room not found).

### Message Schemas

#### Client → Server

- **join**
  `{"type": "join", "player": { "name": "string", "color": "number" }}`
- **ready**
  `{"type": "ready", "playerId": "string"}`
- **action**
  `{"type": "action", "playerId": "string", "action": "MOVE_LEFT" | "MOVE_RIGHT" | "MOVE_DOWN" | "ROTATE" | "DROP"}`
- **leave**
  `{"type": "leave", "playerId": "string"}`

#### Server → Client

- **player_joined**
  `{"type": "player_joined", "player": { "id": "string", "name": "string", "color": "number" }}`
- **player_left**
  `{"type": "player_left", "playerId": "string"}`
- **game_started**
  `{"type": "game_started", "players": [ ... ]}`
- **Renderer-based updates**
  - `insert_shape`
    `{"type": "insert_shape", "shape": { ... }}`
  - `update_shape`
    `{"type": "update_shape", "shape": { ... }}`
  - `remove_shape`
    `{"type": "remove_shape", "shapeId": "number"}`
  - `update_points`
    `{"type": "update_points", "playerId": "string", "points": "number"}`
  - `clear_lines`
    `{"type": "clear_lines", "playerId": "string", "lines": [number]}`
- **game_over**
  `{"type": "game_over", "results": { ... }}`
- **error**
  `{"type": "error", "message": "string"}`

### Example Flow

1. Client connects and sends a `join` message.
2. Server responds with `player_joined` and updates the lobby for all clients.
3. Each client sends a `ready` message when ready.
4. When all players are ready, server sends `game_started` to all clients.
5. During gameplay, clients send `action` messages; server processes and sends incremental renderer-based updates.
6. When a player leaves, server sends `player_left`.
7. When the game ends, server sends `game_over`.

## Consequences

- Enables efficient, real-time multiplayer gameplay with minimal network overhead.
- Renderer-based incremental updates reduce bandwidth and improve responsiveness.
- The protocol is extensible for future features (e.g., chat, spectators).
- Requires careful handling of message ordering, error cases, and reconnection logic.

---

_See also: [04-client-server-components.md](./04-client-server-components.md), [05-user-experience-flow.md](./05-user-experience-flow.md)_

I’m not going to go through all the details, we’re going to go with the vibe. Think you are guiding a junior developer over Slack while they are working on something. You may not see the code until the end.

Next, let’s get to generating the code. Perhaps we should try out a different model instead. The internet seem to love “Claude Sonnet 4”, so let’s give it a try. Is it too much to ask in one go? Let’s try it out.

Prompt: As an expert software engineer hired to add multiple device support to a split screen multi player game. Carefully read and understand given blog posts, architecture review documents, ADRs and proposed implementation steps. Give extra attention to ADRs that describe the user flow and the websocket API.

Task 1: Setup a new NodeJS + Typescript project in the server directory. Use Yarn v4 as the package manager. Create a package.json file. Add a script tos tart the server on port 8000. Create a tsconfig file that would work with ts-node. Then generate a WebSocket server implementing the API described on ADR#6 and the user flow described on ADR#5. Please note that there can be multiple active games running in parallel. Copy all game logic code from the proof of concept directory but replace the pixi.js renderer with a network renderer that would forward changes as websocket messages to clients.

Task 2: Setup a new project using Parcel (similar to the proof of concept code) in the client directory. Use Yarn v4 as the package manager. Create a package.json file. Add a task to start the client on port 8001. Create pages described on ADR#5. For the Game screen, use the Pixi.js renderer on the proof of concept code. Methods on the Pixi.js renderer should be called when the client receives messages from the server.

It created an empty package.json file and then struggled with it file for a while, otherwise both the client and the server is up and running.

Let’s fix some bugs. The first one is quite simple, the PIXI.js library is not included in the index.html file. We can fix it with a simple prompt.

Prompt: /fix fix the bug where PIXI is not available on window. See how it’s loaded on the proof of concept code and fix this.

Next, it seems it’s failing to update a shape because it tries to get the shape by reference, this would no longer work because the shape object we receive from the server and the shape object available on the client are not the same.

renderer.ts:80 Uncaught TypeError: Cannot read properties of undefined (reading 'position')
    at Object.updateShape (renderer.ts:80:8)
    at TetrisClient.handleServerMessage (app.ts:350:31)
    at state.socket.onmessage (app.ts:119:14)

Let’s see if it is smart enough to figure this out without any help.

Prompt: Find out why I am getting this error on the client when trying to play the game. renderer.ts:80 Uncaught TypeError: Cannot read properties of undefined (reading ‘position’) at Object.updateShape (renderer.ts:80:8) at TetrisClient.handleServerMessage (app.ts:350:31) at state.socket.onmessage (app.ts:119:14)

Now we have something that kinda works. It’s buggy as hell but I am still impressed with the result :)

Basic components of the game

I haven’t looked at the generated code yet. Let’s do a code review and fix remaining bugs on a follow up blog post.

Tags: jsai