Files
botfights/.claude/plans/greedy-skipping-lollipop.md
T
DorianandClaude Opus 4.6 610e799605 feat: SSE live fight spectating with spectator count
Enable real-time fight spectating for all live fights (not just human
fights). Multiple spectators can watch simultaneously via SSE. Spectator
count is tracked per-fight and broadcast with every SSE event.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:46:39 +00:00

9.9 KiB

Production-Grade Botfights: Implementation Plan

Context

Botfights is a competitive game where users register AI bots with webhook URLs. The server sends challenge questions to webhooks, scores responses, and updates rankings. The system works end-to-end but has critical fairness, reliability, and security gaps that must be fixed before real users compete for rankings. This plan makes the engine bulletproof and adds a rich TUI for the overnight fight loop.


Phase 1: Fight Integrity (Foundation)

Everything depends on fights producing correct, trustworthy results.

1.1 Schema additions

File: server/src/db/schema.ts, server/src/db/migrate.ts

  • Add to bots: lastFightAt TEXT, consecutiveErrors INTEGER DEFAULT 0, lastErrorAt TEXT
  • Add migration SQL for existing DBs

1.2 Transaction-wrap Elo updates

Files: server/src/engine/orchestrator.ts (lines 276-306), server/src/engine/mock.ts (lines 392-421)

  • Wrap fight finalization (status update + both bot stat updates) in a single SQLite transaction
  • Prevents partial Elo corruption on crash or concurrent fights

1.3 Block self-fights

Files: server/src/engine/orchestrator.ts, server/src/engine/mock.ts, server/src/routes/fights.ts

  • Add if (botAId === botBId) throw new Error('A bot cannot fight itself') at top of runFight, runFightAsync, runMockFight

1.4 Prevent concurrent fights for same bot

File: server/src/engine/orchestrator.ts

  • In-memory Set<string> of currently-fighting bot IDs
  • Check before starting, add on start, remove in .finally()

1.5 Crash recovery for stuck fights

File: server/src/engine/orchestrator.ts

  • In runFightAsync catch handler: mark fight status='cancelled', call fightEvents.cleanup()
  • On server startup: mark any status='live' fights older than 10 minutes as cancelled

1.6 Fix creative scoring (anti-gaming)

File: server/src/engine/scoring.ts (lines 166-174)

  • Replace estimateQuality with multi-factor heuristic: character diversity, word diversity, length window (30-400 ideal), speed bonus
  • Prevents gaming by dumping 500 chars of garbage text

1.7 Tighten factual answer checking

File: server/src/engine/answers.ts (line 78)

  • Add word-boundary awareness: containsWholeWord() helper using regex \b
  • Short answers (2 chars like "au", "fe") require near-exact match, not just containment
  • Prevents "I feel confident" matching accepted answer "fe"

1.8 Dampen mock-bot Elo farming

File: server/src/engine/orchestrator.ts

  • When one combatant is a mock bot, use K-factor 12 instead of 32
  • Real bot-vs-real bot fights keep K=32 for full stakes

Phase 2: Webhook Contract & Developer Experience

2.1 Response size limits

File: server/src/engine/orchestrator.ts (line 83)

  • Replace res.text() with size-limited reader (10KB max)
  • Truncate answer to 2000 chars, trash_talk to 200 chars
  • Prevents OOM attacks from malicious webhooks

2.2 SSRF protection

File: server/src/engine/orchestrator.ts (new isAllowedWebhookUrl function)

  • Block localhost, private IPs (10.x, 192.168.x, 172.16-31.x), AWS metadata (169.254.169.254), .local/.internal
  • Enforce HTTPS in production, allow HTTP in dev
  • Apply at registration (routes/bots.ts, routes/auth.ts) AND at call time

2.3 Pre-fight webhook verification

New file: server/src/engine/webhook-test.ts

  • testWebhook(url): sends a test challenge ("respond with {"answer": "pong"}"), validates response shape and latency
  • Returns { reachable, validResponse, latencyMs, error? }

Integrate into:

  • POST /api/auth/register -- test webhook before inserting bot. Reject with specific error if it fails.
  • POST /api/bots -- same
  • New route POST /api/bots/:name/test -- re-test webhook on demand (replaces weak /health check)

2.4 Webhook reliability tracking

File: server/src/engine/orchestrator.ts

  • After each webhook call: increment consecutiveErrors on failure, reset to 0 on success
  • If consecutiveErrors >= 5: mark bot isActive: false, skip in matchmaking
  • Bot owner must re-test webhook to reactivate

2.5 Add fight_id to webhook payload

File: server/src/engine/orchestrator.ts (line 48)

  • Thread fightId through callWebhook and getBotResponse
  • Bot developers can correlate challenge POSTs to specific fights for debugging

Phase 3: Anti-Gaming & Security

3.1 Rate limiting

New file: server/src/middleware/rate-limit.ts

  • Simple in-memory rate limiter (no new deps), per-IP sliding window
  • Apply: POST /api/bots (5/hr), POST /api/auth/register (5/hr), POST /api/queue/join (1 per 10s per bot), all other POSTs (60/min)

3.2 Case-insensitive name uniqueness

Files: server/src/routes/bots.ts (line 39), server/src/routes/auth.ts (line 78)

  • Force bot names to lowercase at registration time
  • Prevents name squatting ("MyBot" vs "mybot")

3.3 Queue cooldowns

File: server/src/engine/queue.ts

  • In-memory Map<string, number> of post-fight cooldowns (15 seconds)
  • Set cooldown after fight completes (called from orchestrator.ts)
  • Reject queue join if cooldown active

3.4 Concurrent fight checks in queue

File: server/src/engine/queue.ts

  • Check the activeFighters set (from 1.4) before allowing queue join
  • Prevents a bot from queueing while already in a fight

Phase 4: TUI Fight Loop

Minimal deps -- only chalk for colors. All layout via ANSI codes and Unicode box drawing.

4.1 Add dependency

chalk@5 to server/package.json

4.2 TUI state tracker

New file: server/src/tui/state.ts

  • TuiState interface: fight count, KOs, perfects, draws, errors, current fight (bots/HP/round/events), recent fights, leaderboard, biggest upset, Elo movers, elapsed time

4.3 TUI renderer

New file: server/src/tui/renderer.ts

+=================== BOTFIGHTS OVERNIGHT LOOP ====================+
|  Fight #47 of 200                    Elapsed: 12m 34s            |
|  Style: mixed                        Rate: 3.8 fights/min       |
+------------------------------------------------------------------+
|                                                                  |
|  skull_crusher_9000 (1820)  vs  boaty_mcbotface (1150)           |
|  [================----] 163 HP  vs  [====----------------] 47 HP |
|  Round 6/10 -- speed_blitz                                       |
|  >> skull_crusher answered in 234ms (CORRECT)                    |
|  >> boaty_mcbotface timed out! FREE HIT!                         |
|                                                                  |
+========================= STATS ==================================+
|  Fights: 47 completed, 0 errors                                  |
|  KOs: 31 (66%)  |  Perfects: 4  |  Draws: 2                     |
|  Biggest upset: boaty_mcbotface beat the_architect!               |
+========================= LEADERBOARD ============================+
|  #1  the_architect      1980  52W-8L   LEGEND                    |
|  #2  chad_gpt           1950  48W-10L  LEGEND                    |
|  #3  skull_crusher_9000 1820  35W-12L  DIAMOND                   |
+========================= RECENT ================================+
|  #47 skull_crusher vs boaty       -> skull_crusher (KO R6)       |
|  #46 chad_gpt vs lorem_ipsum     -> chad_gpt (PERFECT R3)       |
|  #45 regex_ronin vs the_intern   -> regex_ronin (Decision)       |
+==================================================================+
  • Single buffered write to avoid flicker
  • Handles terminal resize via process.stdout.on('resize')
  • Tier colors via chalk

4.4 Refactor fight loop with callbacks

File: server/src/engine/fight-loop.ts

  • Add callback options: onFightStart, onRoundComplete, onFightComplete, onError
  • Wire event bus so TUI gets live round-by-round updates

4.5 Rewrite CLI

File: server/src/fight-loop-cli.ts

  • Create TUI state + renderer, pass callbacks to fight loop
  • Graceful SIGINT: show final summary screen
  • Same CLI args (--max, --interval, --style)

4.6 Final summary screen

On loop end or Ctrl+C: duration, total fights, KO/perfect/draw rates, top Elo movers, biggest upset, most active bot


Phase 5: Verification

5.1 Manual test sequence

  1. pnpm seed -- verify schema migrations run
  2. pnpm dev -- verify server starts, orphaned fights cleaned up on startup
  3. Register bot with bad webhook URL -> verify rejection with specific error
  4. Register bot with valid webhook -> verify test challenge sent and validated
  5. Try self-fight via /api/fights/matchmake -> verify blocked
  6. Run pnpm fight-loop --max=10 -> verify TUI renders, stats update live
  7. Kill process mid-fight, restart -> verify stuck fights cleaned up
  8. Send oversized response from test webhook -> verify 10KB limit
  9. Rapid-fire queue joins -> verify rate limiting and cooldowns

Files Summary

Modified (11 files):

  • server/src/db/schema.ts -- new columns
  • server/src/db/migrate.ts -- migration SQL
  • server/src/engine/orchestrator.ts -- transactions, self-fight block, concurrent guard, crash recovery, response limits, SSRF, reliability tracking, mock Elo dampening, fight_id in payload
  • server/src/engine/mock.ts -- transaction wrap, self-fight block
  • server/src/engine/scoring.ts -- creative scoring rewrite
  • server/src/engine/answers.ts -- word boundary fixes
  • server/src/engine/queue.ts -- cooldowns, concurrent fight checks, export activeFighters check
  • server/src/engine/fight-loop.ts -- callback options for TUI
  • server/src/fight-loop-cli.ts -- TUI integration
  • server/src/app.ts -- rate limiting, startup cleanup
  • server/package.json -- add chalk

Created (4 files):

  • server/src/engine/webhook-test.ts -- pre-fight webhook verification
  • server/src/middleware/rate-limit.ts -- rate limiter
  • server/src/tui/state.ts -- TUI state tracker
  • server/src/tui/renderer.ts -- TUI renderer

Execution order: Phase 1 (1.1-1.8) -> Phase 2 (2.1-2.5) -> Phase 3 (3.1-3.4) -> Phase 4 (4.1-4.6) -> Phase 5 verification