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) }