🎮 Beta Gamer

Tic-tac-toe — AFK timeout

Each player has a configurable idle time limit before they forfeit. Defaults: 60 seconds to place a mark, warning fires 20 seconds before forfeit. Both values are set at join time and can be customised per session.

Timeline (default settings)

AtEventPayloadWhat to do
0s(move made / game started)AFK timer resets.
40s idletictactoe:afk_warning{ playerId, secondsRemaining: 20 }Show countdown banner. Count down locally each second.
40s idletictactoe:afk_warning_cleared{}A move was made — dismiss the banner immediately.
60s idlegame:over{ winner: opponentId, reason: "afk_timeout" }Idle player forfeited.

The warning fires at afkTimeoutSeconds - afkWarnSeconds ms. With custom values of e.g. afkTimeoutSeconds: 30, afkWarnSeconds: 10, the warning fires at 20s and forfeit at 30s.

Configuring the timeout

// Pass in matchmaking:join — applies to this game only
socket.emit('matchmaking:join', {
  username:          'Alex',
  playerId:          session.players[0].id,
  afkTimeoutEnabled: true,
  afkTimeoutSeconds: 30,   // forfeit after 30s idle
  afkWarnSeconds:    10,   // warn 10s before forfeit (fires at 20s)
});

// Disable AFK entirely
socket.emit('matchmaking:join', {
  username:          'Alex',
  afkTimeoutEnabled: false,
});

Banner implementation

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

function AfkBanner({ myPlayerId }: { myPlayerId: string }) {
  const socket = useSocket();
  const [warning, setWarning] = useState<{ isSelf: boolean; seconds: number } | null>(null);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

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

    socket.on('tictactoe:afk_warning', ({ playerId, secondsRemaining }) => {
      setWarning({ isSelf: playerId === myPlayerId, seconds: secondsRemaining });
      if (intervalRef.current) clearInterval(intervalRef.current);
      intervalRef.current = setInterval(() => {
        setWarning(prev => {
          if (!prev || prev.seconds <= 1) { clearInterval(intervalRef.current!); return null; }
          return { ...prev, seconds: prev.seconds - 1 };
        });
      }, 1000);
    });

    socket.on('tictactoe:afk_warning_cleared', () => {
      clearInterval(intervalRef.current!);
      setWarning(null);
    });

    return () => {
      socket.off('tictactoe:afk_warning');
      socket.off('tictactoe:afk_warning_cleared');
      clearInterval(intervalRef.current!);
    };
  }, [socket, myPlayerId]);

  if (!warning) return null;

  return (
    <div className={`px-4 py-2 rounded font-semibold flex justify-between ${
      warning.isSelf ? 'bg-red-600 text-white' : 'bg-yellow-600 text-white'
    }`}>
      <span>
        {warning.isSelf ? "⚠️ Place your mark or you'll forfeit!" : '⏳ Opponent is AFK'}
      </span>
      <span className="font-mono">{warning.seconds}s</span>
    </div>
  );
}
Using the SDK? The TictactoeBoard component from @beta-gamer/react, @beta-gamer/react-native, and @beta-gamer/angular includes a built-in AFK banner by default. Pass showAfkWarning={false} to disable it and use your own implementation below.
⚠️ Always show the AFK banner. Players should never be forfeited without a visible countdown.
Beta Gamer GaaS API — questions? support@beta-gamer.com