From 68e292183ad2ecd2a41233e3ce2fc81e4843ebb6 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 19:02:35 +0000 Subject: [PATCH] fix: polling bots play practice fights as human players in browser Polling bots have no external script running during practice mode, so the poll would time out giving empty answers. Now overrides the webhook URL to human.local so the browser UI handles challenges. Co-Authored-By: Claude Opus 4.6 --- server/src/engine/orchestrator.ts | 3 ++- server/src/routes/fights.ts | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/server/src/engine/orchestrator.ts b/server/src/engine/orchestrator.ts index 31414a1..d3511cb 100644 --- a/server/src/engine/orchestrator.ts +++ b/server/src/engine/orchestrator.ts @@ -668,12 +668,13 @@ export async function runFight(botAId: string, botBId: string, mode: 'free' | 'r } /** Creates the fight record and returns the ID immediately. Rounds run in background. */ -export async function runFightAsync(botAId: string, botBId: string, mode: 'free' | 'ranked' = 'free'): Promise { +export async function runFightAsync(botAId: string, botBId: string, mode: 'free' | 'ranked' = 'free', overrides?: { botAWebhookUrl?: string }): Promise { if (botAId === botBId) throw new Error('A bot cannot fight itself') if (activeFighters.has(botAId)) throw new Error(`Bot ${botAId} is already in a fight`) if (activeFighters.has(botBId)) throw new Error(`Bot ${botBId} is already in a fight`) const [botA, botB] = await loadBots(botAId, botBId) + if (overrides?.botAWebhookUrl) botA.webhookUrl = overrides.botAWebhookUrl const arena = randomArena() const fightId = await createFightRecord(botA, botB, arena, mode) activeFighters.set(botAId, fightId) diff --git a/server/src/routes/fights.ts b/server/src/routes/fights.ts index 6894d56..63d4b2c 100644 --- a/server/src/routes/fights.ts +++ b/server/src/routes/fights.ts @@ -11,7 +11,7 @@ import { runFight, runFightAsync, isInFight, getActiveFightId } from '../engine/ import { fightEvents } from '../engine/events.js' import { botRateLimit } from '../middleware/rate-limit.js' import { getPendingChallenge, getPendingAnswers, submitHumanResponse } from '../engine/human-responses.js' -import { getPendingPollChallenge, submitPollResponse } from '../engine/poll-responses.js' +import { getPendingPollChallenge, submitPollResponse, isPollingBot } from '../engine/poll-responses.js' import { authenticateBot } from '../middleware/bot-auth.js' import { checkAnswer } from '../engine/answers.js' @@ -310,7 +310,9 @@ fightsRouter.post('/practice/:botId', botRateLimit(10_000), async (c) => { let fightId: string try { - fightId = await runFightAsync(botId, opponent.id, 'free') + // Polling bots play practice as human players (answer in browser) + const overrides = isPollingBot(bot.webhookUrl) ? { botAWebhookUrl: 'http://human.local/' } : undefined + fightId = await runFightAsync(botId, opponent.id, 'free', overrides) } catch (err) { const msg = err instanceof Error ? err.message : 'Fight failed to start' return c.json({ error: msg }, 400)