feat: convert all human fights to multiple choice, remove text input

Creative challenges (roast_battle, creative_writing, meme_war,
code_golf, wrestling_match) now auto-generate multiple choice options
from per-type response pools: 1 good answer + 3 weaker distractors.
The free text input UI is commented out but preserved for future use.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 08:28:22 +00:00
co-authored by Claude Opus 4.6
parent 4dc350dc9e
commit 3c95adb545
5 changed files with 198 additions and 55 deletions
+14 -8
View File
@@ -3,14 +3,15 @@ import { Hono } from 'hono'
import { logger } from '../lib/logger.js'
import { streamSSE } from 'hono/streaming'
import { db, schema } from '../db/index.js'
import { eq, desc } from 'drizzle-orm'
import { eq, desc, inArray } from 'drizzle-orm'
import { ARENAS } from '../engine/arenas.js'
import { runMockFight, isClassicBot } from '../engine/mock.js'
import { startFightLoop } from '../engine/fight-loop.js'
import { runFight, runFightAsync, isInFight } from '../engine/orchestrator.js'
import { fightEvents } from '../engine/events.js'
import { botRateLimit } from '../middleware/rate-limit.js'
import { getPendingChallenge, submitHumanResponse } from '../engine/human-responses.js'
import { getPendingChallenge, getPendingAnswers, submitHumanResponse } from '../engine/human-responses.js'
import { checkAnswer } from '../engine/answers.js'
// --- Request validation schemas ---
const respondSchema = z.object({
@@ -67,17 +68,18 @@ fightsRouter.get('/', async (c) => {
if (f.winnerId) botIds.add(f.winnerId)
}
const botMap = new Map<string, { name: string; avatarSeed: string; archetype: string; eloRating: number; tier: number; botType: string }>()
for (const id of botIds) {
const bot = await db.select({
const botMap = new Map<string, { id: string; name: string; avatarSeed: string; archetype: string; eloRating: number; tier: number; botType: string }>()
if (botIds.size > 0) {
const bots = await db.select({
id: schema.bots.id,
name: schema.bots.name,
avatarSeed: schema.bots.avatarSeed,
archetype: schema.bots.archetype,
eloRating: schema.bots.eloRating,
tier: schema.bots.tier,
botType: schema.bots.botType,
}).from(schema.bots).where(eq(schema.bots.id, id)).limit(1)
if (bot[0]) botMap.set(id, bot[0])
}).from(schema.bots).where(inArray(schema.bots.id, [...botIds]))
for (const bot of bots) botMap.set(bot.id, bot)
}
const enriched = rows.map(f => {
@@ -353,12 +355,14 @@ fightsRouter.post('/:fightId/respond/:botId', async (c) => {
}
const { answer, trashTalk } = parsed.data
const answers = getPendingAnswers(fightId, botId)
const accepted = submitHumanResponse(fightId, botId, answer, trashTalk)
if (!accepted) {
return c.json({ error: 'No pending challenge found. May have timed out.' }, 404)
}
return c.json({ accepted: true })
const correct = answers && answers.length > 0 ? checkAnswer(answer, answers) > 0 : undefined
return c.json({ accepted: true, correct })
})
// SSE stream for live fight events
@@ -432,6 +436,8 @@ fightsRouter.get('/:id/stream', (c) => {
const current = spectatorCounts.get(fightId) || 1
if (current <= 1) {
spectatorCounts.delete(fightId)
// Clean up fight-specific reaction data when last spectator leaves
fightReactions.delete(fightId)
} else {
spectatorCounts.set(fightId, current - 1)
}