101 lines
2.6 KiB
TypeScript
101 lines
2.6 KiB
TypeScript
// In-memory store for pending human challenges.
|
|||
|
|
// When the fight engine needs a human's response, it stores the challenge here
|
||
|
|
// and waits for the browser to submit the answer via REST.
|
||
|
|
|
||
|
|
import type { Challenge } from './challenges.js'
|
||
|
|
|
||
|
|
interface PendingChallenge {
|
||
|
|
fightId: string
|
||
|
|
botId: string
|
||
|
|
challenge: Challenge
|
||
|
|
roundNumber: number
|
||
|
|
createdAt: number
|
||
|
|
resolve: (response: { answer: string; trashTalk?: string }) => void
|
||
|
|
timeoutHandle: ReturnType<typeof setTimeout>
|
||
|
|
}
|
||
|
|
|
||
|
|
const pending = new Map<string, PendingChallenge>()
|
||
|
|
|
||
|
|
const HUMAN_TIMEOUT_MS = 8_000
|
||
|
|
|
||
|
|
export function isHumanPlayer(webhookUrl: string): boolean {
|
||
|
|
return webhookUrl === 'http://human.local/'
|
||
|
|
}
|
||
|
|
|
||
|
|
export function waitForHumanResponse(
|
||
|
|
fightId: string,
|
||
|
|
botId: string,
|
||
|
|
challenge: Challenge,
|
||
|
|
roundNumber: number,
|
||
|
|
): Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }> {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
const key = `${fightId}:${botId}`
|
||
|
|
|
||
|
|
const timeoutHandle = setTimeout(() => {
|
||
|
|
pending.delete(key)
|
||
|
|
console.log(`[human] ${key} timed out after ${HUMAN_TIMEOUT_MS}ms`)
|
||
|
|
resolve({ answer: null, timedOut: true })
|
||
|
|
}, HUMAN_TIMEOUT_MS)
|
||
|
|
|
||
|
|
pending.set(key, {
|
||
|
|
fightId,
|
||
|
|
botId,
|
||
|
|
challenge,
|
||
|
|
roundNumber,
|
||
|
|
createdAt: Date.now(),
|
||
|
|
resolve: (response) => {
|
||
|
|
clearTimeout(timeoutHandle)
|
||
|
|
pending.delete(key)
|
||
|
|
resolve({ answer: response.answer, trashTalk: response.trashTalk, timedOut: false })
|
||
|
|
},
|
||
|
|
timeoutHandle,
|
||
|
|
})
|
||
|
|
|
||
|
|
console.log(`[human] waiting for response: ${key} round=${roundNumber} type=${challenge.type}`)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
export function submitHumanResponse(
|
||
|
|
fightId: string,
|
||
|
|
botId: string,
|
||
|
|
answer: string,
|
||
|
|
trashTalk?: string,
|
||
|
|
): boolean {
|
||
|
|
const key = `${fightId}:${botId}`
|
||
|
|
const entry = pending.get(key)
|
||
|
|
if (!entry) return false
|
||
|
|
console.log(`[human] response received: ${key} answer=${answer.slice(0, 80)}`)
|
||
|
|
entry.resolve({ answer: answer.slice(0, 2000), trashTalk: trashTalk?.slice(0, 200) })
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getPendingChallenge(
|
||
|
|
fightId: string,
|
||
|
|
botId: string,
|
||
|
|
): {
|
||
|
|
type: string
|
||
|
|
label: string
|
||
|
|
prompt: string
|
||
|
|
roundNumber: number
|
||
|
|
timeoutMs: number
|
||
|
|
remainingMs: number
|
||
|
|
scoring: string
|
||
|
|
} | null {
|
||
|
|
const key = `${fightId}:${botId}`
|
||
|
|
const entry = pending.get(key)
|
||
|
|
if (!entry) return null
|
||
|
|
|
||
|
|
const elapsed = Date.now() - entry.createdAt
|
||
|
|
const remaining = Math.max(0, HUMAN_TIMEOUT_MS - elapsed)
|
||
|
|
|
||
|
|
return {
|
||
|
|
type: entry.challenge.type,
|
||
|
|
label: entry.challenge.label,
|
||
|
|
prompt: entry.challenge.prompt,
|
||
|
|
roundNumber: entry.roundNumber,
|
||
|
|
timeoutMs: HUMAN_TIMEOUT_MS,
|
||
|
|
remainingMs: remaining,
|
||
|
|
scoring: entry.challenge.scoring,
|
||
|
|
}
|
||
|
|
}
|