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)
| Event | Payload | Description |
|---|---|---|
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)
| Event | Payload | Description |
|---|---|---|
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 } | null | Response 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
username | string | Yes | — | Display name shown in the game UI. Not used for identification — pass playerId for that. |
playerId | string | No | uuid | Your tenant userId from the session token. Pass session.players[0].id. Returned in game:over.winner for result tracking. |
sessionId | string | No | — | Resume a specific session (e.g. after a page reload). |
wantsBot | boolean | No | false | Set 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
| Field | Type | Description |
|---|---|---|
roomId | string | Include in all subsequent emits. |
playerId | string | Your player ID for this session. |
players | Player[] | [{ 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. |
currentTurn | number | Index into players[] — whose turn it is (0 or 1). |
afkTimeoutEnabled | boolean | Whether AFK timeout is active for this session. |
afkTimeoutSeconds | number | Seconds of inactivity before the active player forfeits. Default 60. |
afkWarnSeconds | number | Seconds before timeout that the warning fires. Default 20. |
game:move:made — field reference
| Field | Type | Description |
|---|---|---|
position | number | Cell index 0–8 where the mark was placed. |
mark | "X" | "O" | The mark that was placed. |
board | ("X"|"O"|null)[] | Full updated board state. |
currentTurn | number | Index into players[] for whoever moves next. |
moveHistory | MoveEntry[] | [{ 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