🎮 Beta Gamer

Connect 4 — Socket events

Connect 4 uses the /connect4 Socket.IO namespace. Use useSocket() to access the raw socket for custom listeners.

Emit (client → server)

EventPayloadDescription
matchmaking:join{ username, playerId?, sessionId?, wantsBot?, botDifficulty?, afkTimeoutEnabled?, afkTimeoutSeconds?, afkWarnSeconds? }Enter matchmaking or start a bot game.
matchmaking:leave{ playerId }Cancel matchmaking before a match is found.
game:move{ roomId, playerId, column }Drop a piece in the given column (0–6, left to right).
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.

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.
sessionIdstringNoThe sessionId from your session token.
wantsBotbooleanNofalseSkip matchmaking and play against the AI immediately.
botDifficulty"easy"|"medium"|"hard"No"medium"Bot strength. Only used when wantsBot is true.
afkTimeoutEnabledbooleanNotrueWhether the AFK forfeit rule is active.
afkTimeoutSecondsnumberNo90Seconds of idle time before forfeit.
afkWarnSecondsnumberNo30How many seconds before forfeit the warning fires.

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, board, currentTurn, rows, cols, afkTimeoutEnabled, afkTimeoutSeconds, afkWarnSeconds }Game is ready.
game:move:made{ column, position, board, currentTurn, moveHistory, lastMove }A piece was dropped by either player.
connect4:afk_warning{ playerId, secondsRemaining }Active player has been idle. Count down locally from secondsRemaining.
connect4: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, winningCells?: number[] }Game ended.
player:disconnected{ socketId }Opponent lost connection. 60-second reconnect window starts.
player:reconnected{ playerId }Opponent reconnected.
game:reconnected{ roomId, playerId, players, color, board, currentTurn, moveHistory, rows, cols, 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.

game:started — field reference

FieldTypeDescription
roomIdstringInclude in all subsequent emits.
playerIdstringYour player ID for this session.
playersPlayer[][{ id, username }] — both players. Index 0 is red, index 1 is yellow.
color"red" | "yellow"Your assigned color. Red always moves first.
board(string|null)[]Flat array, length rows×cols. null = empty, "red"/"yellow" = piece. Row 0 is the top row.
currentTurnnumberIndex into players[] — whose turn it is (0 or 1).
rowsnumber6
colsnumber7
afkTimeoutEnabledbooleanWhether AFK forfeit is active for this game.
afkTimeoutSecondsnumberTotal idle seconds before forfeit.
afkWarnSecondsnumberSeconds before forfeit that the warning fires.

game:move:made — field reference

FieldTypeDescription
columnnumberColumn the piece was dropped into (0–6).
positionnumberBoard index where the piece landed (row * cols + col). Use this to animate the drop.
board(string|null)[]Full updated board state.
currentTurnnumberIndex into players[] for whoever moves next.
moveHistoryMoveEntry[][{ column, position, playerId }] — full move history.
lastMovenumberSame as position — the board index of the piece just placed.

Usage — with Connect4Board component

Use Connect4Board when you want the board rendered for you. You only need useSocket() for side events — AFK warnings, disconnect banners, game over.

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

function Connect4SideEvents({ 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('connect4:afk_warning', ({ playerId, secondsRemaining }) => {
      // show countdown banner
    });

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

    socket.on('game:over', ({ winner, reason, winningCells }) => {
      // show result screen — winningCells highlights the four in a row
    });

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

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

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

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

Usage — manual (no Connect4Board component)

Skip Connect4Board entirely if you want to render your own board. You emit game:move with a column number — the server handles gravity and returns the updated board.

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

function Connect4GameManual({ myPlayerId }: { myPlayerId: string }) {
  const socket = useSocket();
  const roomIdRef   = useRef('');
  const playerIdRef = useRef('');
  const [board, setBoard]         = useState<(string | null)[]>(Array(42).fill(null));
  const [currentTurn, setCurrentTurn] = useState(0);
  const [players, setPlayers]     = useState<any[]>([]);
  const [winningCells, setWinningCells] = useState<number[] | null>(null);

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

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

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

    socket.on('game:move:made', ({ board: b, currentTurn: t, position }) => {
      setBoard(b);
      setCurrentTurn(t);
      // position = board index where piece landed — use for drop animation
    });

    socket.on('game:over', ({ winner, reason, winningCells: wc }) => {
      setWinningCells(wc ?? null); // highlight the four 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 === playerIdRef.current;

  // Player clicks a column header to drop a piece
  const dropPiece = (column: number) => {
    if (!isMyTurn) return;
    socket?.emit('game:move', {
      roomId: roomIdRef.current, playerId: playerIdRef.current, column,
    });
  };

  // Render your own 7×6 grid using board[], winningCells
  return null;
}
💡 No chess-style clock in Connect 4 — only AFK timeout. No draw offers — draw only happens when the board fills with no winner. Both approaches use the same events.
Beta Gamer GaaS API — questions? support@beta-gamer.com