human fight sequence fix
This commit is contained in:
@@ -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?.()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user