Implementing Reversi game board and broadcasting moves
Once I got the WebSocket connections up and running, the next step was to implement the Reversi game board. There are a lot of moving parts to this, from viewing the board to interacting with it, receiving feedback and everything in between. I want to focus on two user flows for now: the initial connection and what happens when a player makes a move. These two flows need to meet the following requirements:
- players joining at any time always see the latest state of the board
- players can interact with the board and place their pieces
- moves played by other players are displayed instantly
I made a flowchart on Mermaid to illustrate how I envision my client and server to communmicate for the first flow (view it on Mermaid here):

Now I know you might be wondering: what the hell is DumbReversi? 😂
As mentioned in my previous post, the server should maintain the authoritative state to ensure synchronicity. I’ve created a Reversi class in my server to be the source of truth, but my client also needs a Reversi-like class to help with rendering the UI, so to differentiate between those two files (especially when I’m working on both codebases at the same time), I’ve named the client one DumbReversi as it has no logic and simply acts as a “messenger”.
My server Reversi looks a little something like this:
// server's "smart" reversi.ts
class Reversi {
#board: Int8Array[];
#dimension: number;
constructor(props) {
const { dimension } = props;
this.#dimension = dimension;
this.#board = Array.from({ length: this.#dimension }, () =>
new Int8Array(this.#dimension).fill(0)
);
}
get board(): Int8Array[] {
return this.#board;
}
isMoveValid(x: number, y: number, team: Team): boolean {
// logic to check if move is valid
}
makeMove(
x: number,
y: number,
team: Team
): { success: boolean, message: string } {
// checks if move is valid
// if not, return
// if yes, add to board
}
getPiecesToFlip(x: number, y: number, team: Team): [number, number][] {
// more logic
}
}
While in comparison, my client one looks like this:
// client's dumb-reversi.ts
class DumbReversi {
#board: Int8Array[];
get board(): Int8Array[] {
return this.#board;
}
loadBoard(board: Int8Array[]) {
this.#board = board;
}
addPiece(x: number, y: number, teamName: TeamName) {
// add piece to board in memory
}
reverseMove(x: number, y: number, teamName: TeamName) {
// remove piece from board in memory
}
renderFlips(previousTeam: TeamName, piecesToFlip: [number, number][]) {
// flip previousTeam's piecesToFlip in board in memory
}
}
Upon a client’s initial connection, my server will fetch the current state of the board to respond with…
io.on("connection", (socket) => {
console.log("Client connected:", socket.id);
socket.emit("init-state", {
board: game.getBoard(),
});
});
… and my SvelteKit client listening to the init-state event will load the board into memory. I’m rendering it as a grid of <div>s for now and that should tackle the first point of my to-do list where players joining at any time always see the latest state of the board.

My final implementation of this game will use three.js to render a 3D board so for the time being, the <div> grid will suffice.
Moving on to the second flow, here’s another flowchart depicting what happens when a player makes a move (and you can also view this one on Mermaid here):

When a player clicks on a cell to make a move, the client updates the board immediately while emitting the play-piece event to the server in the background. This optimistic UI pattern ensures that the user gets instant feedback for an improved game experience.
// Board.svelte
function handleCellClick(x: number, y: number) {
reversiStore.playPiece(x, y, team);
}
// reversi-store.ts
import { DumbReversi } from "$lib/dumb-reversi";
import { emit } from "$lib/socket";
import { writable } from "svelte/store";
function createReversiStore() {
const { update } = writable({ board: null /* and other state values */ });
const dumbReversi = new DumbReversi();
return {
// [...]
setState: (serverState: ServerState) => {
dumbReversi.loadBoard(serverState.board);
// [...]
},
playPiece: (x: number, y: number, teamName: TeamName) => {
update((state) => {
const board = dumbReversi.addPiece(x, y, teamName);
emit("play-piece", { x, y, teamName });
return { ...state, board };
});
},
reverseMove: (x: number, y: number, teamName: TeamName) => {
dumbReversi.reverseMove(x, y, teamName);
// [...]
},
};
}
If the move is invalid, the client simply rolls it back while other clients wouldn’t even be aware that a move was played.1
If it is valid, the server will emit a board-updated event to all connected clients for an update.
socket.on("board-updated", (/* data */) => {
reversiStore.updateBoard(/* data */);
// [...]
});
// reversi-store.ts
function createReversiStore() {
return {
// [...]
updateBoard: (/* data */) => {
// if not player who made move, add piece to board
// flip pieces that need to be flipped
},
};
}
Here’s how the interaction looks like from both ends:

You can view the full source code so far in this reversi-game-board branch on GitHub.
Footnotes
-
Now I could broadcast the invalid move to them too but it serves them no purpose in knowing that the player has made an invalid move–the player will still need to make a move anyway before the opponent can play. There are other aspects of same-team dynamics that might benefit from this broadcast but that’s out of scope for this post. ↩