Documentation Index Fetch the complete documentation index at: https://mintlify.com/Ashokaas/BeeHex/llms.txt
Use this file to discover all available pages before exploring further.
Overview
BeeHex uses WebSocket for bidirectional real-time communication between clients and the game server. The protocol is packet-based with JSON serialization.
Server Endpoint: ws://{IP_HOST}:3002/All packets are JSON-encoded with a type discriminator field.
Connection Lifecycle
WebsocketHandler Class
Location: src/app/game_mode/WebsocketHandler.ts
The client-side handler manages WebSocket connection and packet routing.
interface WebsocketCallbacks {
errorCallback : ( message : string ) => void ;
gameSearchCallback : (
game_parameters : GameParameters ,
player_count : number ,
elo_range : [ number , number ]
) => void ;
gameFoundCallback : ( game_id : GameId ) => void ;
joinGameCallback : ( game : Game ) => void ;
movePlayedCallback : (
x : number ,
y : number ,
turn : number ,
grid_array : Array < Array < number >>
) => void ;
connectionEndedCallback : () => void ;
}
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 ClientBoundGenericPacket ;
this . handlePacket ( packet );
};
this . socket . onclose = () => {
console . log ( "Disconnected from game server" );
this . callbacks . connectionEndedCallback ();
};
}
}
Sending Packets
sendPacket ( packet : ServerBoundGenericPacket ) {
this . socket . send ( JSON . stringify ( packet ));
}
Handling Packets
handlePacket ( packet : ClientBoundGenericPacket ) {
switch ( packet . type ) {
case ClientBoundPacketType . ERROR_MESSAGE :
this . callbacks . errorCallback (
( packet as ClientBoundErrorMessagePacket ). message
);
break ;
case ClientBoundPacketType . GAME_SEARCH :
const gameSearchPacket = packet as ClientBoundGameSearchPacket ;
this . callbacks . gameSearchCallback (
gameSearchPacket . game_parameters ,
gameSearchPacket . player_count ,
gameSearchPacket . elo_range
);
break ;
case ClientBoundPacketType . GAME_FOUND :
this . callbacks . gameFoundCallback (
( packet as ClientBoundGameFoundPacket ). game_id
);
break ;
case ClientBoundPacketType . JOIN_GAME :
this . callbacks . joinGameCallback (
( packet as ClientBoundJoinGamePacket ). game
);
break ;
case ClientBoundPacketType . MOVE_PLAYED :
const movePlayedPacket = packet as ClientBoundMovePlayedPacket ;
this . callbacks . movePlayedCallback (
movePlayedPacket . x ,
movePlayedPacket . y ,
movePlayedPacket . turn ,
movePlayedPacket . grid_array
);
break ;
}
}
Packet Types
Location: src/app/definitions.ts
Server-Bound Packets
Packets sent from client to server:
enum ServerBoundPacketType {
GAME_SEARCH = 0 ,
CANCEL_GAME_SEARCH = 1 ,
JOIN_GAME = 2 ,
JOIN_ROOM = 3 ,
PLAY_MOVE = 4 ,
FORFEIT_GAME = 5
}
Client-Bound Packets
Packets sent from server to client:
enum ClientBoundPacketType {
ERROR_MESSAGE = 0 ,
GAME_SEARCH = 1 ,
GAME_FOUND = 2 ,
JOIN_GAME = 3 ,
MOVE_PLAYED = 4 ,
GAME_END = 5
}
Packet Specifications
Server-Bound
Client-Bound
GAME_SEARCH Initiates matchmaking for a new game. interface ServerBoundGameSearchPacket {
type : ServerBoundPacketType . GAME_SEARCH ;
game_parameters : GameParameters ;
}
interface GameParameters {
ranked : boolean ; // Whether this is a ranked game
board_size : number ; // 5, 7, or 9
time_limit : number ; // Currently only 0 (unlimited) supported
}
Example: {
"type" : 0 ,
"game_parameters" : {
"ranked" : true ,
"board_size" : 9 ,
"time_limit" : 0
}
}
CANCEL_GAME_SEARCH Cancels active matchmaking request. interface ServerBoundCancelGameSearchPacket {
type : ServerBoundPacketType . CANCEL_GAME_SEARCH ;
}
Example: JOIN_GAME Joins an existing game by ID (used after GAME_FOUND). interface ServerBoundJoinGamePacket {
type : ServerBoundPacketType . JOIN_GAME ;
game_id : GameId ;
}
type GameId = string ;
Example: {
"type" : 2 ,
"game_id" : "game_abc123xyz"
}
JOIN_ROOM Joins a private room (for custom games with friends). interface ServerBoundJoinRoomPacket {
type : ServerBoundPacketType . JOIN_ROOM ;
room_id : RoomId ;
game_parameters : GameParameters ;
}
type RoomId = string ;
Example: {
"type" : 3 ,
"room_id" : "room_friend_game" ,
"game_parameters" : {
"ranked" : false ,
"board_size" : 7 ,
"time_limit" : 0
}
}
PLAY_MOVE Submits a move to the server. interface ServerBoundPlayMovePacket {
type : ServerBoundPacketType . PLAY_MOVE ;
x : number ; // Column index (0-based)
y : number ; // Row index (0-based)
}
Example: {
"type" : 4 ,
"x" : 4 ,
"y" : 3
}
The server validates that:
It’s the player’s turn
The cell at (y, x) is empty
The game is still in progress
FORFEIT_GAME Forfeits the current game (instant loss). interface ServerBoundForfeitGamePacket {
type : ServerBoundPacketType . FORFEIT_GAME ;
}
Example: ERROR_MESSAGE Server error notification. interface ClientBoundErrorMessagePacket {
type : ClientBoundPacketType . ERROR_MESSAGE ;
message : string ;
}
Example: {
"type" : 0 ,
"message" : "Invalid move: cell already occupied"
}
GAME_SEARCH Confirms matchmaking has started and provides queue info. interface ClientBoundGameSearchPacket {
type : ClientBoundPacketType . GAME_SEARCH ;
game_parameters : GameParameters ;
player_count : number ; // Number of players in queue
elo_range : [ number , number ]; // [min_elo, max_elo] for matchmaking
}
Example: {
"type" : 1 ,
"game_parameters" : {
"ranked" : true ,
"board_size" : 9 ,
"time_limit" : 0
},
"player_count" : 3 ,
"elo_range" : [ 1400 , 1600 ]
}
GAME_FOUND Matchmaking successful - opponent found. interface ClientBoundGameFoundPacket {
type : ClientBoundPacketType . GAME_FOUND ;
game_id : GameId ;
}
Example: {
"type" : 2 ,
"game_id" : "game_xyz789abc"
}
Client should immediately send JOIN_GAME packet with this game_id.
JOIN_GAME Full game state sent when joining or spectating. interface ClientBoundJoinGamePacket {
type : ClientBoundPacketType . JOIN_GAME ;
game : Game ;
}
interface Game {
game_id : string ;
game_parameters : GameParameters | LocalGameParameters ;
grid : Array < Array < number >>; // Current board state
first_player_id : string ; // Red player
second_player_id : string ; // Blue player
turn : number ; // Current turn number (1-indexed)
}
Example: {
"type" : 3 ,
"game" : {
"game_id" : "game_xyz789abc" ,
"game_parameters" : {
"ranked" : true ,
"board_size" : 5 ,
"time_limit" : 0
},
"grid" : [
[ 0 , 0 , 0 , 0 , 0 ],
[ 0 , 1 , 0 , 0 , 0 ],
[ 0 , 0 , 2 , 0 , 0 ],
[ 0 , 0 , 0 , 1 , 0 ],
[ 0 , 0 , 0 , 0 , 0 ]
],
"first_player_id" : "user_alice" ,
"second_player_id" : "user_bob" ,
"turn" : 3
}
}
MOVE_PLAYED Broadcasts a move to both players. interface ClientBoundMovePlayedPacket {
type : ClientBoundPacketType . MOVE_PLAYED ;
x : number ;
y : number ;
turn : number ;
grid_array : Array < Array < number >>; // Updated board state
}
Example: {
"type" : 4 ,
"x" : 2 ,
"y" : 1 ,
"turn" : 4 ,
"grid_array" : [
[ 0 , 0 , 0 , 0 , 0 ],
[ 0 , 1 , 2 , 0 , 0 ],
[ 0 , 0 , 2 , 0 , 0 ],
[ 0 , 0 , 0 , 1 , 0 ],
[ 0 , 0 , 0 , 0 , 0 ]
]
}
The client updates local GameInstance state by calling: gameInstance . updateGameState ( packet . grid_array , packet . turn );
GAME_END Game concluded with final result. interface ClientBoundGameEndPacket {
type : ClientBoundPacketType . GAME_END ;
status : GameStatus ;
moves : string ; // Encoded move history
winningHexagons : Array <[ number , number ]>; // Winning path coordinates
}
enum GameStatus {
IN_PROGRESS = 0 ,
FIRST_PLAYER_WIN = 1 ,
SECOND_PLAYER_WIN = 2 ,
DRAW = 3 , // Not implemented (impossible in Hex)
ABORTED = 4 // Game abandoned
}
Example: {
"type" : 5 ,
"status" : 1 ,
"moves" : "c3,d4,c4,d3,c5,d5,c6" ,
"winningHexagons" : [
[ 2 , 0 ], [ 2 , 1 ], [ 2 , 2 ], [ 2 , 3 ], [ 2 , 4 ]
]
}
Game Flow Example
Here’s a complete example of a multiplayer game session:
Player connects and searches
Client → Server: {
"type" : 0 ,
"game_parameters" : {
"ranked" : true ,
"board_size" : 9 ,
"time_limit" : 0
}
}
Server → Client: {
"type" : 1 ,
"game_parameters" : { "ranked" : true , "board_size" : 9 , "time_limit" : 0 },
"player_count" : 1 ,
"elo_range" : [ 1450 , 1550 ]
}
Match found
Server → Client: {
"type" : 2 ,
"game_id" : "game_match_001"
}
Join game
Client → Server: {
"type" : 2 ,
"game_id" : "game_match_001"
}
Server → Client: {
"type" : 3 ,
"game" : {
"game_id" : "game_match_001" ,
"game_parameters" : { "ranked" : true , "board_size" : 9 , "time_limit" : 0 },
"grid" : [[ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ], ... ],
"first_player_id" : "user_alice" ,
"second_player_id" : "user_bob" ,
"turn" : 0
}
}
First player makes move
Client (Alice) → Server: {
"type" : 4 ,
"x" : 4 ,
"y" : 4
}
Server → Both Clients: {
"type" : 4 ,
"x" : 4 ,
"y" : 4 ,
"turn" : 1 ,
"grid_array" : [[ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ], ... , [ 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 ], ... ]
}
Second player responds
Client (Bob) → Server: {
"type" : 4 ,
"x" : 4 ,
"y" : 3
}
Server → Both Clients: {
"type" : 4 ,
"x" : 4 ,
"y" : 3 ,
"turn" : 2 ,
"grid_array" : [[ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ], ... , [ 0 , 0 , 0 , 0 , 2 , 0 , 0 , 0 , 0 ], [ 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 ], ... ]
}
Game continues...
Players alternate moves until one player connects their edges.
Game ends
Server → Both Clients: {
"type" : 5 ,
"status" : 1 ,
"moves" : "e5,e4,d5,f4,c5,f5,b5,g5,a5" ,
"winningHexagons" : [[ 4 , 0 ], [ 4 , 1 ], [ 4 , 2 ], [ 4 , 3 ], [ 4 , 4 ], [ 4 , 5 ], [ 4 , 6 ], [ 4 , 7 ], [ 4 , 8 ]]
}
Error Handling
The server sends error messages for invalid operations:
Common Errors
Invalid Move
Wrong Turn
Game Not Found
Already in Game
{
"type" : 0 ,
"message" : "Invalid move: cell already occupied"
}
Cause: Attempting to play on a non-empty cell.{
"type" : 0 ,
"message" : "Not your turn"
}
Cause: Sending PLAY_MOVE when it’s opponent’s turn.{
"type" : 0 ,
"message" : "Game not found"
}
Cause: JOIN_GAME with invalid game_id.{
"type" : 0 ,
"message" : "Already in active game"
}
Cause: Attempting GAME_SEARCH while already in a game.
User Status States
enum UserStatus {
IDLE = 0 , // Just connected, authenticating
IN_GAME = 1 , // Currently playing a game
SEARCHING_GAME = 2 // In matchmaking queue
}
Server tracks user status to enforce valid transitions:
Database Game Structure
Games are persisted in the database with the following schema:
interface DatabaseGame {
gameId : GameId ;
gameParameters : GameParameters ;
firstPlayerId : UserId ;
secondPlayerId : UserId ;
gameDate : EpochTimeStamp ;
status : GameStatus ;
moves ?: string ; // Comma-separated move notation (e.g., "c3,d4,c5")
}
The moves field uses algebraic notation where each move is encoded as a grid position.
Connection Management
Awaiting Connection
awaitConnection (): Promise < WebsocketHandler > {
return new Promise < WebsocketHandler >(( resolve , reject ) => {
this . socket . onopen = () => {
resolve ( this );
};
this . socket . onerror = ( e ) => {
reject ( e );
}
});
}
Graceful Disconnection
When the WebSocket closes, the client is notified:
this . socket . onclose = () => {
console . log ( "Disconnected from game server" );
this . callbacks . connectionEndedCallback ();
};
The UI should handle this by:
Displaying a “Connection Lost” message
Attempting to reconnect
Restoring game state if reconnection succeeds
Security Considerations
Server-side validation is critical:
Always verify it’s the correct player’s turn
Validate move coordinates are within bounds
Check that target cell is empty
Authenticate user identity before accepting moves
The client-side code does NOT perform authentication - this must be handled by the server through session tokens or similar mechanisms.
Next Steps
Architecture Overview Return to high-level architecture documentation
Game Engine Explore AI algorithms and game state management