human fight sequence fix

This commit is contained in:
Dorian
2026-03-11 10:34:08 +00:00
parent 29a0a48eb1
commit ea72c097c4
5 changed files with 80 additions and 37 deletions
+2 -1
View File
@@ -65,6 +65,7 @@ export function useFightPolling(fightId: Ref<string>) {
function startPolling(callbacks?: {
onFinished?: () => void
onTimeout?: () => void
keepLive?: boolean // Don't set isLive=false (human fights manage lifecycle via SSE)
}) {
pollCount = 0
pollHandle = setInterval(async () => {
@@ -72,7 +73,7 @@ export function useFightPolling(fightId: Ref<string>) {
const s = await loadFight()
if (s === 'finished') {
if (!eventSource) {
isLive.value = false
if (!callbacks?.keepLive) isLive.value = false
disconnectSSE()
stopPolling()
callbacks?.onFinished?.()
+38 -23
View File
@@ -2,7 +2,10 @@ import { ref, type Ref } from 'vue'
// Buffer to ensure client timer finishes BEFORE server timer
// (accounts for SSE delivery delay + network latency)
const CLIENT_TIMER_BUFFER_MS = 1500
const CLIENT_TIMER_BUFFER_MS = 1000
// Minimum time (ms) to display a challenge — even if server has less remaining
const MIN_DISPLAY_MS = 5000
export function useHumanChallenge(
fightId: Ref<string>,
@@ -28,19 +31,26 @@ export function useHumanChallenge(
let humanPollHandle: ReturnType<typeof setInterval> | null = null
let timerHandle: ReturnType<typeof setInterval> | null = null
let cooldownHandle: ReturnType<typeof setInterval> | null = null
// Track which round was last applied to prevent double-apply from SSE+polling
let lastAppliedRound = 0
function applyChallenge(data: any, receivedAt?: number) {
const roundNum = data.roundNumber || data.round
// Deduplicate: don't re-apply the same round
if (roundNum && roundNum === lastAppliedRound && humanChallenge.value) return
lastAppliedRound = roundNum || 0
const elapsed = receivedAt ? Date.now() - receivedAt : 0
// Use server-computed remainingMs when available (from polling), otherwise derive from timeoutMs
const hasChoices = data.choices && data.choices.length > 0
const rawTimeout = data.remainingMs || (hasChoices ? Math.min(data.timeoutMs || 8000, 10000) : (data.timeoutMs || 8000))
const rawTimeout = data.remainingMs || (data.timeoutMs || 8000)
// Subtract buffer so client always submits before server times out
const remaining = Math.max(1000, rawTimeout - elapsed - CLIENT_TIMER_BUFFER_MS)
// But guarantee at least MIN_DISPLAY_MS so the user always has time to read + answer
const remaining = Math.max(MIN_DISPLAY_MS, rawTimeout - elapsed - CLIENT_TIMER_BUFFER_MS)
humanChallenge.value = {
type: data.type,
label: data.label,
prompt: data.prompt,
roundNumber: data.roundNumber || data.round,
roundNumber: roundNum,
remainingMs: remaining,
scoring: data.scoring,
}
@@ -65,29 +75,36 @@ export function useHumanChallenge(
}, 1000)
}
async function submitHumanAnswer() {
if (!myBotId.value || !humanChallenge.value || humanSubmitted.value) return
if (!humanAnswer.value.trim()) return
humanSubmitted.value = true
if (timerHandle) clearInterval(timerHandle)
/** Send an answer to the server (shared by text input + choice buttons) */
async function sendAnswer(answer: string) {
if (!myBotId.value || !humanChallenge.value) return
try {
await fetch(`/api/fights/${fightId.value}/respond/${myBotId.value}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answer: humanAnswer.value.trim() }),
body: JSON.stringify({ answer }),
})
} catch (err) {
console.warn('[HumanChallenge] submitHumanAnswer failed:', err)
console.warn('[HumanChallenge] sendAnswer failed:', err)
}
}
/** Submit from text input */
async function submitHumanAnswer() {
if (!myBotId.value || !humanChallenge.value || humanSubmitted.value) return
if (!humanAnswer.value.trim()) return
humanSubmitted.value = true
if (timerHandle) clearInterval(timerHandle)
await sendAnswer(humanAnswer.value.trim())
}
/** Submit from multiple-choice button tap */
function submitChoice(choice: string) {
if (humanSubmitted.value) return // Prevent double-tap
humanAnswer.value = choice
humanSubmitted.value = true // Lock immediately before async
void submitHumanAnswer()
humanSubmitted.value = true
if (timerHandle) clearInterval(timerHandle)
void sendAnswer(choice)
}
async function submitTimeout() {
@@ -110,12 +127,11 @@ export function useHumanChallenge(
try {
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`)
if (res.status === 429) {
// Back off on rate limit — skip next few polls
pollBackoff = Math.min(pollBackoff + 2, 8)
return
}
if (!res.ok) return
pollBackoff = Math.max(0, pollBackoff - 1) // Recover gradually
pollBackoff = Math.max(0, pollBackoff - 1)
const data = await res.json()
if (data.pending) {
@@ -129,7 +145,6 @@ 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') {
@@ -160,7 +175,6 @@ export function useHumanChallenge(
}
function startCooldown(seconds: number) {
// Clear any existing cooldown interval to prevent double-decrement
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
roundCooldown.value = seconds
cooldownHandle = setInterval(() => {
@@ -178,6 +192,7 @@ export function useHumanChallenge(
function clearChallenge() {
humanChallenge.value = null
humanSubmitted.value = false
pendingChallengeData.value = null
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
}
@@ -195,12 +210,13 @@ export function useHumanChallenge(
pendingChallengeData.value = null
entrancePlaying.value = false
animatingRound.value = false
lastAppliedRound = 0
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
}
/** Mark entrance as playing — queues any challenges that arrive during animation */
function setEntrancePlaying(playing: boolean) {
entrancePlaying.value = playing
// When entrance finishes, flush any queued challenge
if (!playing && pendingChallengeData.value) {
applyChallenge(pendingChallengeData.value.data, pendingChallengeData.value.receivedAt)
pendingChallengeData.value = null
@@ -208,7 +224,6 @@ export function useHumanChallenge(
}
function handleSSEChallenge(sseData: any) {
// Queue challenges during entrance animation, round animation, or cooldown
if (entrancePlaying.value || animatingRound.value || roundCooldown.value > 0) {
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
} else {