From 112bcde515ba436fa90a8a530ff6991292670f17 Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 10 Mar 2026 23:26:36 +0000 Subject: [PATCH] human fight non multiple choice fix --- frontend/src/composables/useHumanChallenge.ts | 3 +++ frontend/src/pages/FightPage.vue | 2 +- server/src/engine/orchestrator.ts | 10 +++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/composables/useHumanChallenge.ts b/frontend/src/composables/useHumanChallenge.ts index e68d8c9..7c376be 100644 --- a/frontend/src/composables/useHumanChallenge.ts +++ b/frontend/src/composables/useHumanChallenge.ts @@ -113,6 +113,9 @@ export function useHumanChallenge( } if (!humanChallenge.value || humanChallenge.value.roundNumber !== data.roundNumber) { applyChallenge(data) + } else if (data.choices?.length && humanChoices.value.length === 0) { + // SSE fallback had no choices, polling found them — update + humanChoices.value = data.choices } } else if (data.fightStatus === 'finished') { humanChallenge.value = null diff --git a/frontend/src/pages/FightPage.vue b/frontend/src/pages/FightPage.vue index eb9d36d..9a703f4 100644 --- a/frontend/src/pages/FightPage.vue +++ b/frontend/src/pages/FightPage.vue @@ -553,7 +553,6 @@ async function fightAgain(botId: string) { window.history.replaceState({}, '', `/arena/${data.fightId}`) startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() }) if (isHumanFight.value) { - startHumanPolling() wireSSE() for (let i = 0; i < 20; i++) { await loadFight() @@ -563,6 +562,7 @@ async function fightAgain(botId: string) { await nextTick() await nextTick() if (liveFightData.value) await initLiveScene() + startHumanPolling() } } else { fightError.value = `Failed to start fight (${res.status})` diff --git a/server/src/engine/orchestrator.ts b/server/src/engine/orchestrator.ts index d3511cb..da03e85 100644 --- a/server/src/engine/orchestrator.ts +++ b/server/src/engine/orchestrator.ts @@ -265,6 +265,11 @@ async function getBotResponse( ): Promise { if (isHumanPlayer(bot.webhookUrl)) { logger.info('fight', `${bot.name} is human player, waiting for browser response`) + // Start waitForHumanResponse FIRST so the pending challenge is stored + // before the SSE event tells the frontend to fetch it (fixes race condition + // where frontend fetched before pending was set → no choices shown) + const start = Date.now() + const resultPromise = waitForHumanResponse(fightId, bot.id, challenge, roundNumber) emit(fightId, 'human_challenge', { botId: bot.id, round: roundNumber, @@ -274,8 +279,7 @@ async function getBotResponse( timeoutMs: challenge.timeout_ms, scoring: challenge.scoring, }) - const start = Date.now() - const result = await waitForHumanResponse(fightId, bot.id, challenge, roundNumber) + const result = await resultPromise const elapsed = Date.now() - start return { answer: result.answer, trashTalk: result.trashTalk, timeMs: elapsed, timedOut: result.timedOut, error: false } } @@ -389,7 +393,7 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec for (let round = 1; round <= MAX_ROUNDS; round++) { lastRound = round - const challenge = round === retroRound + const challenge = (round === retroRound && !hasHuman) ? generateRetroChallenge() : pickChallenge(usedTypes, arena.modifier, undefined, round, hasHuman) usedTypes.add(challenge.type)