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 );
}
});
}
Initialize Handler
Create a new WebsocketHandler instance with callback functions for each event type.
Await Connection
Use awaitConnection() to ensure the WebSocket is fully established before sending packets.
Send Packets
Use sendPacket() to transmit game actions to the server.
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:
GAME_SEARCH
JOIN_ROOM
PLAY_MOVE
FORFEIT_GAME
Request matchmaking with specific game parameters: export interface ServerBoundGameSearchPacket {
type : ServerBoundPacketType . GAME_SEARCH ;
game_parameters : GameParameters ;
}
export interface GameParameters {
ranked : boolean ;
board_size : number ;
time_limit : number ;
}
Usage: websocketHandler . sendPacket ({
type: ServerBoundPacketType . GAME_SEARCH ,
game_parameters: {
time_limit: 0 ,
board_size: 9 ,
ranked: true
}
});
Join a private room using a room code: export interface ServerBoundJoinRoomPacket {
type : ServerBoundPacketType . JOIN_ROOM ;
room_id : RoomId ;
game_parameters : GameParameters ;
}
Usage: websocketHandler . sendPacket ({
type: ServerBoundPacketType . JOIN_ROOM ,
room_id: "ABCD1234" ,
game_parameters: {
time_limit: 0 ,
board_size: 7 ,
ranked: false
}
});
Submit a move during active gameplay: export interface ServerBoundPlayMovePacket {
type : ServerBoundPacketType . PLAY_MOVE ;
x : number ;
y : number ;
}
Usage: websocketHandler . sendPacket ({
type: ServerBoundPacketType . PLAY_MOVE ,
x: 3 ,
y: 4
});
Resign from the current game: export interface ServerBoundForfeitGamePacket {
type : ServerBoundPacketType . FORFEIT_GAME ;
}
Client-Bound Packets
Packets sent from server to client:
Error notifications from the server: export interface ClientBoundErrorMessagePacket {
type : ClientBoundPacketType . ERROR_MESSAGE ;
message : string ;
}
Matchmaking status updates: export interface ClientBoundGameSearchPacket {
type : ClientBoundPacketType . GAME_SEARCH ;
game_parameters : GameParameters ;
player_count : number ;
elo_range : [ number , number ];
}
Provides real-time information about:
Number of players currently searching
The ELO range being considered for matches
Confirmed game parameters
Notification when a match is found: 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: 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: 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: 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
Connection
Player establishes WebSocket connection to game server. let websocketHandler = await new WebsocketHandler ({
errorCallback ,
gameSearchCallback ,
gameFoundCallback ,
joinGameCallback ,
movePlayedCallback ,
connectionEndedCallback
}). awaitConnection ();
Matchmaking
Client sends GAME_SEARCH or JOIN_ROOM packet with desired parameters. Server responds with periodic GAME_SEARCH updates showing matchmaking status.
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.
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
});
}
}
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:
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.
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:
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:
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