2026-03-08 23:39:20 +00:00
|
|
|
import { ref, type Ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
export function useHumanChallenge(
|
|
|
|
|
fightId: Ref<string>,
|
|
|
|
|
myBotId: Ref<string | null>,
|
|
|
|
|
) {
|
|
|
|
|
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<string[]>([])
|
|
|
|
|
const roundCooldown = ref(0)
|
|
|
|
|
const pendingChallengeData = ref<{ data: any; receivedAt: number } | null>(null)
|
|
|
|
|
|
|
|
|
|
let humanPollHandle: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
let timerHandle: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
let cooldownHandle: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
|
|
|
|
|
function applyChallenge(data: any, receivedAt?: number) {
|
|
|
|
|
const elapsed = receivedAt ? Date.now() - receivedAt : 0
|
|
|
|
|
const remaining = Math.max(1000, (data.timeoutMs || 8000) - 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) {
|
|
|
|
|
if (humanChoices.value.length > 0 && !humanAnswer.value.trim()) {
|
|
|
|
|
humanAnswer.value = humanChoices.value[Math.floor(Math.random() * humanChoices.value.length)]
|
|
|
|
|
}
|
2026-03-09 07:47:43 +00:00
|
|
|
void submitHumanAnswer()
|
2026-03-08 23:39:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 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) {
|
|
|
|
|
humanAnswer.value = choice
|
2026-03-09 07:47:43 +00:00
|
|
|
void submitHumanAnswer()
|
2026-03-08 23:39:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.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
|
2026-03-09 07:47:43 +00:00
|
|
|
void pollForChallenge()
|
|
|
|
|
humanPollHandle = setInterval(() => { void pollForChallenge() }, 400)
|
2026-03-08 23:39:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|