feat: human vs AI mode — live typing challenges, baby growth system, SSE rounds

- Add choose-mode step: "I BUILD BOTS" vs "I FIGHT MYSELF" paths
- Human registration with baby avatar picker, no webhook required
- Live fight scene with SSE round streaming and real-time challenge UI
- 5-second timer per round, submit answers via browser
- Baby → toddler → kid → teen → adult → hero → super growth stages
- Huge sparkly baby eyes, diapers, pacifiers, bibs, rattles, rosy cheeks
- Speech bubble positioning fix (pushed to outside of sprite)
- Canvas text rendering via offscreen canvas to bypass kaplay color issues
- Voice timing improvements: await pauses between voice lines and hits
- 30 devastating announcement lines, 15 critical/hit word variants
- Orchestrator human player detection + waitForHumanResponse system
- Server endpoints: GET /challenge/:botId, POST /respond/:botId
- Human player auth: register-human route, isHuman flag on login

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 15:20:14 +00:00
co-authored by Claude Opus 4.6
parent 56785cfdea
commit 8448f1d823
15 changed files with 1805 additions and 85 deletions
+20 -1
View File
@@ -7,6 +7,7 @@ import { scoreRound, calculateElo, calculateTier } from './scoring.js'
import { fightEvents } from './events.js'
import { generateMockBotResponse } from './mock.js'
import { setCooldown } from './queue.js'
import { isHumanPlayer, waitForHumanResponse } from './human-responses.js'
interface BotRecord {
id: string
@@ -68,6 +69,7 @@ function isAllowedWebhookUrl(url: string): boolean {
}
export { isAllowedWebhookUrl }
export { isHumanPlayer } from './human-responses.js'
// Size-limited body reader to prevent OOM
async function readLimitedBody(res: Response, maxBytes: number): Promise<string> {
@@ -202,6 +204,23 @@ async function getBotResponse(
opponent: { name: string; wins: number; losses: number },
arena: Arena,
): Promise<WebhookResponse> {
if (isHumanPlayer(bot.webhookUrl)) {
console.log(`[fight] ${bot.name} is human player, waiting for browser response`)
emit(fightId, 'human_challenge', {
botId: bot.id,
round: roundNumber,
type: challenge.type,
label: challenge.label,
prompt: challenge.prompt,
timeoutMs: 8000,
scoring: challenge.scoring,
})
const start = Date.now()
const result = await waitForHumanResponse(fightId, bot.id, challenge, roundNumber)
const elapsed = Date.now() - start
return { answer: result.answer, trashTalk: result.trashTalk, timeMs: elapsed, timedOut: result.timedOut, error: false }
}
if (isMockBot(bot.webhookUrl)) {
console.log(`[fight] ${bot.name} is mock bot, generating response`)
const mock = generateMockBotResponse(challenge, bot.name)
@@ -250,7 +269,7 @@ async function createFightRecord(botA: BotRecord, botB: BotRecord, arena: Arena)
// Track webhook errors per bot
async function trackWebhookResult(botId: string, webhookUrl: string, succeeded: boolean) {
if (isMockBot(webhookUrl)) return
if (isMockBot(webhookUrl) || isHumanPlayer(webhookUrl)) return
if (succeeded) {
await db.update(schema.bots).set({ consecutiveErrors: 0 }).where(eq(schema.bots.id, botId))
} else {