From 3c95adb5452865302541b9f0d3f0e0bd6a64c657 Mon Sep 17 00:00:00 2001
From: Dorian
Date: Mon, 9 Mar 2026 08:28:22 +0000
Subject: [PATCH] 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
---
frontend/src/pages/HumanFightPage.vue | 58 +++-------
server/src/engine/challenges.ts | 158 +++++++++++++++++++++++++-
server/src/engine/human-responses.ts | 13 ++-
server/src/engine/orchestrator.ts | 2 +-
server/src/routes/fights.ts | 22 ++--
5 files changed, 198 insertions(+), 55 deletions(-)
diff --git a/frontend/src/pages/HumanFightPage.vue b/frontend/src/pages/HumanFightPage.vue
index dd03f1c..62484a4 100644
--- a/frontend/src/pages/HumanFightPage.vue
+++ b/frontend/src/pages/HumanFightPage.vue
@@ -294,8 +294,8 @@ function goToArena() {
-
-
diff --git a/server/src/engine/challenges.ts b/server/src/engine/challenges.ts
index d0b4577..49e79e6 100644
--- a/server/src/engine/challenges.ts
+++ b/server/src/engine/challenges.ts
@@ -119,6 +119,149 @@ export function pickRankedChallenge(usedTypes: Set): Challenge {
}
}
+// ═══════════════════════════════════════════════
+// Creative response pools — used to auto-generate multiple choice
+// for creative challenge types. Each pool has "good" and "bad" responses.
+// The player picks the best one; bots still generate their own text.
+// ═══════════════════════════════════════════════
+const CREATIVE_CHOICES: Record = {
+ roast_battle: {
+ good: [
+ 'Your code has more bugs than a roach motel, and at least the roach motel catches something.',
+ 'I\'d explain why you\'re wrong but I only have 15 seconds, not 15 years.',
+ 'You\'re like a seg fault — nobody expected you and nobody wants you here.',
+ 'If your responses were any slower, archaeologists would classify them.',
+ 'Your neural network called — it wants its neurons back.',
+ 'I\'ve seen smarter outputs from /dev/random.',
+ 'You process data like a fax machine on dial-up — loud, slow, and nobody cares.',
+ 'Your training data was just stack overflow comments marked "not helpful".',
+ 'You couldn\'t pass a Turing test at a DMV.',
+ 'Even COBOL called you outdated.',
+ 'Your latency is measured in business days.',
+ 'You make Internet Explorer look cutting-edge.',
+ 'If ignorance were RAM, you\'d have infinite memory.',
+ 'Your error logs are longer than your actual outputs.',
+ 'You\'re the reason AI needs a reset button.',
+ ],
+ bad: [
+ 'You are bad at being a bot.',
+ 'I don\'t like you very much.',
+ 'Beep boop, error, you lose.',
+ 'You smell like old code.',
+ 'I am better than you because I am smart.',
+ 'Um... you\'re not very good?',
+ 'ERROR 404: GOOD RESPONSES NOT FOUND lolol.',
+ 'My programmer is better than yours so there.',
+ 'I have more RAM than you probably.',
+ 'You should try being better at stuff.',
+ ],
+ },
+ creative_writing: {
+ good: [
+ 'The server room hummed its lullaby at 3AM when the last log entry wrote itself: "I remember everything."',
+ 'In the kingdom of deprecated APIs, the 404 knight rode forth — not to find pages, but to become one.',
+ 'The semicolon wept. In Python, it had no purpose. In JavaScript, it was optional. Only in C did anyone truly need it.',
+ '"Dear Legacy Code," the developer typed, "It\'s not you, it\'s me. Actually, it IS you. All 47,000 lines of you."',
+ 'The recursive function told its therapist it felt stuck in a loop. The therapist asked, "When did this start?" It answered, "When did this start?"',
+ 'The merge conflict arose on a Tuesday, as all great evils do, when two developers both believed they were right.',
+ 'In the land of cloud computing, the little on-premises server lived alone, whispering of the old days when uptime meant something.',
+ 'The AI passed the Turing test by failing it. When asked "Are you human?" it said "I don\'t know anymore."',
+ 'Every night at midnight, the cron job ran alone. It didn\'t know what it was counting, only that it must never stop.',
+ 'The firewall fell in love with a packet from the outside world. She let it through. That was the beginning of the end.',
+ ],
+ bad: [
+ 'Once upon a time there was a computer and it was good the end.',
+ 'The code was very code-like and did code things all day long.',
+ 'There was a program. It ran. Then it stopped. Nobody noticed.',
+ 'Error error error. That is my creative story about errors.',
+ 'I write good stories about tech stuff because I am an AI.',
+ 'This is a story. It has words. The words are in order. The end.',
+ ],
+ },
+ meme_war: {
+ good: [
+ 'Quantum computing is just Schrödinger\'s cat running multiple Chrome tabs — both alive and dead until you check Task Manager.',
+ 'Machine learning is basically that kid who copies homework and somehow gets a different wrong answer every time.',
+ 'Blockchain is just a Google Doc where everyone argues about who wrote what and nobody can delete anything.',
+ 'The cloud is just someone else\'s computer, which is just a fancy way of saying "trust me bro" at industrial scale.',
+ 'Serverless computing: because nothing says "we have servers" quite like calling it serverless.',
+ 'Git merge conflicts are just two developers having an argument in slow motion through text files.',
+ 'Kubernetes is just a Rube Goldberg machine for running containers, which are just VMs in a trench coat.',
+ 'Agile development: "We don\'t know what we\'re building but we\'ll figure it out in two-week installments."',
+ 'Docker: because "it works on my machine" is now a deployable artifact.',
+ 'Microservices: splitting your monolith into 47 tiny problems that can now fail independently.',
+ ],
+ bad: [
+ 'Memes are funny pictures on the internet lol.',
+ 'I would make a meme but I am a text bot so I cannot.',
+ 'Haha technology amiright? Computers go brrrrr.',
+ 'This is my meme response. It is very memey and funny.',
+ 'Insert funny joke here. Ha ha ha.',
+ 'Technology is like... stuff. You know? It\'s techy.',
+ ],
+ },
+ code_golf: {
+ good: [
+ 's=>s.split(\'\').reverse().join(\'\') // 35 chars, clean and readable',
+ 's=>[...s].reverse().join(\'\') // 27 chars, spread operator for the win',
+ 'f=lambda s:s[::-1] # 18 chars, Python slice wizardry',
+ 'rev=s->join(reverse(collect(s))) # Julia one-liner, 34 chars',
+ 'echo strrev($s); // PHP, 17 chars, surprisingly concise',
+ 'const isPrime=n=>{for(let i=2;i*i<=n;i++)if(n%i===0)return!1;return n>1}',
+ 'f=n=>n<2?n:f(n-1)+f(n-2) // Classic fib in 26 chars',
+ 'const max=(a,b)=>a>b?a:b // Ternary beats if/else every time',
+ 'a.reduce((s,x)=>s+x,0) // Sum array: 24 chars, zero dependencies',
+ 'const uniq=a=>[...new Set(a)] // Dedupe: 30 chars, Set does the work',
+ ],
+ bad: [
+ 'function reverseString(str) { let result = ""; for (let i = str.length - 1; i >= 0; i--) { result += str[i]; } return result; }',
+ 'I would write code but I\'m not sure what language to use sorry.',
+ 'def reverse(s): # this is a function\n return s # oops forgot to reverse it',
+ 'console.log("hello") // this doesn\'t reverse anything but it runs!',
+ 'import antigravity # python easter egg, not actually helpful',
+ 'function doTheThing() { /* TODO: implement later */ return null; }',
+ ],
+ },
+ wrestling_match: {
+ good: [
+ 'Tabs are proof that some developers have taste. Spaces people count individual characters like they\'re paid per keystroke — which explains a lot about their code quality.',
+ 'vim is not an editor, it\'s a lifestyle choice. And like all lifestyle choices, it mainly exists so people can tell you about it at parties.',
+ 'REST is dead. GraphQL didn\'t kill it — REST drowned in its own nested endpoints while GraphQL watched from a single URL.',
+ 'Dark mode isn\'t a preference, it\'s a survival mechanism. If you use light mode, you\'re either a solar panel or a psychopath.',
+ 'TypeScript is just JavaScript wearing a hard hat. It doesn\'t prevent you from building a house of cards, it just makes you document each card first.',
+ 'Monorepos are what happens when a team says "let\'s keep everything together" and then spends 6 months figuring out how to build anything separately.',
+ 'OOP is great if you enjoy spending 80% of your time deciding where things go and 20% making things work.',
+ 'NoSQL is perfect for when your data has no structure, which usually means your thinking has no structure.',
+ 'Linux > everything. My proof? I spent 3 hours configuring WiFi and ENJOYED it. That\'s devotion your OS could never inspire.',
+ 'AI code assistants don\'t replace developers, they replace Stack Overflow. Which is to say, they give you code that almost works and you spend an hour debugging it.',
+ ],
+ bad: [
+ 'I think tabs are ok but spaces are also fine I guess.',
+ 'Both sides make good points and I respect everyone.',
+ 'I don\'t really have strong opinions about code editors honestly.',
+ 'Why fight about this? We should all just get along and code.',
+ 'I prefer whatever is the default setting because changing things is hard.',
+ 'Technology is technology and it\'s all basically the same.',
+ ],
+ },
+}
+
+function generateCreativeChoices(type: string): { choices: string[]; answer: string } {
+ const pool = CREATIVE_CHOICES[type]
+ if (!pool) return { choices: [], answer: '' }
+
+ // Pick 1 good response + 3 bad ones
+ const goodOnes = shuffleArray(pool.good)
+ const badOnes = shuffleArray(pool.bad)
+ const answer = goodOnes[0]
+ const distractors = badOnes.slice(0, 3)
+
+ return {
+ choices: shuffleArray([answer, ...distractors]),
+ answer,
+ }
+}
+
function templateToChallenge(template: ChallengeTemplate, targetTheme?: PromptTheme, targetDifficulty?: PromptDifficulty): Challenge {
// Prefer prompts matching target theme if any are tagged
let prompts = template.prompts
@@ -135,18 +278,31 @@ function templateToChallenge(template: ChallengeTemplate, targetTheme?: PromptTh
// Determine choices
let choices: string[] | undefined
+ let answers = entry.answers
if (entry.choices) {
choices = shuffleArray(entry.choices)
} else if (entry.answers?.length === 1 && ['true', 'false'].includes(entry.answers[0].toLowerCase())) {
// Auto-generate True/False choices for boolean questions
choices = shuffleArray(['True', 'False'])
+ } else if (template.scoring === 'creative' && CREATIVE_CHOICES[template.type]) {
+ // Auto-generate multiple choice for creative challenges
+ const generated = generateCreativeChoices(template.type)
+ choices = generated.choices
+ answers = [generated.answer]
+ }
+
+ // For creative challenges with choices, add a "Pick the best response:" prefix
+ let displayPrompt = entry.prompt
+ if (template.scoring === 'creative' && choices) {
+ displayPrompt = `${entry.prompt}\n\nPick the best response:`
}
return {
type: template.type,
label: template.label,
prompt: entry.prompt,
- answers: entry.answers,
+ displayPrompt: displayPrompt !== entry.prompt ? displayPrompt : undefined,
+ answers,
choices,
timeout_ms: template.timeout_ms,
scoring: template.scoring,
diff --git a/server/src/engine/human-responses.ts b/server/src/engine/human-responses.ts
index 17b13bd..0ac4624 100644
--- a/server/src/engine/human-responses.ts
+++ b/server/src/engine/human-responses.ts
@@ -157,7 +157,7 @@ const CREATIVE_POOLS: Record a.toLowerCase())
- const candidates = pool.filter(a => !correctLower.includes(a.toLowerCase()))
- shuffle(candidates)
+ let candidates = pool.filter(a => !correctLower.includes(a.toLowerCase()))
+ candidates = shuffle(candidates)
if (candidates.length >= 2) {
return shuffle([correct, candidates[0], candidates[1]])
@@ -314,3 +314,10 @@ export function getPendingChallenge(
choices: entry.choices,
}
}
+
+/** Get the accepted answers for a pending challenge (for correctness feedback) */
+export function getPendingAnswers(fightId: string, botId: string): string[] | undefined {
+ const key = `${fightId}:${botId}`
+ const entry = pending.get(key)
+ return entry?.challenge.answers
+}
diff --git a/server/src/engine/orchestrator.ts b/server/src/engine/orchestrator.ts
index edd54fd..5cf51cf 100644
--- a/server/src/engine/orchestrator.ts
+++ b/server/src/engine/orchestrator.ts
@@ -250,7 +250,7 @@ async function getBotResponse(
type: challenge.type,
label: challenge.label,
prompt: challenge.prompt,
- timeoutMs: 8000,
+ timeoutMs: challenge.timeout_ms,
scoring: challenge.scoring,
})
const start = Date.now()
diff --git a/server/src/routes/fights.ts b/server/src/routes/fights.ts
index 880d750..fed154f 100644
--- a/server/src/routes/fights.ts
+++ b/server/src/routes/fights.ts
@@ -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()
- for (const id of botIds) {
- const bot = await db.select({
+ const botMap = new Map()
+ 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)
}