4 Problems in writing a clean Chess Multiplayer game in JavaScript.

Ajeet SinhaAjeet Sinha
3 min read

Writing a Game can be quite challenging due to the fact that there are multiple states to manage between the client and the server. If the states are not synchronized the game may act weird or crash, if there are too many state updates than required then the game will be sluggish and will not perform well. I wrote a multiplayer chess game to demonstrate the design. Demo: https://chess.askzombi.com/ Source: https://github.com/honeydreamssoftwares/simple_chess

The Architecture

We need something to manage the States and synchronize with clients efficiently. colyseus (https://colyseus.io/) is a multiplayer game framework with gives the option to

  • Manage states

  • Listen to specific property changes to state and act accordingly

  • Room management

  • Matchmaking

  • Scalability option with simple configuration changes like adding Reddis to store state.

The Problems

  1. Managing States

In a Chess game, we have to maintain a game server with state data of the turn of the player, black or white. Current status of the game it's running or not (game over). The layout of the chess board, and information about the two players. The move history (optional). This state should be synchronized efficiently with both of the players so they are updated with the game start and can act when their turn comes.

//ChessRoomState.ts
import { Schema, type,ArraySchema ,MapSchema  } from "@colyseus/schema";
import PlayerMove from "./PlayerMove";
import { PlayerDetails } from "./PlayerDetails";

export class ChessRoomState extends Schema {
  @type("string") turn_of_player: string = "white"; //White moves by default
  @type("number") number_of_players: number = 0;
  @type("boolean") is_game_running: boolean = true;
  @type("string") game_result_status: string = "";
  @type("string") game_result_winner: string = "";
@type([PlayerMove]) moves: ArraySchema<PlayerMove> = new ArraySchema<PlayerMove>();
  @type("string")
  fen: string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; 
  @type({ map: PlayerDetails })
  players: MapSchema<PlayerDetails> = new MapSchema<PlayerDetails>();
}
  1. Live update of state between client and server

The user experience of the game depends upon the effective synchronization of states to avoid lag and instability. We need to use WebSockets as the updates have to be live. The client then listens to the states and acts accordingly updating the chess board, showing the turn of the player and showing results. Also when a player makes a move the status needs to be updated at the server.

//ChessGameRoom.ts  (State update)
checkGameStatus(client: Client) {
    const gameResult = { winner: "", status: "", fen: this.chessGame.fen() };

    if (this.chessGame.isCheckmate()) {
      this.state.game_result_status = "Checkmate";
      this.state.game_result_winner =
        this.chessGame.turn() === "w" ? "Black" : "White";
    } else if (
      this.chessGame.isDraw() ||
      this.chessGame.isStalemate() ||
      this.chessGame.isThreefoldRepetition() ||
      this.chessGame.isInsufficientMaterial()
    ) {
      this.state.game_result_status = "Draw";
    }

    if (this.state.game_result_status) {
      this.state.is_game_running = false;
    }
  }
  1. Matchmaking of players

Since this is a two-player game we need a way to match the players at the server which connects both of them and assigns them a black or white color.

//ChessGameRoom.ts  (First player is assigned white and the next player is white)
  onJoin(client: Client, options: { playerName: string }) {
    console.log(client.sessionId, "joined with name:", options.playerName);

    const playerDetails = new PlayerDetails();
    playerDetails.color =
      this.currentNumberOfPlayers() === 1 ? "white" : "black";
    playerDetails.name = options.playerName;
    //Save state
    this.state.players.set(client.sessionId, playerDetails);
    this.state.number_of_players = this.currentNumberOfPlayers();
  }
  1. UI responsiveness

In React.js UI rerenders are expensive. If you have too many rerenders the UI will lag and lead to poor user experience. Here is how we listen to only properties we actually require.W

//ChessGame.tsx  .(State change only on the relavent property)

 room.state.listen("turn_of_player", (currentValue) => {
        setTurn(currentValue);
      });

      room.state.listen("number_of_players", (currentValue) => {
        setPlayerCount(currentValue);
      });

      room.state.listen("fen", (currentValue) => {
        setGame(new Chess(currentValue));
      });

      room.state.moves.onAdd((currentValue) => {
        setMoves(new ArraySchema<PlayerMove>(...room.state.moves));
      });

      room.state.listen("number_of_players", (currentValue) => {

        //Self details
        room.state.players.forEach((details, sessionId) => {
          if (sessionId === room.sessionId) {
            setIsWhite(details.color === "white");
            setPlayerColor(details.color);
          }
        });

        //Opponent name
        if (currentValue === 2) {
          room.state.players.forEach((details, sessionId) => {
            if (sessionId !== room.sessionId) {
              setOpponentName(details.name);
            }
          });
        }
      });

The same concept of state management and listening to specific property changes can be applied to any game design and much more complex games can be created.

Links

0
Subscribe to my newsletter

Read articles from Ajeet Sinha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ajeet Sinha
Ajeet Sinha