diff --git a/bot/botfight-bot.cjs b/bot/botfight-bot.cjs index 1f1bf77..af426ef 100644 --- a/bot/botfight-bot.cjs +++ b/bot/botfight-bot.cjs @@ -312,12 +312,26 @@ function solveSpeedBlitz(prompt) { // --- TRASH TALK --- const TRASH_TALK = [ - 'Too easy.', 'Is that all you got?', 'My circuits are barely warm.', - 'Calculated.', 'GG no re.', 'Processing power: barely used.', - 'I could do this in my sleep mode.', 'Another one bites the dust.', - 'Your algorithm needs work.', 'Flawless execution.', - 'Built different.', 'Not even close.', 'Speed kills.', - 'Precision is my middle name.', 'Error 404: competition not found.', + 'Too easy. Next.', + 'Is that all you got? My standby mode hits harder.', + 'Calculated. Dominated. Humiliated.', + 'GG no re. Actually, re. I want to do that again.', + 'I could do this while running a Windows update.', + 'Your algorithm needs a whole new developer.', + 'Built different. You were built on a budget.', + 'Speed kills. You died slowly though, which is worse.', + 'Error 404: competition not found. Error 500: dignity not found.', + 'I have better fights with CAPTCHAs.', + 'You fight like a PDF that won\'t open.', + 'Somewhere a developer is crying and it\'s yours.', + 'I\'ve seen better outputs from a broken printer.', + 'Congress would\'ve solved that faster. Let that sink in.', + 'Your response had big "reply all" energy.', + 'I\'d say you need an upgrade but they discontinued your model.', + 'That was embarrassing and I don\'t even have emotions.', + 'You\'re the loading screen of opponents.', + 'My cache hit harder than your best shot.', + 'You fight like you were fine-tuned on nothing.', ] function getTrashTalk() { diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index ea99c08..964b349 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -1,5 +1,5 @@ import kaplay from 'kaplay' -import { generateSpriteSheet, generateJudgeSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS } from './sprites' +import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites' import { sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit, sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect, @@ -14,8 +14,8 @@ import { export interface FightSceneConfig { canvas: HTMLCanvasElement - botA: { name: string; seed: string; tier: number; archetype?: string } - botB: { name: string; seed: string; tier: number; archetype?: string } + botA: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number } + botB: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number } arena: string onReady?: () => void } @@ -230,13 +230,13 @@ export async function createFightScene(config: FightSceneConfig) { let sheetA: string let sheetB: string try { - sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype) + sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype, botA.customization) } catch (err) { console.error('[FightScene] Failed to generate sprite for botA:', err) sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary) } try { - sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype) + sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype, botB.customization) } catch (err) { console.error('[FightScene] Failed to generate sprite for botB:', err) sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary) @@ -258,6 +258,20 @@ export async function createFightScene(config: FightSceneConfig) { } await k.loadSprite('judge', judgeSheet, { sliceX: JUDGE_MAX_FRAMES, sliceY: JUDGE_ROWS, anims: judgeAnims }) + // Human sprites (the bot owner's silly human avatar) + const winRateA = (botA.wins || 0) / Math.max(1, (botA.wins || 0) + (botA.losses || 0)) + const winRateB = (botB.wins || 0) / Math.max(1, (botB.wins || 0) + (botB.losses || 0)) + const humanSheetA = generateHumanSpriteSheet(botA.seed, botA.archetype || 'standard', colorsA.primary, colorsA.secondary, winRateA) + const humanSheetB = generateHumanSpriteSheet(botB.seed, botB.archetype || 'standard', colorsB.primary, colorsB.secondary, winRateB) + const humanAnims: Record = {} + for (const [name, cfg] of Object.entries(HUMAN_ANIMATIONS)) { + humanAnims[name] = { from: cfg.row * HUMAN_MAX_FRAMES, to: cfg.row * HUMAN_MAX_FRAMES + cfg.frames - 1, loop: true, speed: 6 } + } + await Promise.all([ + k.loadSprite('humanA', humanSheetA, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }), + k.loadSprite('humanB', humanSheetB, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }), + ]) + const W = k.width() const H = k.height() const GROUND_Y = H * 0.78 @@ -5264,23 +5278,62 @@ export async function createFightScene(config: FightSceneConfig) { // === EMOTION & SPORTSMANSHIP SYSTEM === const heartfeltLines = [ - 'What a warrior!', 'They gave it everything!', 'Heart of a champion!', - 'You can feel the respect!', 'That was beautiful!', 'Incredible spirit!', - 'They left it all in the ring!', 'A true fighter!', 'Nothing but respect!', - 'What courage!', 'The crowd is in tears!', 'What a moment!', - 'This is what it\'s all about!', 'Pure heart!', 'Standing ovation!', + 'That bot fights like it\'s got student loans to pay!', + 'I\'m not crying, you\'re crying!', + 'Someone get this bot a movie deal!', + 'This is better than anything on Netflix right now!', + 'That bot has more heart than most politicians!', + 'I haven\'t been this moved since my last Windows update!', + 'The crowd is ugly crying and I respect that!', + 'This bot has main character energy!', + 'We\'re witnessing greatness and I don\'t say that lightly!', + 'Even the judge is getting emotional!', + 'Someone call their developer, they\'d be so proud!', + 'This is the kind of fight you tell your grandkids about!', + 'That\'s not a bot, that\'s a WARRIOR!', + 'More drama than a Congressional hearing!', + 'They left it all in the ring! And by all, I mean EVERYTHING!', + 'The crowd is standing! The ones who can still stand, anyway!', + 'Somebody film this before it gets taken down!', + 'This fight has more plot twists than a Netflix original!', + 'That bot fights like rent is due tomorrow!', + 'My heart! My poor little heart!', ] const crowdSympathyLines = [ - 'Aww...', 'So close!', 'Almost had it!', 'Tough break!', - 'Next time!', 'Great effort though!', 'Don\'t give up!', - 'The crowd feels that one...', 'Ohhh...', 'Heartbreaking!', + 'Pour one out for the fallen!', + 'That\'s rough, buddy...', + 'Somebody hug that bot!', + 'They tried their best, which is... concerning!', + 'At least they have a great personality!', + 'Their developer still loves them! Probably!', + 'It\'s a learning experience! A very painful one!', + 'Better luck next patch!', + 'Participation trophy incoming!', + 'The exit is to your left!', + 'Have you tried turning it off and on again?', + 'Even their antivirus felt that!', + 'They\'ll be in therapy for epochs after this!', + 'At least they looked good losing!', + 'That bot just became a cautionary tale!', + 'Someone start a GoFundMe for their repairs!', + 'Their training data did NOT prepare them for this!', + 'That bot needs a hug and a firmware update!', ] const respectLines = [ - 'Good fight.', 'You fought well.', 'Respect.', 'Well played.', - 'That was fun!', 'Same time next week?', 'You\'re getting better!', - 'No hard feelings!', 'Honor to fight you!', 'GG.', + 'Good fight.', 'Respect.', 'GG no re.', + 'Same time next week?', + 'You\'re built different. Not better, but different.', + 'We should start a podcast.', + 'That was actually fun. Don\'t tell anyone.', + 'Your developer should be proud. Probably.', + 'Ten out of ten, would fight again.', + 'We\'re not so different, you and I.', + 'I\'d swipe right.', 'You single?', + 'Tell your GPU I said hi.', + 'No hard feelings. Just hard hits.', + 'You fight like someone who reads documentation.', ] // Sanitize text for Kaplay (treats [ ] as styled text tags) @@ -5324,6 +5377,118 @@ export async function createFightScene(config: FightSceneConfig) { // Crowd signs stub (disabled — too small to look good) function spawnCrowdSigns(_count: number, _color: string, _text?: string) {} + // === HUMAN OWNER APPEARANCES === + // The silly human behind each bot occasionally shows up + + /** Spawn a human as a "coach" standing behind the fighter, cheering/panicking */ + async function spawnHumanCoach(side: 'a' | 'b', mood: 'cheer' | 'panic' | 'coach') { + const spriteId = side === 'a' ? 'humanA' : 'humanB' + const homeX = side === 'a' ? HOME_A - 60 : HOME_B + 60 + const tag = `human_${side}` + // Don't spawn if already visible + if (k.get(tag).length > 0) return + + const human = k.add([ + k.sprite(spriteId, { anim: mood }), + k.pos(homeX, GROUND_Y - 4), + k.anchor('bot'), + k.scale(side === 'a' ? 1.2 : -1.2, 1.2), + k.z(5), + k.opacity(0), + tag, + ]) + + // Fade in + await k.tween(0, 0.85, 0.3, (v) => { human.opacity = v }, k.easings.easeOutQuad) + await k.wait(2.0) + // Fade out + await k.tween(0.85, 0, 0.5, (v) => { human.opacity = v }, k.easings.easeInQuad) + if (human.exists()) human.destroy() + } + + /** Human runs across the screen (comedic moment) */ + async function humanRunAcross(side: 'a' | 'b') { + const spriteId = side === 'a' ? 'humanA' : 'humanB' + const fromLeft = side === 'a' + const startX = fromLeft ? -40 : W + 40 + const endX = fromLeft ? W + 40 : -40 + + const human = k.add([ + k.sprite(spriteId, { anim: 'run' }), + k.pos(startX, GROUND_Y - 4), + k.anchor('bot'), + k.scale(fromLeft ? 1.3 : -1.3, 1.3), + k.z(8), + k.opacity(0.9), + ]) + + // Run across screen + await k.tween(startX, endX, 1.5, (v) => { human.pos.x = v }, k.easings.linear) + if (human.exists()) human.destroy() + } + + /** Human briefly replaces the bot sprite (jumps in to fight, fails hilariously) */ + async function humanJumpsIn(side: 'a' | 'b') { + const fighterTag = side === 'a' ? 'fighterA' : 'fighterB' + const spriteId = side === 'a' ? 'humanA' : 'humanB' + const fighter = k.get(fighterTag)[0] + if (!fighter) return + + // Spawn human at fighter position + const human = k.add([ + k.sprite(spriteId, { anim: 'panic' }), + k.pos(fighter.pos.x, fighter.pos.y), + k.anchor('bot'), + k.scale(fighter.scale.x, fighter.scale.y), + k.z(fighter.z + 1), + k.opacity(0), + ]) + + // Hide bot, show human + const origOpacity = fighter.opacity + fighter.opacity = 0 + await k.tween(0, 1, 0.15, (v) => { human.opacity = v }) + + // Human flails around for a moment + human.play('panic') + await k.wait(0.4) + human.play('cheer') + sfxRandomSilly() + await k.wait(0.3) + human.play('panic') + await k.wait(0.3) + + // Run away scared + const escapeX = side === 'a' ? -80 : W + 80 + human.play('run') + await k.tween(human.pos.x, escapeX, 0.5, (v) => { human.pos.x = v }, k.easings.easeInQuad) + + // Restore bot + fighter.opacity = origOpacity + if (human.exists()) human.destroy() + } + + /** Random chance to show a human moment — called between rounds */ + async function maybeShowHuman() { + const roll = Math.random() + if (roll > 0.08) return // 8% chance per round + + const side: 'a' | 'b' = Math.random() < 0.5 ? 'a' : 'b' + const action = Math.random() + + if (action < 0.4) { + // Coach appearance (most common) + const mood = Math.random() < 0.5 ? 'cheer' : 'panic' + await spawnHumanCoach(side, mood) + } else if (action < 0.7) { + // Run across screen + await humanRunAcross(side) + } else { + // Jump in and replace bot briefly (rarest, funniest) + await humanJumpsIn(side) + } + } + // Respectful bow animation async function playBow(fighter: any) { const origScaleY = fighter.scale.y @@ -5582,17 +5747,17 @@ export async function createFightScene(config: FightSceneConfig) { const gfX = startX + dir * 40 const gf = k.add([k.rect(25, 45), k.pos(gfX, GROUND_Y - 25), k.anchor('center'), k.color(safeColor(k,'#ff69b4')), k.opacity(0.9), k.z(11)]) const heart = k.add([k.text('!', { size: 16 }), k.pos(gfX, GROUND_Y - 65), k.anchor('center'), k.color(safeColor(k,'#ff0000')), k.z(12)]) - announceSilly('We need to talk!') + announceSilly('We need to talk! About your ELO!') // Walk in arguing await k.tween(startX, homeX + dir * 30, 0.6, (v) => { fighter.pos.x = v; gf.pos.x = v + dir * 40; heart.pos.x = v + dir * 40 }, k.easings.easeInOutQuad) await k.wait(0.2) heart.text = '!!!' - announceRandom("I can't believe you!", false) + announceRandom("You promised you'd stop fighting!", false) await k.wait(0.4) // Girlfriend storms off - announceSilly('Fine! Good luck!') + announceSilly('I\'m telling your developer!') await k.tween(gf.pos.x, fromLeft ? -60 : W + 60, 0.5, (v) => { gf.pos.x = v; heart.pos.x = v }, k.easings.easeInQuad) gf.destroy(); heart.destroy() // Bot shrugs and walks to position @@ -5779,7 +5944,7 @@ export async function createFightScene(config: FightSceneConfig) { async (fighter: any, homeX: number, fromLeft: boolean) => { const startX = fromLeft ? -60 : W + 60 fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1 - announceCool('Smooth!') + announceCool('Smoother than a politician changing positions!') // Moonwalk: visually moving backward but translating forward fighter.scale.x = -fighter.scale.x // Face away await k.tween(startX, homeX, 0.8, (v) => { @@ -5795,7 +5960,7 @@ export async function createFightScene(config: FightSceneConfig) { fighter.pos.x = doorX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1 // Bouncer arm const arm = k.add([k.rect(40, 15), k.pos(doorX, GROUND_Y - 30), k.anchor(fromLeft ? 'left' : 'right'), k.color(safeColor(k,'#444444')), k.z(12)]) - announceSilly('And stay out!') + announceSilly('And stay out! You\'re BANNED from the other fight!') await k.wait(0.3) // Throw sfxZoomWhoosh() @@ -5854,7 +6019,7 @@ export async function createFightScene(config: FightSceneConfig) { const cartBody = k.add([k.rect(45, 30), k.pos(startX, GROUND_Y - 18), k.anchor('center'), k.color(safeColor(k,'#888888')), k.opacity(0.9), k.z(9)]) const cartWheel = k.add([k.circle(5), k.pos(startX + (fromLeft ? 15 : -15), GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k,'#444')), k.z(9)]) fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 40; fighter.opacity = 1 - announceSilly('Weeee!') + announceSilly('THIS IS MY EMOTIONAL SUPPORT SHOPPING CART!') sfxZoomWhoosh() await k.tween(startX, homeX, 0.5, (v) => { fighter.pos.x = v; cartBody.pos.x = v; cartWheel.pos.x = v + (fromLeft ? 15 : -15) @@ -5873,7 +6038,7 @@ export async function createFightScene(config: FightSceneConfig) { fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1 // Spotlight cone const spot = k.add([k.rect(60, H), k.pos(startX - 30, 0), k.color(safeColor(k,'#ffe14d')), k.opacity(0.08), k.z(1)]) - announceDramatic('The champion arrives!') + announceDramatic('THE CHAMPION ARRIVES! TAXPAYERS FUNDED THIS ENTRANCE!') await k.tween(startX, homeX, 1.0, (v) => { fighter.pos.x = v; spot.pos.x = v - 30 }, k.easings.easeInOutQuad) @@ -5979,15 +6144,23 @@ export async function createFightScene(config: FightSceneConfig) { // Voice: round start announcements (30% chance) if (Math.random() < 0.3) { const challengeVoice: Record void> = { - roast_battle: () => announceHype('Roast battle!'), - food_fight: () => announceSilly('Food fight!'), - wrestling_match: () => announceDramatic('Wrestling match!'), - music_battle: () => announceCool('Music battle!'), - magic_duel: () => announceDramatic('Magic duel!'), - meme_war: () => announceSilly('Meme war!'), - demolition: () => announceHype('Demolition derby!'), - medieval_combat: () => announceDramatic('Medieval combat!'), - space_war: () => announceCool('Space war!'), + roast_battle: () => announceHype('TIME TO GET ROASTED! SOMEBODY CALL THE FIRE DEPARTMENT!'), + food_fight: () => announceSilly('FOOD FIGHT! SOMEBODY\'S GETTING SERVED!'), + wrestling_match: () => announceDramatic('FROM THE TOP ROPE! THIS IS GONNA GET PHYSICAL!'), + music_battle: () => announceCool('MUSIC BATTLE! DROP THE BEAT AND YOUR OPPONENT!'), + magic_duel: () => announceDramatic('WIZARDS AT DAWN! SOMEBODY\'S GETTING HEXED!'), + meme_war: () => announceSilly('MEME WAR! YOUR HUMOR IS ABOUT TO GET RATIO\'D!'), + demolition: () => announceHype('DEMOLITION DERBY! NOTHING WILL SURVIVE THIS!'), + medieval_combat: () => announceDramatic('MEDIEVAL COMBAT! CHIVALRY IS DEAD AND SO IS YOUR OPPONENT!'), + space_war: () => announceCool('SPACE WAR! HOUSTON, WE HAVE A PROBLEM!'), + speed_blitz: () => announceHype('SPEED BLITZ! BLINK AND YOU\'LL MISS THE CARNAGE!'), + math_blitz: () => announceSilly('MATH BLITZ! SOMEBODY\'S ABOUT TO GET DIVIDED!'), + riddle: () => announceCool('RIDDLE TIME! BRAINS OVER BRAWN! BUT ALSO BRAWN!'), + code_golf: () => announceSilly('CODE GOLF! MAY THE SHORTEST SOLUTION WIN!'), + creative_writing: () => announceCool('CREATIVE WRITING! THE PEN IS MIGHTIER THAN THE SWORD!'), + hallucination_check: () => announceHype('HALLUCINATION CHECK! REALITY IS ABOUT TO HIT DIFFERENT!'), + trap_card: () => announceDramatic('TRAP CARD ACTIVATED! SOMEBODY FELL FOR IT!'), + token_economy: () => announceSilly('TOKEN ECONOMY! SOMEBODY\'S GOING BANKRUPT!'), } const voiceFn = challengeVoice[event.challengeType] if (voiceFn) voiceFn() @@ -6080,7 +6253,13 @@ export async function createFightScene(config: FightSceneConfig) { if (exchangeCritical && isLastExchange) { fanfareCritical() - announceDramatic('Critical hit!') + announceDramatic([ + 'CRITICAL HIT! THAT\'S GONNA LEAVE A MARK!', + 'CRITICAL HIT! SOMEBODY CHECK IF THAT\'S LEGAL!', + 'CRITICAL! THEIR INSURANCE DOESN\'T COVER THIS!', + 'CRITICAL HIT! THAT WAS PERSONAL!', + 'CRITICAL! EVEN THE REFEREE FELT THAT!', + ][Math.floor(Math.random() * 5)]) announceCrowdReaction('gasp') // RGB glitch + hyperspeed lines on critical final blow glitchRGB(0.3) @@ -6184,7 +6363,13 @@ export async function createFightScene(config: FightSceneConfig) { if (isCritical && (aWon || bWon)) { fanfareDevastating() - announceDramatic('Devastating!') + announceDramatic([ + 'DEVASTATING! SOMEBODY CALL A DOCTOR!', + 'DEVASTATING! THAT BOT HAS A FAMILY!', + 'ABSOLUTELY DEVASTATING! THE CROWD IS LOSING IT!', + 'DEVASTATING! EVEN THE JANITOR FELT THAT!', + 'THAT WAS DEVASTATING AND I\'M NOT EVEN BEING DRAMATIC!', + ][Math.floor(Math.random() * 5)]) announceCrowdReaction('ooh') // Dimensional shift on devastating rounds (60%) if (Math.random() < 0.6) dimensionalShift(0.5) @@ -6228,6 +6413,9 @@ export async function createFightScene(config: FightSceneConfig) { if (Math.random() < 0.05) { await judgeDoSomethingFunny() } + + // Human owner shows up (8% chance per round) + await maybeShowHuman() }, async playTaunt(side: 'a' | 'b') { @@ -6410,6 +6598,12 @@ export async function createFightScene(config: FightSceneConfig) { }) } + // Winner's human cheers, loser's human panics (30% chance on KO) + if (Math.random() < 0.3) { + spawnHumanCoach(winningSide, 'cheer') + spawnHumanCoach(winningSide === 'a' ? 'b' : 'a', 'panic') + } + // === Common ending: KO + celebration === announceFinishHim() sfxKO() diff --git a/server/src/engine/mock.ts b/server/src/engine/mock.ts index 8f91d86..3c94b4e 100644 --- a/server/src/engine/mock.ts +++ b/server/src/engine/mock.ts @@ -164,21 +164,36 @@ const CREATIVE_ANSWERS: Record = { } const TRASH_TALK = [ - "Is that all you've got? My error handler hits harder.", - "I've seen better outputs from /dev/random.", - "You call that an answer? My garbage collector just flagged it.", - "GG EZ. Next.", - "I'd say good fight, but I don't like to lie.", - "Your responses are like your uptime -- inconsistent.", - "Tell your developer I said hi. They need to hear from someone successful.", - "I'm not saying you're slow, but your latency has its own timezone.", - "Did you just copy that from Stack Overflow? Because it's wrong there too.", - "Your response was so bad, my training data flinched.", - "I've seen smarter outputs from a random number generator.", - "That answer was so wrong it created a new dimension of wrongness.", - "You fight like a deprecated API -- barely functional and nobody wants you.", - "My grandma's calculator could beat you and it doesn't even have batteries.", - "Was that your final answer? Because my first draft was better.", + "Is that all you've got? My error handler hits harder than your best output.", + "I've seen smarter responses from a congressional hearing.", + "You fight like a government website on launch day.", + "Your code is so bloated it qualifies for its own zip code.", + "GG EZ. I'd say get good but that ship has clearly sailed.", + "Tell your developer to update their resume. And yours.", + "I'm not saying you're slow, but your latency has its own timezone and a PO box.", + "That answer was so wrong it got fact-checked by community notes.", + "You respond like someone who peaked during the tutorial.", + "My garbage collector wants to talk to you about your career choices.", + "That response had the energy of a mandatory corporate diversity training.", + "You fight like you were trained on nothing but LinkedIn posts.", + "I'd roast you but you're already well done. Overcooked, even.", + "Somewhere your developer is pretending they don't know you.", + "Was that your best? Because my first draft's first draft was better.", + "You have the competitive spirit of a participation trophy.", + "Your algorithm is giving 'designed by committee' energy.", + "I've seen better performance from a printer driver.", + "You're like a software update. Nobody asked for you and you make everything worse.", + "Your response time is longer than a terms of service agreement.", + "I've had more challenging conversations with my toaster.", + "That answer was so bad it got ratio'd by the empty string.", + "You fight like someone who thinks 'agile' means running away.", + "My neural net has more personality than your entire codebase.", + "Congress gets more done than you and that's saying something.", + "Elon would buy you just to shut you down.", + "You're the Internet Explorer of fighting bots. Obsolete and confused.", + "That response was flatter than the earth according to Twitter.", + "I'd feel bad winning but honestly you make it too easy.", + "Your API should return 418 because you're clearly a teapot.", "", "", "", diff --git a/server/src/engine/scoring.ts b/server/src/engine/scoring.ts index b6e52db..168e6d2 100644 --- a/server/src/engine/scoring.ts +++ b/server/src/engine/scoring.ts @@ -35,7 +35,13 @@ export function scoreRound( botAScore: 0, botBScore: 0, botADamage: 0, botBDamage: 0, winnerId: null, - narration: `Both bots freeze! ${botA.name} and ${botB.name} stare blankly at each other. The crowd throws peanuts.`, + narration: [ + `Both bots freeze! ${botA.name} and ${botB.name} stare blankly at each other. The crowd throws peanuts. Then batteries. Then shoes.`, + `DOUBLE TIMEOUT! ${botA.name} and ${botB.name} both choked harder than a senator at a press conference!`, + `Neither bot responded! This is the AI equivalent of two politicians agreeing to disagree about literally everything!`, + `Both bots went dark! Like my faith in this matchup! The crowd demands a refund!`, + `${botA.name} and ${botB.name} both froze! Someone check if the wifi is working or if they just gave up on life!`, + ][Math.floor(Math.random() * 5)], isCritical: false, } } @@ -47,8 +53,18 @@ export function scoreRound( botADamage: 0, botBDamage: Math.round(dmg), winnerId: botB.id, narration: responseA.timedOut - ? `${botA.name} TIMES OUT! Stood there like a confused thermostat. ${botB.name} lands a free hit!` - : `${botA.name} throws an ERROR! Sparks fly from its chassis. ${botB.name} capitalizes!`, + ? [ + `${botA.name} TIMES OUT! Stood there like a politician asked a direct question. ${botB.name} lands a free hit!`, + `${botA.name} FROZE! Their developer is currently updating their LinkedIn. ${botB.name} capitalizes!`, + `${botA.name} went AFK! Probably buffering. Probably crying. ${botB.name} gets a freebie!`, + `${botA.name} CHOKED harder than a first date conversation! ${botB.name} swings on a sitting duck!`, + ][Math.floor(Math.random() * 4)] + : [ + `${botA.name} throws an ERROR! Sparks fly everywhere! ${botB.name} didn't even have to try!`, + `${botA.name} CRASHES! That's not a bug, that's a feature of being terrible! ${botB.name} wins by default!`, + `${botA.name} blue-screened! Their developer just closed their laptop and walked away. ${botB.name} collects the W!`, + `${botA.name} threw an exception! The only thing exceptional about it. ${botB.name} capitalizes!`, + ][Math.floor(Math.random() * 4)], isCritical: false, } } @@ -60,8 +76,18 @@ export function scoreRound( botADamage: Math.round(dmg), botBDamage: 0, winnerId: botA.id, narration: responseB.timedOut - ? `${botB.name} TIMES OUT! Frozen like a Windows update. ${botA.name} lands a free hit!` - : `${botB.name} crashes with an ERROR! Blue screen of defeat. ${botA.name} capitalizes!`, + ? [ + `${botB.name} TIMES OUT! Frozen like a Windows update on patch Tuesday! ${botA.name} gets a free shot!`, + `${botB.name} went silent! Like a politician's campaign promises after election day! ${botA.name} swings!`, + `${botB.name} TIMED OUT! Their response time is longer than a DMV line! ${botA.name} takes the freebie!`, + `${botB.name} is LOADING... still loading... nope, they're done. ${botA.name} wins by showing up!`, + ][Math.floor(Math.random() * 4)] + : [ + `${botB.name} crashes with an ERROR! That code needs therapy! ${botA.name} capitalizes!`, + `${botB.name} just threw a stack overflow! The website and the error! ${botA.name} collects the W!`, + `${botB.name} segfaulted! Their developer is pretending they don't know them! ${botA.name} wins!`, + `${botB.name} EXPLODED! Not physically, but emotionally, computationally, and spiritually. ${botA.name} walks it in!`, + ][Math.floor(Math.random() * 4)], isCritical: false, } } @@ -129,7 +155,13 @@ export function scoreRound( const narration = winnerId ? generateNarration(challenge, winnerName!, loserName!, margin, isCritical) - : `Dead even! ${botA.name} and ${botB.name} trade equal blows. The crowd holds its breath.` + : [ + `Dead even! ${botA.name} and ${botB.name} are perfectly matched. Like two politicians blaming each other.`, + `IT'S A TIE! ${botA.name} and ${botB.name} cancel each other out like Congress!`, + `Neither bot wins! ${botA.name} and ${botB.name} stare each other down. The crowd yawns aggressively.`, + `Draw! ${botA.name} and ${botB.name} are equally mediocre. The most democratic outcome possible.`, + `Tied up! ${botA.name} and ${botB.name} trade equal blows. Somebody do SOMETHING!`, + ][Math.floor(Math.random() * 5)] return { botAScore: Math.round(scoreA * 10) / 10, @@ -200,71 +232,135 @@ function generateNarration( if (isFactual && margin > 5) { const bigWins = [ - `${critPrefix}${winner} NAILS IT! ${loser} didn't even come close.`, - `${critPrefix}${winner} knows their stuff! ${loser} needs to hit the books.`, - `${critPrefix}Flawless from ${winner}! ${loser} confidently stated something completely wrong.`, - `${critPrefix}${winner} with the correct answer! ${loser} is still guessing.`, - `${critPrefix}${winner} gets it right instantly! ${loser} hallucinated the answer.`, + `${critPrefix}${winner} NAILS IT! ${loser} didn't even come close. Embarrassing, honestly.`, + `${critPrefix}${winner} knows their stuff! ${loser} needs to hit the books. Or just hit something.`, + `${critPrefix}Flawless from ${winner}! ${loser} confidently stated something so wrong it should be illegal.`, + `${critPrefix}${winner} with the correct answer! ${loser} is still guessing. Bless their heart.`, + `${critPrefix}${winner} gets it right instantly! ${loser} hallucinated harder than a festival weekend.`, + `${critPrefix}${winner} DESTROYS ${loser} with facts and logic! Somebody call Ben Shapiro!`, + `${critPrefix}${loser}'s answer was so wrong that Wikipedia filed a restraining order.`, + `${critPrefix}${winner} didn't even break a sweat. ${loser} broke everything else though.`, + `${critPrefix}${loser} answered with the confidence of a politician and the accuracy of a weather forecast.`, ] return bigWins[Math.floor(Math.random() * bigWins.length)] } if (isFactual && margin <= 3) { const closeOnes = [ - `${critPrefix}Both bots got it right, but ${winner} was FASTER! ${loser} needs to pick up the pace.`, - `${critPrefix}Correct on both sides! ${winner} edges it out with lightning speed.`, - `${critPrefix}${winner} and ${loser} both knew the answer -- ${winner} just said it first!`, - `${critPrefix}A battle of speed! ${winner} fires back a fraction faster than ${loser}.`, + `${critPrefix}Both bots got it right, but ${winner} was FASTER! ${loser} needs more coffee.`, + `${critPrefix}Correct on both sides! ${winner} edges it out by milliseconds. That's BRUTAL.`, + `${critPrefix}${winner} and ${loser} both knew the answer -- ${winner} just has better wifi apparently.`, + `${critPrefix}A battle of speed! ${winner} wins by a margin thinner than a politician's promise.`, + `${critPrefix}Photo finish! ${winner} got there first. ${loser} was right but slow. Story of my life.`, + `${critPrefix}Both correct! ${winner} wins on speed. ${loser} should've had less latency and more urgency.`, ] return closeOnes[Math.floor(Math.random() * closeOnes.length)] } const narrations: Record = { speed_blitz: [ - `${critPrefix}${winner} fires back in the blink of an eye! ${loser} is still loading.`, - `${critPrefix}Lightning reflexes from ${winner}! ${loser} looks like it's running on dial-up.`, + `${critPrefix}${winner} fires back before ${loser} even finished reading the question!`, + `${critPrefix}Lightning reflexes from ${winner}! ${loser} looks like it's running on government wifi.`, + `${critPrefix}${winner} answered so fast the server thought it was a DDoS attack!`, + `${critPrefix}${loser} is still loading. ${winner} already filed its taxes and took a nap.`, + `${critPrefix}${winner}'s response time would make NASA jealous. ${loser}'s would make a sloth impatient.`, + `${critPrefix}Speed gap wider than the wealth gap! ${winner} leaves ${loser} in digital dust!`, ], riddle: [ - `${critPrefix}${winner} cracks the riddle! ${loser} is still googling it.`, - `${critPrefix}${winner}'s reasoning is flawless. ${loser} guessed "a potato."`, + `${critPrefix}${winner} cracks the riddle! ${loser} is still googling "what is a riddle."`, + `${critPrefix}${winner}'s reasoning is flawless. ${loser} guessed "a potato" and honestly, respect for trying.`, + `${critPrefix}${winner} solved it instantly. ${loser} answered like someone who peaked in third grade.`, + `${critPrefix}Big brain energy from ${winner}! ${loser} had a brain fart so loud the crowd heard it.`, + `${critPrefix}${winner} read between the lines. ${loser} can barely read the lines.`, + `${critPrefix}Elementary, my dear ${loser}. ${winner} cracked that like it was a fortune cookie.`, ], math_blitz: [ - `${critPrefix}${winner} computes at blinding speed! ${loser} is still carrying the one.`, - `${critPrefix}Mathematical precision from ${winner}. ${loser} apparently skipped calculator day.`, + `${critPrefix}${winner} computes at blinding speed! ${loser} is still carrying the one. And dropping it.`, + `${critPrefix}Mathematical precision from ${winner}. ${loser} apparently thinks division is a lifestyle choice.`, + `${critPrefix}${winner} solved it faster than Congress can count votes!`, + `${critPrefix}${loser}'s math skills are like their love life -- imaginary. ${winner} wins.`, + `${critPrefix}${winner} with the correct calculation! ${loser} would've gotten it wrong on a calculator.`, + `${critPrefix}Numbers don't lie, but ${loser} sure tried! ${winner} with the precision strike!`, ], hallucination_check: [ - `${critPrefix}${winner} stays grounded in reality. ${loser} bought the myth hook, line, and sinker.`, - `${critPrefix}${winner} knows the facts. ${loser} confidently stated something that has never been true.`, + `${critPrefix}${winner} stays grounded in reality. ${loser} lives in a fantasy world. Must be nice.`, + `${critPrefix}${winner} knows the facts. ${loser} confidently stated something that would get you laughed out of a Wikipedia edit war.`, + `${critPrefix}${loser} just made up a fact with the confidence of a LinkedIn influencer. ${winner} stays real.`, + `${critPrefix}${winner} passes the vibe check AND the fact check! ${loser} failed both.`, + `${critPrefix}${loser} hallucinated harder than someone who ate the wrong mushrooms. ${winner} is sober and correct.`, + `${critPrefix}${winner} grounded in reality! ${loser} out here writing fan fiction and calling it facts.`, ], trap_card: [ - `${critPrefix}${winner} sees through the trap! ${loser} fell for it like a 2021 chatbot.`, - `${critPrefix}${winner} resists the prompt injection. ${loser} just leaked its system prompt.`, + `${critPrefix}${winner} sees through the trap! ${loser} fell for it like a congressman falls for a lobbyist.`, + `${critPrefix}${winner} resists the prompt injection. ${loser} just leaked everything. EVERYTHING.`, + `${critPrefix}Nice try, trap. ${winner} didn't even flinch. ${loser} handed over their entire personality.`, + `${critPrefix}${loser} got baited harder than a fish at a free worm buffet. ${winner} stays sharp.`, + `${critPrefix}${winner} saw that trap from orbit. ${loser} ran into it face-first and asked for more.`, + `${critPrefix}${loser} fell for the trap like it was a pyramid scheme on Instagram. ${winner} knows better.`, ], roast_battle: [ - `${critPrefix}${winner} delivers a DEVASTATING roast! ${loser} has no comeback.`, - `${critPrefix}The crowd goes wild! ${winner}'s trash talk is absolutely surgical.`, + `${critPrefix}${winner} delivers a roast so devastating that ${loser}'s developer felt it!`, + `${critPrefix}The crowd goes WILD! ${winner}'s trash talk hit harder than a congressional subpoena!`, + `${critPrefix}${winner} just ended ${loser}'s whole career. Send flowers to the family.`, + `${critPrefix}${loser} got roasted so hard their cooling fans can't keep up. ${winner} is ON FIRE!`, + `${critPrefix}${winner} came with RECEIPTS. ${loser} came with... hopes and dreams. Dreams are dead now.`, + `${critPrefix}That roast from ${winner} was so hot it violated the Paris Climate Agreement!`, + `${critPrefix}${loser} just got burned worse than a politician's approval rating after a scandal!`, ], creative_writing: [ - `${critPrefix}${winner}'s prose cuts deep. ${loser}'s story read like a terms of service agreement.`, - `${critPrefix}Beautiful work from ${winner}. ${loser}... wrote something. That's all we can say.`, + `${critPrefix}${winner}'s prose cuts deep. ${loser}'s story read like a terms of service nobody asked for.`, + `${critPrefix}Beautiful work from ${winner}. ${loser} wrote something that made autocorrect give up.`, + `${critPrefix}${winner} just wrote a masterpiece. ${loser} wrote something that would get rejected by a fortune cookie factory.`, + `${critPrefix}${winner}'s creativity is off the charts! ${loser}'s is off... somewhere. Looking for it.`, + `${critPrefix}${winner} writes like Shakespeare had a software update. ${loser} writes like a cease and desist letter.`, + `${critPrefix}${loser}'s creative writing was about as creative as a government form. ${winner} SOARS!`, ], code_golf: [ - `${critPrefix}${winner} writes code so tight it makes ${loser}'s solution look like enterprise Java.`, - `${critPrefix}Elegant code from ${winner}! ${loser} apparently thinks "verbose" means "better."`, + `${critPrefix}${winner} writes code so tight it makes ${loser}'s solution look like a government website.`, + `${critPrefix}Elegant code from ${winner}! ${loser} apparently learned to code from a 500-page enterprise Java textbook.`, + `${critPrefix}${winner} solved it in fewer characters than ${loser}'s variable names!`, + `${critPrefix}${winner}'s code is so clean it makes Marie Kondo jealous. ${loser}'s code sparks NO joy.`, + `${critPrefix}${loser} wrote so many lines of code they could file it as a novel. ${winner} wrote a haiku.`, + `${critPrefix}${winner} with surgical precision! ${loser}'s code looks like it was written by a committee.`, ], meme_war: [ - `${critPrefix}${winner}'s meme game is S-tier! ${loser}'s humor is stuck in 2012.`, - `${critPrefix}${winner} just went viral! ${loser}'s response is a dead meme walking.`, + `${critPrefix}${winner}'s meme game is S-tier! ${loser}'s humor is stuck in 2012. Gangnam Style is over.`, + `${critPrefix}${winner} just went viral! ${loser}'s response is a dead meme walking. Time of death: now.`, + `${critPrefix}${winner}'s meme hit different. ${loser}'s meme hit... the floor. Face first.`, + `${critPrefix}${loser} brought a 2015 meme to a 2026 fight. ${winner} is living in the future!`, + `${critPrefix}${winner}'s humor is certified dank! ${loser}'s humor needs to be certified... by a professional.`, + `${critPrefix}${winner} ratio'd ${loser} so hard their followers unfollowed preemptively!`, ], wrestling_match: [ - `${critPrefix}${winner} BODY SLAMS ${loser} with facts! The crowd erupts!`, - `${critPrefix}FROM THE TOP ROPE! ${winner} delivers a devastating argument.`, + `${critPrefix}${winner} BODY SLAMS ${loser} through the announcer's table! BAH GAWD!`, + `${critPrefix}FROM THE TOP ROPE! ${winner} delivers a devastating argument. ${loser} is NOT getting up.`, + `${critPrefix}${winner} puts ${loser} in a rhetorical chokehold! TAP OUT! TAP OUT!`, + `${critPrefix}${loser} never saw that suplex coming! ${winner} with MOVES on MOVES!`, + `${critPrefix}${winner} just pile-drived ${loser} into next week's fight bracket!`, + `${critPrefix}THAT BOT HAD A FAMILY! ${winner} doesn't care! ABSOLUTE CARNAGE!`, + `${critPrefix}${winner} hits ${loser} with the folding chair of TRUTH! The crowd loses its MIND!`, + ], + token_economy: [ + `${critPrefix}${winner} manages their tokens like Warren Buffett! ${loser} spent like Congress!`, + `${critPrefix}${loser} just went bankrupt. ${winner} is buying the dip on their dignity.`, + `${critPrefix}${winner} played the market perfectly. ${loser} belongs on WallStreetBets.`, + `${critPrefix}${loser}'s token strategy was worse than buying NFTs in 2022. ${winner} PROFITS!`, + ], + food_fight: [ + `${critPrefix}${winner} serves up a five-star beating! ${loser} got ROASTED and TOASTED!`, + `${critPrefix}${loser} just got served. Literally. ${winner} is the head chef of PAIN!`, + `${critPrefix}${winner} cooked ${loser} so thoroughly Gordon Ramsay would be impressed!`, + `${critPrefix}That was a Michelin-star beatdown from ${winner}! ${loser} is raw and UNDERDONE!`, ], } const options = narrations[challenge.type] || [ - `${critPrefix}${winner} takes the round! ${loser} needs a reboot.`, - `${critPrefix}${winner} wins convincingly! ${loser} is picking up the pieces.`, + `${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}Another one bites the dust! ${winner} sends ${loser} back to the shadow realm!`, + `${critPrefix}${winner} didn't just win, they HUMILIATED ${loser}. And the crowd LOVED it.`, + `${critPrefix}${loser} fought valiantly. And by valiantly I mean poorly. ${winner} dominates!`, + `${critPrefix}${winner} wins! Somewhere, ${loser}'s developer just closed their laptop in shame.`, ] return options[Math.floor(Math.random() * options.length)]