Skip to main content
BeeHex uses WebSocket connections to enable real-time multiplayer gameplay. Players can compete online with automatic matchmaking or create private rooms for custom games.

Architecture Overview

The multiplayer system is built on a WebSocket-based client-server architecture that provides low-latency, bidirectional communication.

WebSocket Server

Port 3002 handles all game connections and real-time move synchronization

REST API

Port 3001 manages user authentication, game history, and persistent data

Client Handler

React-based WebSocket handler manages connection state and packet routing

Game Instance

Synchronized game state across all connected clients

WebSocket Connection

Establishing Connection

The WebsocketHandler class manages the client-side WebSocket connection:
src/app/game_mode/WebsocketHandler.ts
export class WebsocketHandler {
  private socket: WebSocket;
  private callbacks: WebsocketCallbacks;

  constructor(callbacks: WebsocketCallbacks) {
    this.callbacks = callbacks;
    this.socket = new WebSocket(`ws:/${getEnv()['IP_HOST']}:3002/`);
    
    this.socket.onopen = () => {
      console.log("Connected to game server");
    };
    
    this.socket.onmessage = (e) => {
      const packet = JSON.parse(e.data) as packets.ClientBoundGenericPacket;
      this.handlePacket(packet);
    };
    
    this.socket.onclose = () => {
      console.log("Disconnected from game server");
      this.callbacks.connectionEndedCallback();
    };
  }
}

Connection Promise

The handler provides an async connection method that resolves when the WebSocket is ready:
src/app/game_mode/WebsocketHandler.ts
awaitConnection() {
  return new Promise<WebsocketHandler>((resolve, reject) => {
    this.socket.onopen = () => {
      resolve(this);
    };
    this.socket.onerror = (e) => {
      reject(e);
    }
  });
}
1

Initialize Handler

Create a new WebsocketHandler instance with callback functions for each event type.
2

Await Connection

Use awaitConnection() to ensure the WebSocket is fully established before sending packets.
3

Send Packets

Use sendPacket() to transmit game actions to the server.
4

Handle Responses

The handler automatically routes incoming packets to the appropriate callback functions.

Packet System

BeeHex uses a strongly-typed packet system for all client-server communication.

Server-Bound Packets

Packets sent from client to server:

Client-Bound Packets

Packets sent from server to client:
Error notifications from the server:
src/app/definitions.ts
export interface ClientBoundErrorMessagePacket {
  type: ClientBoundPacketType.ERROR_MESSAGE;
  message: string;
}
Notification when a match is found:
src/app/definitions.ts
export interface ClientBoundGameFoundPacket {
  type: ClientBoundPacketType.GAME_FOUND;
  game_id: GameId;
}
The client automatically redirects to the game page using this ID.
Complete game state when joining:
src/app/definitions.ts
export interface ClientBoundJoinGamePacket {
  type: ClientBoundPacketType.JOIN_GAME;
  game: Game;
}

export interface Game {
  game_id: string;
  game_parameters: GameParameters | LocalGameParameters;
  grid: Array<Array<number>>;
  first_player_id: string;
  second_player_id: string;
  turn: number;
}
This packet provides all information needed to initialize the game board.
Real-time move synchronization:
src/app/definitions.ts
export interface ClientBoundMovePlayedPacket {
  type: ClientBoundPacketType.MOVE_PLAYED;
  x: number;
  y: number;
  turn: number;
  grid_array: Array<Array<number>>;
}
Sent to all spectators and the opponent whenever a move is played.
Game conclusion with complete data:
src/app/definitions.ts
export interface ClientBoundGameEndPacket {
  type: ClientBoundPacketType.GAME_END;
  status: GameStatus;
  moves: string;
  winningHexagons: Array<[number, number]>;
}
Includes:
  • Winner determination
  • Complete move sequence
  • Winning path coordinates for visualization

Packet Handler Implementation

The WebSocket handler routes incoming packets to appropriate callbacks:
src/app/game_mode/WebsocketHandler.ts
handlePacket(packet: packets.ClientBoundGenericPacket) {
  switch (packet.type) {
    case packets.ClientBoundPacketType.ERROR_MESSAGE:
      this.callbacks.errorCallback((packet as packets.ClientBoundErrorMessagePacket).message);
      break;
      
    case packets.ClientBoundPacketType.GAME_SEARCH:
      const gameSearchPacket = packet as packets.ClientBoundGameSearchPacket;
      this.callbacks.gameSearchCallback(
        gameSearchPacket.game_parameters, 
        gameSearchPacket.player_count, 
        gameSearchPacket.elo_range
      );
      break;
      
    case packets.ClientBoundPacketType.GAME_FOUND:
      this.callbacks.gameFoundCallback((packet as packets.ClientBoundGameFoundPacket).game_id);
      break;
      
    case packets.ClientBoundPacketType.JOIN_GAME:
      this.callbacks.joinGameCallback((packet as packets.ClientBoundJoinGamePacket).game);
      break;
      
    case packets.ClientBoundPacketType.MOVE_PLAYED:
      const movePlayedPacket = packet as packets.ClientBoundMovePlayedPacket;
      this.callbacks.movePlayedCallback(
        movePlayedPacket.x, 
        movePlayedPacket.y, 
        movePlayedPacket.turn, 
        movePlayedPacket.grid_array
      );
      break;
  }
}

Game Flow

Complete Multiplayer Lifecycle

1

Connection

Player establishes WebSocket connection to game server.
let websocketHandler = await new WebsocketHandler({
  errorCallback,
  gameSearchCallback,
  gameFoundCallback,
  joinGameCallback,
  movePlayedCallback,
  connectionEndedCallback
}).awaitConnection();
2

Matchmaking

Client sends GAME_SEARCH or JOIN_ROOM packet with desired parameters.Server responds with periodic GAME_SEARCH updates showing matchmaking status.
3

Game Start

Server sends GAME_FOUND with unique game ID.Client navigates to game page: /hex/o_{game_id}Server sends JOIN_GAME with complete initial state.
4

Active Gameplay

Players alternate sending PLAY_MOVE packets.Server validates moves and broadcasts MOVE_PLAYED to all participants.
src/app/hex/[gameId]/page.tsx
function onlineClickCallback(i: number, j: number) {
  if (gameState === GameState.PLAYING && 
      game && 
      game.isTurnOf(ownId) && 
      game.isValidMove(i, j)) {
    websocketHandler.sendPacket({
      type: ServerBoundPacketType.PLAY_MOVE,
      x: i,
      y: j
    });
  }
}
5

Game End

Server detects win condition and sends GAME_END packet.Client displays victory/defeat screen and enters review mode.Game data is saved to database with final status and MMR changes.

Reconnection Handling

BeeHex implements automatic reconnection for network interruptions:
src/app/hex/[gameId]/page.tsx
function connectionEndedCallback() {
  console.log('Connection ended');
  if (workingGameState === GameState.PLAYING) {
    setGameState(GameState.RECONNECTING);
    workingGameState = GameState.RECONNECTING;
    onlineGameInitialize(); // Attempt to reconnect
  }
}
When reconnecting, the client automatically rejoins the game using the stored game ID and synchronizes to the current state.

User Status Management

The server tracks each player’s current status:
src/app/definitions.ts
export enum UserStatus {
  IDLE = 0,           // Brief authentication period
  IN_GAME = 1,        // Currently playing
  SEARCHING_GAME = 2  // In matchmaking queue
}
Players should quickly transition from IDLE to either SEARCHING_GAME or IN_GAME. Extended IDLE status may result in connection timeout.

Player Information

The system fetches and displays player information for each match:
src/app/hex/[gameId]/page.tsx
const fetchUser = async (userId: UserId) => {
  try {
    const user = await axios.get(`http://${getEnv()['IP_HOST']}:3001/get_user/${userId}`);
    return [user.data.id, user.data.username, user.data.mmr, user.data.registration_date];
  } catch (error) {
    console.error('Error fetching user:', error);
    return [null, null, null, null];
  }
};
Displayed information includes:
  • Username
  • Current MMR (for ranked games)
  • Player status
  • Timer (when implemented)

Spectator Support

While not fully implemented in the current version, the packet system is designed to support spectators:
src/app/definitions.ts
export interface ClientBoundJoinGamePacket {
  type: ClientBoundPacketType.JOIN_GAME;
  game: Game;
  // Sent when a player/spectator joins a game
}
Spectators receive the same MOVE_PLAYED and GAME_END packets as active players.

Security Considerations

All moves are validated on the server before broadcasting:
  • Move legality checking
  • Turn order enforcement
  • Game state verification
  • Player authentication
The client performs preliminary validation to provide immediate feedback:
if (game.isTurnOf(ownId) && game.isValidMove(i, j)) {
  // Send move to server
}
However, the server has final authority on all game actions.

Database Integration

Completed games are persisted to the database:
src/app/definitions.ts
export interface DatabaseGame {
  gameId: GameId;
  gameParameters: GameParameters;
  firstPlayerId: UserId;
  secondPlayerId: UserId;
  gameDate: EpochTimeStamp;
  status: GameStatus;
  moves?: string;
}
This enables:
  • Game history viewing
  • MMR calculation and tracking
  • Post-game analysis
  • Statistics and leaderboards

Error Handling

The multiplayer system includes comprehensive error callbacks:
src/app/hex/[gameId]/page.tsx
function errorCallback(message: string) {
  console.error(message);
  // Display error to user
}
Common error scenarios:
  • Invalid game ID
  • Connection timeout
  • Invalid move attempts
  • Authentication failures
  • Server unavailability
Always implement error callbacks to provide meaningful feedback to users when network issues or game errors occur.

Next Steps

Game Modes

Learn about different ways to play BeeHex

AI Analysis

Explore the move evaluation engine