# 3. Make the server the referee

Before adding network traffic, write rules that are easy to read and test. The browser may ask to play square 4. It may not declare “I won”, choose the other player's identity, or replace the board.


```ts
export type Mark = 'X' | 'O';
export type Result = Mark | 'draw' | null;
const wins = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]];

/** Synchronous rules: no network, clocks, browser state, or asynchronous turn races. */
export class Game {
    private readonly players: string[] = [];
    private readonly board: (Mark | null)[] = Array(9).fill(null);
    private revision = 0;
    private result: Result = null;

    join(account: string) {
        if (this.players.includes(account)) return;
        if (this.players.length === 2) throw new Error('This room is full.');
        this.players.push(account);
    }

    snapshot(account: string) {
        const seat = this.players.indexOf(account);
        if (seat < 0) throw new Error('Join this room first.');
        return { board: [...this.board], players: [...this.players], revision: this.revision,
            result: this.result, turn: (this.revision % 2 === 0 ? 'X' : 'O') as Mark,
            you: (seat === 0 ? 'X' : 'O') as Mark };
    }

    move(account: string, cell: number, revision: number) {
        const { you, turn } = this.snapshot(account);
        if (this.players.length !== 2) throw new Error('Wait for a second player.');
        if (this.result) throw new Error('The game is finished. Choose a new room.');
        if (revision !== this.revision) throw new Error('The board changed. Try again.');
        if (you !== turn) throw new Error('Wait for your turn.');
        if (!Number.isInteger(cell) || cell < 0 || cell > 8 || this.board[cell] !== null) throw new Error('Choose an empty square.');
        this.board[cell] = you;
        this.revision++;
        if (wins.some(line => line.every(index => this.board[index] === you))) this.result = you;
        else if (this.revision === 9) this.result = 'draw';
    }
}

/** Rooms are public to signed-in players. Seats stay reserved for reconnects. */
export class Games {
    private readonly rooms = new Map<string, Game>();
    join(room: string, account: string) {
        let game = this.rooms.get(room);
        if (!game) {
            if (this.rooms.size >= 100) throw new Error('Room capacity reached. Restart this tutorial server.');
            game = new Game();
            this.rooms.set(room, game);
        }
        game.join(account);
        return game;
    }
    resume(room: string, account: string) {
        const game = this.rooms.get(room);
        if (!game) throw new Error('Room not found. Join a new room.');
        game.snapshot(account);
        return game;
    }
}
```


The first distinct account takes X; the second takes O. Joining again with the same account is idempotent. `move()` verifies membership, a second player, an unfinished game, the current revision, the correct turn and an empty square—in that order—before changing anything.

The revision is a move number, not a clock. If an old click arrives after the board changed, it is rejected instead of being applied to a different turn. Both validation and mutation are synchronous, so another socket callback cannot interleave halfway through a move in this single Node process.

`snapshot()` copies arrays. A consumer cannot edit the game's internal board by changing a returned snapshot. `Games` caps room creation at 100; a full tutorial server fails explicitly instead of growing forever.

## Run the rules

```sh
npm run test:coverage
```

The unit tests exercise every winning line, both winning marks, a draw, illegal turns, stale revisions, invalid cells, nonmember access and capacity. They enforce 100% coverage of **game.ts**. Real-network tests run alongside them; that number is not a claim about every branch of the authentication or browser code.

**Checkpoint:** the referee works without a browser. Rooms and seats survive a disconnected socket, but not a server restart. That is a deliberate teaching boundary, not persistence.
