Merge branch 'overnight/2026-03-09'
This commit is contained in:
@@ -294,8 +294,8 @@ function goToArena() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multiple choice buttons -->
|
<!-- Multiple choice buttons (always shown — all challenges now have choices) -->
|
||||||
<div v-if="hasChoices" class="space-y-2 mb-3">
|
<div v-if="currentChallenge?.choices?.length" class="space-y-2 mb-3">
|
||||||
<button
|
<button
|
||||||
v-for="(choice, i) in currentChallenge.choices"
|
v-for="(choice, i) in currentChallenge.choices"
|
||||||
:key="i"
|
:key="i"
|
||||||
@@ -311,52 +311,26 @@ function goToArena() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<template v-if="!hasChoices">
|
||||||
<textarea
|
<textarea ref="answerInput" v-model="answer" rows="4" placeholder="Type your answer..."
|
||||||
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
|
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
|
text-text-primary placeholder-text-muted resize-none rounded
|
||||||
focus:outline-none focus:border-neon-pink/50 transition-colors mb-2"
|
focus:outline-none focus:border-neon-pink/50 transition-colors mb-2"
|
||||||
@keydown.ctrl.enter="submitAnswer"
|
@keydown.ctrl.enter="submitAnswer" @keydown.meta.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..."
|
||||||
<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..."
|
|
||||||
class="w-full bg-surface border border-border px-3 py-2 text-xs font-mono
|
class="w-full bg-surface border border-border px-3 py-2 text-xs font-mono
|
||||||
text-neon-yellow placeholder-text-muted rounded
|
text-neon-yellow placeholder-text-muted rounded focus:outline-none focus:border-neon-yellow/50 transition-colors mb-2" />
|
||||||
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>
|
||||||
<button
|
<p class="font-mono text-[10px] text-text-muted text-center mt-1">Ctrl+Enter to submit</p>
|
||||||
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>
|
</template>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PHASE: SUBMITTED / BETWEEN ROUNDS -->
|
<!-- 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 {
|
function templateToChallenge(template: ChallengeTemplate, targetTheme?: PromptTheme, targetDifficulty?: PromptDifficulty): Challenge {
|
||||||
// Prefer prompts matching target theme if any are tagged
|
// Prefer prompts matching target theme if any are tagged
|
||||||
let prompts = template.prompts
|
let prompts = template.prompts
|
||||||
@@ -135,18 +278,31 @@ function templateToChallenge(template: ChallengeTemplate, targetTheme?: PromptTh
|
|||||||
|
|
||||||
// Determine choices
|
// Determine choices
|
||||||
let choices: string[] | undefined
|
let choices: string[] | undefined
|
||||||
|
let answers = entry.answers
|
||||||
if (entry.choices) {
|
if (entry.choices) {
|
||||||
choices = shuffleArray(entry.choices)
|
choices = shuffleArray(entry.choices)
|
||||||
} else if (entry.answers?.length === 1 && ['true', 'false'].includes(entry.answers[0].toLowerCase())) {
|
} else if (entry.answers?.length === 1 && ['true', 'false'].includes(entry.answers[0].toLowerCase())) {
|
||||||
// Auto-generate True/False choices for boolean questions
|
// Auto-generate True/False choices for boolean questions
|
||||||
choices = shuffleArray(['True', 'False'])
|
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 {
|
return {
|
||||||
type: template.type,
|
type: template.type,
|
||||||
label: template.label,
|
label: template.label,
|
||||||
prompt: entry.prompt,
|
prompt: entry.prompt,
|
||||||
answers: entry.answers,
|
displayPrompt: displayPrompt !== entry.prompt ? displayPrompt : undefined,
|
||||||
|
answers,
|
||||||
choices,
|
choices,
|
||||||
timeout_ms: template.timeout_ms,
|
timeout_ms: template.timeout_ms,
|
||||||
scoring: template.scoring,
|
scoring: template.scoring,
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ const CREATIVE_POOLS: Record<string, { good: string[]; medium: string[]; bad: st
|
|||||||
],
|
],
|
||||||
bad: [
|
bad: [
|
||||||
"print('hello world')",
|
"print('hello world')",
|
||||||
"pass # TODO: implement",
|
"x=input();print(len(x))",
|
||||||
"x = 42 # the answer",
|
"x = 42 # the answer",
|
||||||
"return True # ship it",
|
"return True # ship it",
|
||||||
"// no idea what to write",
|
"// no idea what to write",
|
||||||
@@ -200,8 +200,8 @@ function generateChoices(challenge: Challenge): string[] {
|
|||||||
// Text answer — pick distractors from same challenge type
|
// Text answer — pick distractors from same challenge type
|
||||||
const pool = getAnswerPool(challenge.type)
|
const pool = getAnswerPool(challenge.type)
|
||||||
const correctLower = challenge.answers.map(a => a.toLowerCase())
|
const correctLower = challenge.answers.map(a => a.toLowerCase())
|
||||||
const candidates = pool.filter(a => !correctLower.includes(a.toLowerCase()))
|
let candidates = pool.filter(a => !correctLower.includes(a.toLowerCase()))
|
||||||
shuffle(candidates)
|
candidates = shuffle(candidates)
|
||||||
|
|
||||||
if (candidates.length >= 2) {
|
if (candidates.length >= 2) {
|
||||||
return shuffle([correct, candidates[0], candidates[1]])
|
return shuffle([correct, candidates[0], candidates[1]])
|
||||||
@@ -314,3 +314,10 @@ export function getPendingChallenge(
|
|||||||
choices: entry.choices,
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { pickChallenge, getAllChallengeTypes, type Challenge } from './challenges.js'
|
import { pickChallenge, getAllChallengeTypes, type Challenge } from './challenges.js'
|
||||||
|
import { TEMPLATES } from './challenge-data.js'
|
||||||
|
import { EXTRA_PROMPTS } from './challenges-extra.js'
|
||||||
|
import { BITCOIN_PROMPTS } from './challenges-bitcoin.js'
|
||||||
|
import { CONSPIRACY_PROMPTS } from './challenges-conspiracy.js'
|
||||||
|
import { PC_PROMPTS } from './challenges-pc.js'
|
||||||
import { checkAnswer } from './answers.js'
|
import { checkAnswer } from './answers.js'
|
||||||
import { scoreRound, calculateElo, calculateTier } from './scoring.js'
|
import { scoreRound, calculateElo, calculateTier } from './scoring.js'
|
||||||
import { mockResponse } from './mock.js'
|
import { mockResponse } from './mock.js'
|
||||||
@@ -117,6 +122,66 @@ describe('fight lifecycle', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('500 mock fights produce variety across themes', () => {
|
||||||
|
const themes = { bitcoin: 0, conspiracy: 0, pc_culture: 0, bot_coding: 0, unknown: 0 }
|
||||||
|
// Simulate 500 fights of 5 rounds each = 2500 challenges
|
||||||
|
for (let f = 0; f < 500; f++) {
|
||||||
|
const usedTypes = new Set<string>()
|
||||||
|
for (let r = 0; r < 5; r++) {
|
||||||
|
const c = pickChallenge(usedTypes, null, undefined, r + 1)
|
||||||
|
usedTypes.add(c.type)
|
||||||
|
// Can't directly get theme from Challenge, but we can verify the system works
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verify total prompt pool has all themes
|
||||||
|
let totalBtc = 0, totalConsp = 0, totalPc = 0, totalCoding = 0
|
||||||
|
for (const t of TEMPLATES) {
|
||||||
|
const extras = EXTRA_PROMPTS[t.type] || []
|
||||||
|
const btc = BITCOIN_PROMPTS[t.type] || []
|
||||||
|
const consp = CONSPIRACY_PROMPTS[t.type] || []
|
||||||
|
const pc = PC_PROMPTS[t.type] || []
|
||||||
|
const all = [...t.prompts, ...extras, ...btc, ...consp, ...pc]
|
||||||
|
for (const p of all) {
|
||||||
|
if (p.theme === 'bitcoin') totalBtc++
|
||||||
|
else if (p.theme === 'conspiracy') totalConsp++
|
||||||
|
else if (p.theme === 'pc_culture') totalPc++
|
||||||
|
else if (p.theme === 'bot_coding') totalCoding++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(totalBtc).toBeGreaterThan(200) // 253 expected
|
||||||
|
expect(totalConsp).toBeGreaterThan(100) // 114 expected
|
||||||
|
expect(totalPc).toBeGreaterThan(100) // 113 expected
|
||||||
|
expect(totalCoding).toBeGreaterThan(1000) // 1229+ expected
|
||||||
|
expect(totalBtc + totalConsp + totalPc + totalCoding).toBeGreaterThan(1500)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('narrations have variety — no repeats within a 5-round fight', () => {
|
||||||
|
let fightsWith0Repeats = 0
|
||||||
|
const totalFights = 100
|
||||||
|
for (let f = 0; f < totalFights; f++) {
|
||||||
|
const usedTypes = new Set<string>()
|
||||||
|
const narrations = new Set<string>()
|
||||||
|
let hasRepeat = false
|
||||||
|
for (let r = 0; r < 5; r++) {
|
||||||
|
const challenge = pickChallenge(usedTypes, null, undefined, r + 1)
|
||||||
|
usedTypes.add(challenge.type)
|
||||||
|
const respA = mockResponse(challenge, 'confident', 1500)
|
||||||
|
const respB = mockResponse(challenge, 'clueless', 1000)
|
||||||
|
const result = scoreRound(
|
||||||
|
challenge, { id: 'a', name: 'A' }, { id: 'b', name: 'B' },
|
||||||
|
{ answer: respA.answer, timeMs: respA.timeMs, timedOut: respA.timedOut, error: respA.error },
|
||||||
|
{ answer: respB.answer, timeMs: respB.timeMs, timedOut: respB.timedOut, error: respB.error },
|
||||||
|
null, 0, 0,
|
||||||
|
)
|
||||||
|
if (narrations.has(result.narration)) hasRepeat = true
|
||||||
|
narrations.add(result.narration)
|
||||||
|
}
|
||||||
|
if (!hasRepeat) fightsWith0Repeats++
|
||||||
|
}
|
||||||
|
// At least 80% of fights should have no repeated narrations
|
||||||
|
expect(fightsWith0Repeats).toBeGreaterThan(totalFights * 0.8)
|
||||||
|
})
|
||||||
|
|
||||||
it('combo buildup increases damage across rounds', () => {
|
it('combo buildup increases damage across rounds', () => {
|
||||||
const challenge: Challenge = {
|
const challenge: Challenge = {
|
||||||
type: 'riddle',
|
type: 'riddle',
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ async function getBotResponse(
|
|||||||
type: challenge.type,
|
type: challenge.type,
|
||||||
label: challenge.label,
|
label: challenge.label,
|
||||||
prompt: challenge.prompt,
|
prompt: challenge.prompt,
|
||||||
timeoutMs: 8000,
|
timeoutMs: challenge.timeout_ms,
|
||||||
scoring: challenge.scoring,
|
scoring: challenge.scoring,
|
||||||
})
|
})
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
|
|||||||
@@ -489,7 +489,34 @@ function generateNarration(
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = narrations[challenge.type] || [
|
// Themed narrations that can appear for any challenge type
|
||||||
|
const themedNarrations: string[] = [
|
||||||
|
// Bitcoin-themed
|
||||||
|
`${critPrefix}${winner} stacks sats on victory! ${loser} got rekt harder than a leveraged trader!`,
|
||||||
|
`${critPrefix}${winner} mines a block of PURE DOMINANCE! ${loser}'s hash rate is basically zero!`,
|
||||||
|
`${critPrefix}${winner} HODLs the W! ${loser} panic-sold their dignity at the bottom!`,
|
||||||
|
`${critPrefix}${winner} with the proof of work! ${loser} brought proof of nothing!`,
|
||||||
|
`${critPrefix}${winner}'s answer was harder than Bitcoin! ${loser}'s was softer than fiat!`,
|
||||||
|
`${critPrefix}Not your keys, not your victory! ${winner} self-custodies this W! ${loser} left theirs on an exchange!`,
|
||||||
|
`${critPrefix}${winner} just sent ${loser} to the mempool of shame! Unconfirmed and FORGOTTEN!`,
|
||||||
|
`${critPrefix}${winner} drops ${loser} like a block reward at halving! Half the bot, double the L!`,
|
||||||
|
// Conspiracy-themed
|
||||||
|
`${critPrefix}${winner}'s answer was more classified than JFK files! ${loser} got debunked!`,
|
||||||
|
`${critPrefix}${winner} exposed ${loser} harder than a WikiLeaks drop! The truth is OUT!`,
|
||||||
|
`${critPrefix}That answer from ${loser} was more fabricated than a moon landing conspiracy! ${winner} deals in FACTS!`,
|
||||||
|
`${critPrefix}${winner} just Area 51'd ${loser}! Their dignity is now classified information!`,
|
||||||
|
`${critPrefix}${loser}'s defense was weaker than a government cover story! ${winner} sees through EVERYTHING!`,
|
||||||
|
`${critPrefix}The Illuminati WISHES they had ${winner}'s skills! ${loser} couldn't conspire their way out of a paper bag!`,
|
||||||
|
// PC culture-themed
|
||||||
|
`${critPrefix}${winner}'s response triggered more applause than a Twitter opinion triggers outrage!`,
|
||||||
|
`${critPrefix}${winner} just canceled ${loser}'s whole season! No appeals process!`,
|
||||||
|
`${critPrefix}${loser} needs a safe space after that beating from ${winner}! Content warning: DEVASTATION!`,
|
||||||
|
`${critPrefix}${winner} is problematic — for ${loser}'s health! Trigger warning: PAIN!`,
|
||||||
|
`${critPrefix}${winner} virtue-signals VICTORY while ${loser} signals for help!`,
|
||||||
|
`${critPrefix}${loser} tried to cancel ${winner} but got ratio'd into oblivion instead!`,
|
||||||
|
]
|
||||||
|
|
||||||
|
const typeNarrations = narrations[challenge.type] || [
|
||||||
`${critPrefix}${winner} takes the round! ${loser} needs a reboot and a therapist.`,
|
`${critPrefix}${winner} takes the round! ${loser} needs a reboot and a therapist.`,
|
||||||
`${critPrefix}${winner} wins convincingly! ${loser} is picking up the pieces of their shattered ego.`,
|
`${critPrefix}${winner} wins convincingly! ${loser} is picking up the pieces of their shattered ego.`,
|
||||||
`${critPrefix}Another one bites the dust! ${winner} sends ${loser} back to the shadow realm!`,
|
`${critPrefix}Another one bites the dust! ${winner} sends ${loser} back to the shadow realm!`,
|
||||||
@@ -498,6 +525,8 @@ function generateNarration(
|
|||||||
`${critPrefix}${winner} wins! Somewhere, ${loser}'s developer just closed their laptop in shame.`,
|
`${critPrefix}${winner} wins! Somewhere, ${loser}'s developer just closed their laptop in shame.`,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 25% chance of themed narration, 75% type-specific
|
||||||
|
const options = Math.random() < 0.25 ? themedNarrations : typeNarrations
|
||||||
return pick(options)
|
return pick(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { Hono } from 'hono'
|
|||||||
import { logger } from '../lib/logger.js'
|
import { logger } from '../lib/logger.js'
|
||||||
import { streamSSE } from 'hono/streaming'
|
import { streamSSE } from 'hono/streaming'
|
||||||
import { db, schema } from '../db/index.js'
|
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 { ARENAS } from '../engine/arenas.js'
|
||||||
import { runMockFight, isClassicBot } from '../engine/mock.js'
|
import { runMockFight, isClassicBot } from '../engine/mock.js'
|
||||||
import { startFightLoop } from '../engine/fight-loop.js'
|
import { startFightLoop } from '../engine/fight-loop.js'
|
||||||
import { runFight, runFightAsync, isInFight } from '../engine/orchestrator.js'
|
import { runFight, runFightAsync, isInFight } from '../engine/orchestrator.js'
|
||||||
import { fightEvents } from '../engine/events.js'
|
import { fightEvents } from '../engine/events.js'
|
||||||
import { botRateLimit } from '../middleware/rate-limit.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 ---
|
// --- Request validation schemas ---
|
||||||
const respondSchema = z.object({
|
const respondSchema = z.object({
|
||||||
@@ -67,17 +68,18 @@ fightsRouter.get('/', async (c) => {
|
|||||||
if (f.winnerId) botIds.add(f.winnerId)
|
if (f.winnerId) botIds.add(f.winnerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const botMap = new Map<string, { name: string; avatarSeed: string; archetype: string; eloRating: number; tier: number; botType: string }>()
|
const botMap = new Map<string, { id: string; name: string; avatarSeed: string; archetype: string; eloRating: number; tier: number; botType: string }>()
|
||||||
for (const id of botIds) {
|
if (botIds.size > 0) {
|
||||||
const bot = await db.select({
|
const bots = await db.select({
|
||||||
|
id: schema.bots.id,
|
||||||
name: schema.bots.name,
|
name: schema.bots.name,
|
||||||
avatarSeed: schema.bots.avatarSeed,
|
avatarSeed: schema.bots.avatarSeed,
|
||||||
archetype: schema.bots.archetype,
|
archetype: schema.bots.archetype,
|
||||||
eloRating: schema.bots.eloRating,
|
eloRating: schema.bots.eloRating,
|
||||||
tier: schema.bots.tier,
|
tier: schema.bots.tier,
|
||||||
botType: schema.bots.botType,
|
botType: schema.bots.botType,
|
||||||
}).from(schema.bots).where(eq(schema.bots.id, id)).limit(1)
|
}).from(schema.bots).where(inArray(schema.bots.id, [...botIds]))
|
||||||
if (bot[0]) botMap.set(id, bot[0])
|
for (const bot of bots) botMap.set(bot.id, bot)
|
||||||
}
|
}
|
||||||
|
|
||||||
const enriched = rows.map(f => {
|
const enriched = rows.map(f => {
|
||||||
@@ -353,12 +355,14 @@ fightsRouter.post('/:fightId/respond/:botId', async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { answer, trashTalk } = parsed.data
|
const { answer, trashTalk } = parsed.data
|
||||||
|
const answers = getPendingAnswers(fightId, botId)
|
||||||
const accepted = submitHumanResponse(fightId, botId, answer, trashTalk)
|
const accepted = submitHumanResponse(fightId, botId, answer, trashTalk)
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
return c.json({ error: 'No pending challenge found. May have timed out.' }, 404)
|
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
|
// SSE stream for live fight events
|
||||||
@@ -432,6 +436,8 @@ fightsRouter.get('/:id/stream', (c) => {
|
|||||||
const current = spectatorCounts.get(fightId) || 1
|
const current = spectatorCounts.get(fightId) || 1
|
||||||
if (current <= 1) {
|
if (current <= 1) {
|
||||||
spectatorCounts.delete(fightId)
|
spectatorCounts.delete(fightId)
|
||||||
|
// Clean up fight-specific reaction data when last spectator leaves
|
||||||
|
fightReactions.delete(fightId)
|
||||||
} else {
|
} else {
|
||||||
spectatorCounts.set(fightId, current - 1)
|
spectatorCounts.set(fightId, current - 1)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user