🎮 Beta Gamer

Tic-tac-toe — Socket events

Tic-tac-toe uses the /tictactoe Socket.IO namespace. Use useSocket() to access the raw socket for custom listeners.

Emit (client → server)

EventPayloadDescription
matchmaking:join{ username, playerId?, sessionId?, wantsBot?, botDifficulty? }Enter matchmaking or start a bot game.
matchmaking:leave{ playerId }Cancel matchmaking before a match is found.
game:move{ roomId, playerId, position }Place your mark on cell 0–8.
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:reconnect{ playerId }Rejoin after a disconnect within the 60-second window.
game:spectate{ roomId, username }Join a room as a read-only spectator.

Receive (server → client)

EventPayloadDescription
matchmaking:waiting{ playerId, position }In queue. position is your place in line.
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, mark, board, currentTurn, afkTimeoutEnabled, afkTimeoutSeconds, afkWarnSeconds }Game is ready.
game:move:made{ position, mark, board, currentTurn, moveHistory }A mark was placed by either player.
tictactoe:afk_warning{ playerId, secondsRemaining }Active player has been idle. secondsRemaining counts down to timeout.
tictactoe: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, winningLine?: number[] }Game ended. reason can be victory, draw, afk_timeout, or resignation.
player:disconnected{ socketId }Opponent lost connection. 60-second reconnect window starts.
player:reconnected{ playerId }Opponent reconnected.
game:reconnected{ roomId, playerId, players, mark, board, currentTurn, moveHistory, afkWarning }You successfully rejoined. afkWarning is { playerId, expiresAt } if a warning is active, null otherwise.
game:spectate:joined{ roomId, players, board, currentTurn, moveHistory }Spectator joined — current board state.

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.
sessionIdstringNoResume a specific session (e.g. after a page reload).
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.

game:started — field reference

FieldTypeDescription
roomIdstringInclude in all subsequent emits.
playerIdstringYour player ID for this session.
playersPlayer[][{ id, username }] — both players. Index 0 is X, index 1 is O.
mark"X" | "O"Your assigned mark. X always moves first.
board("X"|"O"|null)[]Flat array of length 9. null = empty. Positions 0–8, left-to-right, top-to-bottom.
currentTurnnumberIndex into players[] — whose turn it is (0 or 1).
afkTimeoutEnabledbooleanWhether AFK timeout is active for this session.
afkTimeoutSecondsnumberSeconds of inactivity before the active player forfeits. Default 60.
afkWarnSecondsnumberSeconds before timeout that the warning fires. Default 20.

game:move:made — field reference

FieldTypeDescription
positionnumberCell index 0–8 where the mark was placed.
mark"X" | "O"The mark that was placed.
board("X"|"O"|null)[]Full updated board state.
currentTurnnumberIndex into players[] for whoever moves next.
moveHistoryMoveEntry[][{ position, mark, playerId }] — full move history.

Usage — with TictactoeBoard component

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

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

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

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

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

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

    socket.on('game:over', ({ winner, reason, winningLine }) => {
      // show result screen — winningLine highlights the three winning cells
    });

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

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

  return (
    <div>
      <TictactoeBoard />
      <button onClick={resign}>Resign</button>
    </div>
  );
}

export default function TictactoePage({ sessionToken }: { sessionToken: string }) {
  return (
    <BetaGamerProvider token={sessionToken}>
      <TictactoeSideEvents myPlayerId="user_123" />
    </BetaGamerProvider>
  );
}

Usage — manual (no TictactoeBoard component)

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

function TictactoeGameManual({ myPlayerId }: { myPlayerId: string }) {
  const socket = useSocket();
  const roomIdRef = useRef('');
  const [board, setBoard]             = useState<('X'|'O'|null)[]>(Array(9).fill(null));
  const [currentTurn, setCurrentTurn] = useState(0);
  const [players, setPlayers]         = useState<any[]>([]);
  const [myMark, setMyMark]           = useState<'X'|'O'>('X');
  const [winningLine, setWinningLine] = useState<number[]|null>(null);

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

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

    socket.on('game:started', ({ roomId, mark, board: b, players: p, currentTurn: t }) => {
      roomIdRef.current = roomId;
      setMyMark(mark);
      setBoard(b);
      setPlayers(p);
      setCurrentTurn(t);
    });

    socket.on('game:move:made', ({ board: b, currentTurn: t }) => {
      setBoard(b);
      setCurrentTurn(t);
    });

    socket.on('game:over', ({ winner, reason, winningLine: wl }) => {
      if (wl) setWinningLine(wl); // highlight the three winning cells
      console.log('Game over:', reason, winner ?? 'draw');
    });

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

  const isMyTurn = players[currentTurn]?.id === myPlayerId;

  const move = (position: number) => {
    if (!isMyTurn || board[position]) return;
    socket?.emit('game:move', {
      roomId: roomIdRef.current, playerId: myPlayerId, position,
    });
  };

  // Render your own 3×3 grid using board[], winningLine
  return null;
}
💡 No clock in Tic-tac-toe. No draw offers — draw only happens when all 9 cells are filled with no winner. AFK timeout defaults to 60s with a 20s warning.
Beta Gamer GaaS API — questions? support@beta-gamer.com