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:
co-authored by
Claude Opus 4.6
parent
4dc350dc9e
commit
3c95adb545
@@ -294,8 +294,8 @@ function goToArena() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Multiple choice buttons -->
|
||||
<div v-if="hasChoices" class="space-y-2 mb-3">
|
||||
<!-- Multiple choice buttons (always shown — all challenges now have choices) -->
|
||||
<div v-if="currentChallenge?.choices?.length" class="space-y-2 mb-3">
|
||||
<button
|
||||
v-for="(choice, i) in currentChallenge.choices"
|
||||
:key="i"
|
||||
@@ -311,52 +311,26 @@ function goToArena() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Free text input (for creative or when no choices) -->
|
||||
<!-- Free text input — hidden for now, all challenges use multiple choice.
|
||||
Kept for future reintroduction of creative open-ended input. -->
|
||||
<!--
|
||||
<template v-if="!hasChoices">
|
||||
<textarea
|
||||
ref="answerInput"
|
||||
v-model="answer"
|
||||
rows="4"
|
||||
placeholder="Type your answer..."
|
||||
<textarea ref="answerInput" v-model="answer" rows="4" placeholder="Type your answer..."
|
||||
class="w-full bg-surface border-2 border-border px-4 py-3 text-sm font-mono
|
||||
text-text-primary placeholder-text-muted resize-none rounded
|
||||
focus:outline-none focus:border-neon-pink/50 transition-colors mb-2"
|
||||
@keydown.ctrl.enter="submitAnswer"
|
||||
@keydown.meta.enter="submitAnswer"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="!showTrashTalk"
|
||||
class="font-mono text-[10px] text-text-muted hover:text-neon-yellow transition-colors mb-2"
|
||||
@click="showTrashTalk = true"
|
||||
>
|
||||
+ add trash talk
|
||||
</button>
|
||||
<input
|
||||
v-if="showTrashTalk"
|
||||
v-model="trashTalk"
|
||||
type="text"
|
||||
maxlength="200"
|
||||
placeholder="Talk smack to your opponent..."
|
||||
@keydown.ctrl.enter="submitAnswer" @keydown.meta.enter="submitAnswer" />
|
||||
<button v-if="!showTrashTalk" class="font-mono text-[10px] text-text-muted hover:text-neon-yellow transition-colors mb-2"
|
||||
@click="showTrashTalk = true">+ add trash talk</button>
|
||||
<input v-if="showTrashTalk" v-model="trashTalk" type="text" maxlength="200" placeholder="Talk smack..."
|
||||
class="w-full bg-surface border border-border px-3 py-2 text-xs font-mono
|
||||
text-neon-yellow placeholder-text-muted rounded
|
||||
focus:outline-none focus:border-neon-yellow/50 transition-colors mb-2"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full min-h-[48px] py-4 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
||||
font-display font-black text-lg tracking-[0.15em] rounded
|
||||
hover:bg-neon-pink/20 transition-all neon-border-pink
|
||||
disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
:disabled="!answer.trim()"
|
||||
@click="submitAnswer"
|
||||
>
|
||||
SUBMIT ANSWER
|
||||
</button>
|
||||
<p class="font-mono text-[10px] text-text-muted text-center mt-1">
|
||||
Ctrl+Enter to submit
|
||||
</p>
|
||||
text-neon-yellow placeholder-text-muted rounded focus:outline-none focus:border-neon-yellow/50 transition-colors mb-2" />
|
||||
<button class="w-full min-h-[48px] py-4 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
||||
font-display font-black text-lg tracking-[0.15em] rounded hover:bg-neon-pink/20 transition-all neon-border-pink
|
||||
disabled:opacity-30 disabled:cursor-not-allowed" :disabled="!answer.trim()" @click="submitAnswer">SUBMIT ANSWER</button>
|
||||
<p class="font-mono text-[10px] text-text-muted text-center mt-1">Ctrl+Enter to submit</p>
|
||||
</template>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<!-- PHASE: SUBMITTED / BETWEEN ROUNDS -->
|
||||
|
||||
@@ -119,6 +119,149 @@ export function pickRankedChallenge(usedTypes: Set<string>): 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<string, { good: string[]; bad: string[] }> = {
|
||||
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,
|
||||
|
||||
@@ -157,7 +157,7 @@ const CREATIVE_POOLS: Record<string, { good: string[]; medium: string[]; bad: st
|
||||
],
|
||||
bad: [
|
||||
"print('hello world')",
|
||||
"pass # TODO: implement",
|
||||
"x=input();print(len(x))",
|
||||
"x = 42 # the answer",
|
||||
"return True # ship it",
|
||||
"// no idea what to write",
|
||||
@@ -200,8 +200,8 @@ function generateChoices(challenge: Challenge): string[] {
|
||||
// Text answer — pick distractors from same challenge type
|
||||
const pool = getAnswerPool(challenge.type)
|
||||
const correctLower = challenge.answers.map(a => 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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user