import { ref, type Ref } from 'vue' export function useHumanChallenge( fightId: Ref, myBotId: Ref, ) { const humanChallenge = ref<{ type: string label: string prompt: string roundNumber: number remainingMs: number scoring: string } | null>(null) const humanAnswer = ref('') const humanTimer = ref(5) const humanSubmitted = ref(false) const humanChoices = ref([]) const roundCooldown = ref(0) const pendingChallengeData = ref<{ data: any; receivedAt: number } | null>(null) let humanPollHandle: ReturnType | null = null let timerHandle: ReturnType | null = null let cooldownHandle: ReturnType | null = null function applyChallenge(data: any, receivedAt?: number) { const elapsed = receivedAt ? Date.now() - receivedAt : 0 // Cap timeout for multiple choice — you're just tapping a button, not typing const hasChoices = data.choices && data.choices.length > 0 const baseTimeout = hasChoices ? Math.min(data.timeoutMs || 8000, 10000) : (data.timeoutMs || 8000) const remaining = Math.max(1000, baseTimeout - elapsed) humanChallenge.value = { type: data.type, label: data.label, prompt: data.prompt, roundNumber: data.roundNumber || data.round, remainingMs: remaining, scoring: data.scoring, } humanChoices.value = data.choices || [] humanAnswer.value = '' humanSubmitted.value = false humanTimer.value = Math.ceil(remaining / 1000) startTimer() } function startTimer() { if (timerHandle) clearInterval(timerHandle) timerHandle = setInterval(() => { humanTimer.value-- if (humanTimer.value <= 0) { if (timerHandle) clearInterval(timerHandle) if (!humanSubmitted.value) { // BUG-9: Submit empty timeout instead of random choice humanSubmitted.value = true void submitTimeout() } } }, 1000) } async function submitHumanAnswer() { if (!myBotId.value || !humanChallenge.value || humanSubmitted.value) return if (!humanAnswer.value.trim()) return humanSubmitted.value = true if (timerHandle) clearInterval(timerHandle) try { await fetch(`/api/fights/${fightId.value}/respond/${myBotId.value}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ answer: humanAnswer.value.trim() }), }) } catch (err) { console.warn('[HumanChallenge] submitHumanAnswer failed:', err) } } function submitChoice(choice: string) { if (humanSubmitted.value) return // Prevent double-tap humanAnswer.value = choice humanSubmitted.value = true // Lock immediately before async void submitHumanAnswer() } async function submitTimeout() { 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: '', timeout: true }), }) } catch { // Server will time out on its own } } async function pollForChallenge() { if (!myBotId.value) return try { const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`) if (!res.ok) return const data = await res.json() if (data.pending) { if (roundCooldown.value > 0) { if (!pendingChallengeData.value || (data.roundNumber && data.roundNumber > (pendingChallengeData.value.data.roundNumber || 0))) { pendingChallengeData.value = { data, receivedAt: Date.now() } } return } 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 humanSubmitted.value = false } else if (humanSubmitted.value) { humanChallenge.value = null } } catch (err) { console.warn('[HumanChallenge] pollForChallenge failed:', err) } } function startHumanPolling() { if (!myBotId.value) return void pollForChallenge() humanPollHandle = setInterval(() => { void pollForChallenge() }, 400) } function stopHumanPolling() { if (humanPollHandle) { clearInterval(humanPollHandle); humanPollHandle = null } if (timerHandle) { clearInterval(timerHandle); timerHandle = null } if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null } } function startCooldown(seconds: number) { roundCooldown.value = seconds cooldownHandle = setInterval(() => { roundCooldown.value-- if (roundCooldown.value <= 0) { if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null } if (pendingChallengeData.value) { applyChallenge(pendingChallengeData.value.data, pendingChallengeData.value.receivedAt) pendingChallengeData.value = null } } }, 1000) } function clearChallenge() { humanChallenge.value = null humanSubmitted.value = false if (timerHandle) { clearInterval(timerHandle); timerHandle = null } } function resetState() { humanChallenge.value = null humanAnswer.value = '' humanSubmitted.value = false humanChoices.value = [] roundCooldown.value = 0 pendingChallengeData.value = null } function handleSSEChallenge(sseData: any) { if (roundCooldown.value > 0) { pendingChallengeData.value = { data: sseData, receivedAt: Date.now() } } else { applyChallenge(sseData) } } return { humanChallenge, humanAnswer, humanTimer, humanSubmitted, humanChoices, roundCooldown, pendingChallengeData, applyChallenge, submitHumanAnswer, submitChoice, pollForChallenge, startHumanPolling, stopHumanPolling, startCooldown, clearChallenge, resetState, handleSSEChallenge, } }