🎮 Beta Gamer

Chess — Socket events

Chess uses the /chess Socket.IO namespace. The BetaGamerProvider connects automatically — use useSocket() to access the raw socket for custom listeners. Emit matchmaking:join once the socket is ready.

Emit (client → server)

EventPayloadDescription
matchmaking:join{ username, sessionId?, wantsBot?, botDifficulty?, afkTimeoutEnabled? }Enter matchmaking or start a bot game.
matchmaking:leave{ playerId }Cancel matchmaking before a match is found.
game:move{ roomId, playerId, move: { from, to, promotion? } }Submit a move. from/to are algebraic squares ("e2", "e4"). promotion is "q"|"r"|"b"|"n".
game:resign{ roomId, playerId }Forfeit the game immediately.
afk:check{ roomId }Ask the server if an AFK warning is still active. Useful when local countdown hits 0 with no response.
game:draw:offer{ roomId, playerId }Offer a draw to the opponent.
game:draw:accept{ roomId }Accept the opponent's draw offer.
game:reconnect{ playerId }Rejoin after a disconnect. Must be within the 60-second window.
game:spectate{ roomId, username }Join a room as a read-only spectator. No session token required.

matchmaking:join — field reference

FieldTypeRequiredDefaultDescription
usernamestringYesDisplay name shown in the game UI. Not used for identification — pass playerId for that.
playerIdstringNouuidYour tenant userId from the session token. Pass session.players[0].id. Returned in game:over.winner for result tracking.
sessionIdstringNoPass the sessionId from your session token to resume a specific session. Required if you want the server to validate against a pre-created session.
wantsBotbooleanNofalseSet true to skip matchmaking and play against the AI immediately.
botDifficulty"easy" | "medium" | "hard"No"medium"Bot strength. Only used when wantsBot is true.
afkTimeoutEnabledbooleanNotrueOverride the session-level AFK setting for this connection.

Receive (server → client)

EventPayloadDescription
matchmaking:waiting{ playerId, position }In queue. position is your place in line (1 = next to be matched).
matchmaking:left{}Cancel confirmed — you have been removed from the queue.
matchmaking:already_started{ roomId }Cancel arrived too late — a match was already made. Proceed to the game.
game:started{ roomId, playerId, players, color, fen, currentTurn, playerTimers, afkTimeoutEnabled, afkTimeoutSeconds }Game is ready. Render the board at fen.
game:move:made{ move: { from, to, san, piece, flags }, fen, currentTurn, isCheck, isCheckmate, moveHistory, playerTimers }A move was played by either player.
timer:update{ playerTimers: { [playerId]: number }, currentTurn }Clock tick — fires every second. playerTimers values are milliseconds remaining.
chess:afk_warning{ playerId, secondsRemaining }Active player has been idle. secondsRemaining counts down from 30.
chess:afk_warning_cleared{}A move was made — dismiss the AFK warning banner.
afk:status{ playerId, expiresAt } | nullResponse to afk:check. null means warning was cleared; otherwise restart countdown from expiresAt - Date.now().
game:over{ winner: string | null, reason, pgn, fen }Game ended. winner is null for draws.
game:draw:offered{ by: playerId }Opponent offered a draw. You can accept with game:draw:accept.
player:disconnected{ socketId }Opponent lost connection. 60-second reconnect window starts.
player:reconnected{ playerId }Opponent reconnected within the window.
game:reconnected{ roomId, playerId, players, color, fen, currentTurn, moveHistory, playerTimers, afkWarning }You successfully rejoined. afkWarning is { playerId, expiresAt } if a warning is active, null otherwise.
game:spectate:joined{ roomId, players, fen, currentTurn, moveHistory }Spectator joined — current board state. Render board at fen.

game:started — field reference

FieldTypeDescription
roomIdstringUnique room identifier. Include in all subsequent emits.
playerIdstringYour player ID for this session. Include in game:move, game:resign, etc.
playersPlayer[][{ id, username, color }] — both players.
color"white" | "black"Your assigned color.
fenstringStarting FEN position (standard starting position for a new game).
currentTurnstringplayerId of whoever moves first (always white).
playerTimers{ [playerId]: number }Starting clock values in milliseconds (600000 = 10 min).
afkTimeoutEnabledbooleanWhether AFK forfeit is active for this game.
afkTimeoutSecondsnumberTotal idle seconds before forfeit (90).

game:move:made — field reference

FieldTypeDescription
move.fromstringSource square in algebraic notation ("e2").
move.tostringDestination square ("e4").
move.sanstringStandard Algebraic Notation ("Nf3", "O-O", "e8=Q+").
move.piecestringPiece type that moved: "p"|"n"|"b"|"r"|"q"|"k".
move.flagsstringMove flags: "n" normal, "c" capture, "e" en passant, "p" promotion, "k"/"q" castling.
fenstringBoard position after the move in FEN notation.
currentTurnstringplayerId of whoever moves next.
isCheckbooleanTrue if the move put the opponent in check.
isCheckmatebooleanTrue if the move resulted in checkmate (game:over follows immediately).
moveHistoryMoveEntry[]Full move history: [{ move: { from, to, san }, playerId, timestamp }].
playerTimers{ [playerId]: number }Updated clock values in milliseconds after the move.

Usage — with ChessBoard component

Use ChessBoard when you want the board rendered for you. You only need useSocket() for side events the component doesn't cover — AFK warnings, draw offers, disconnect banners.

import { useEffect, useRef } from 'react';
import { BetaGamerProvider, ChessBoard, useSocket } from '@beta-gamer/react';

function ChessSideEvents({ myPlayerId }: { myPlayerId: string }) {
  const socket = useSocket();
  const roomIdRef = useRef('');

  useEffect(() => {
    if (!socket) return;

    socket.emit('matchmaking:join', {
      username: 'Alex',
      playerId: myPlayerId,
      afkTimeoutEnabled: true,
    });

    socket.on('game:started', ({ roomId }) => {
      roomIdRef.current = roomId;
    });

    socket.on('chess:afk_warning', ({ playerId, secondsRemaining }) => {
      // show countdown banner
    });

    socket.on('game:draw:offered', ({ by }) => {
      // show accept/decline UI
    });

    socket.on('player:disconnected', () => {
      // show "opponent disconnected" banner
    });

    socket.on('game:over', ({ winner, reason }) => {
      // show result screen
    });

    return () => {
      socket.off('game:started');
      socket.off('chess:afk_warning');
      socket.off('game:draw:offered');
      socket.off('player:disconnected');
      socket.off('game:over');
    };
  }, [socket]);

  const offerDraw = () => socket?.emit('game:draw:offer', {
    roomId: roomIdRef.current, playerId: myPlayerId,
  });
  const resign = () => socket?.emit('game:resign', {
    roomId: roomIdRef.current, playerId: myPlayerId,
  });

  return (
    <div>
      <ChessBoard orientation="white" />
      <button onClick={offerDraw}>Offer draw</button>
      <button onClick={resign}>Resign</button>
    </div>
  );
}

// Wrap with BetaGamerProvider — ChessBoard reads context from it
export default function ChessPage({ sessionToken }: { sessionToken: string }) {
  return (
    <BetaGamerProvider token={sessionToken}>
      <ChessSideEvents myPlayerId="user_123" />
    </BetaGamerProvider>
  );
}

Usage — manual (no ChessBoard component)

Skip ChessBoard entirely if you want to render your own board, use a different chess library, or work outside React. You handle all events and emit all moves yourself.

import { useEffect, useRef, useState } from 'react';
import { useSocket } from '@beta-gamer/react';

function ChessGameManual({ myPlayerId }: { myPlayerId: string }) {
  const socket = useSocket();
  const roomIdRef   = useRef('');
  const playerIdRef = useRef('');
  const [fen, setFen] = useState('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1');
  const [myColor, setMyColor] = useState<'white' | 'black'>('white');

  useEffect(() => {
    if (!socket) return;

    socket.emit('matchmaking:join', { username: 'Alex', playerId: myPlayerId });

    socket.on('game:started', ({ roomId, playerId, color, fen: startFen }) => {
      roomIdRef.current   = roomId;
      playerIdRef.current = playerId;
      setMyColor(color);
      setFen(startFen);
    });

    socket.on('game:move:made', ({ fen: newFen, isCheck }) => {
      setFen(newFen);
    });

    socket.on('game:over', ({ winner, reason, pgn }) => {
      console.log('Game over:', reason, winner ?? 'draw');
    });

    return () => {
      socket.off('game:started');
      socket.off('game:move:made');
      socket.off('game:over');
    };
  }, [socket]);

  // You emit moves yourself — from/to are algebraic squares ("e2", "e4")
  const makeMove = (from: string, to: string, promotion?: string) => {
    socket?.emit('game:move', {
      roomId:   roomIdRef.current,
      playerId: playerIdRef.current,
      move: { from, to, ...(promotion && { promotion }) },
    });
  };

  const resign = () => socket?.emit('game:resign', {
    roomId: roomIdRef.current, playerId: playerIdRef.current,
  });

  // Render your own board using fen + myColor
  return null;
}
💡 Both approaches use the same socket events — the difference is only who renders the board and emits moves. Mix and match: use ChessBoard for the board but handle game:over yourself, or go fully manual. It's your layout.
Beta Gamer GaaS API — questions? support@beta-gamer.com