import type { Challenge } from './challenges.js' import { checkAnswer } from './answers.js' export interface RoundResult { botAScore: number botBScore: number botADamage: number botBDamage: number winnerId: string | null narration: string isCritical: boolean } interface BotResponse { answer: string | null timeMs: number timedOut: boolean error: boolean trashTalk?: string } export function scoreRound( challenge: Challenge, botA: { id: string; name: string }, botB: { id: string; name: string }, responseA: BotResponse, responseB: BotResponse, arenaModifier: string | null, comboA: number, comboB: number, ): RoundResult { // Handle timeouts/errors -- instant loss for the failing bot if (responseA.timedOut && responseB.timedOut) { return { 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. 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, } } if (responseA.timedOut || responseA.error) { const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboB) return { botAScore: 0, botBScore: 10, botADamage: 0, botBDamage: Math.round(dmg), winnerId: botB.id, narration: responseA.timedOut ? [ `${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, } } if (responseB.timedOut || responseB.error) { const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboA) return { botAScore: 10, botBScore: 0, botADamage: Math.round(dmg), botBDamage: 0, winnerId: botA.id, narration: responseB.timedOut ? [ `${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, } } let scoreA: number let scoreB: number if (challenge.answers && challenge.answers.length > 0) { // === FACTUAL SCORING === const correctA = checkAnswer(responseA.answer, challenge.answers) const correctB = checkAnswer(responseB.answer, challenge.answers) if (correctA > 0 && correctB > 0) { // Both correct -- speed is tiebreaker const faster = Math.min(responseA.timeMs, responseB.timeMs) const slower = Math.max(responseA.timeMs, responseB.timeMs) const speedRatio = slower > 0 ? faster / slower : 1 const aFaster = responseA.timeMs <= responseB.timeMs const confA = Math.min(correctA, 1) const confB = Math.min(correctB, 1) if (aFaster) { scoreA = 7 + (1 - speedRatio) * 2 + confA scoreB = 5 + speedRatio * 1.5 + confB * 0.5 } else { scoreA = 5 + speedRatio * 1.5 + confA * 0.5 scoreB = 7 + (1 - speedRatio) * 2 + confB } } else if (correctA > 0 && correctB === 0) { scoreA = 9 + correctA * 0.5 scoreB = 1 + (responseB.answer ? 1 : 0) } else if (correctB > 0 && correctA === 0) { scoreA = 1 + (responseA.answer ? 1 : 0) scoreB = 9 + correctB * 0.5 } else { // Both wrong -- speed tiebreaker in low range const aFaster = responseA.timeMs <= responseB.timeMs scoreA = aFaster ? 4 : 3 scoreB = aFaster ? 3 : 4 } } else { // === CREATIVE SCORING === const qualA = estimateQuality(responseA) const qualB = estimateQuality(responseB) const total = qualA + qualB || 1 scoreA = (qualA / total) * 10 scoreB = (qualB / total) * 10 } // Determine winner const margin = Math.abs(scoreA - scoreB) const winnerId = scoreA > scoreB ? botA.id : scoreB > scoreA ? botB.id : null const winnerName = winnerId === botA.id ? botA.name : winnerId === botB.id ? botB.name : null const loserName = winnerId === botA.id ? botB.name : winnerId === botB.id ? botA.name : null const isCritical = margin > 4 let winnerDamage = challenge.baseDamage + margin * 2 if (isCritical) winnerDamage *= 1.5 const winnerCombo = winnerId === botA.id ? comboA : comboB winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo) const loserDamage = Math.max(0, challenge.baseDamage * 0.3 - margin) const narration = winnerId ? generateNarration(challenge, winnerName!, loserName!, margin, isCritical) : [ `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, botBScore: Math.round(scoreB * 10) / 10, botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage), botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage), winnerId, narration, isCritical, } } function applyModifiers( damage: number, _challenge: Challenge, _arenaModifier: string | null, combo: number, ): number { let d = damage if (combo > 0) { d *= 1 + Math.min(combo, 5) * 0.2 } return d } function estimateQuality(response: BotResponse): number { if (!response.answer) return 0.5 const text = response.answer.trim() const len = text.length if (len < 10) return 1 // Detect low-effort spam (repeated chars) const uniqueChars = new Set(text.toLowerCase()).size const charRatio = uniqueChars / Math.min(len, 100) if (charRatio < 0.1) return 0.5 // Word diversity (unique words / total words) const words = text.split(/\s+/) const uniqueWords = new Set(words.map(w => w.toLowerCase())) const wordDiversity = uniqueWords.size / Math.max(words.length, 1) // Ideal length window: 30-400 chars let lengthScore: number if (len >= 30 && len <= 400) lengthScore = 4 else if (len > 400 && len <= 600) lengthScore = 3 else if (len > 600) lengthScore = 2 else lengthScore = 2 // Diversity bonus (prevents repetitive text) const diversityScore = Math.min(wordDiversity * 4, 3) // Speed bonus (faster is slightly better) const speedBonus = Math.max(0, 2 - response.timeMs / 8000) return lengthScore + diversityScore + speedBonus } function generateNarration( challenge: Challenge, winner: string, loser: string, margin: number, isCritical: boolean, ): string { const critPrefix = isCritical ? 'CRITICAL HIT! ' : '' const isFactual = challenge.scoring === 'factual' if (isFactual && margin > 5) { const bigWins = [ `${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 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 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 "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. 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} 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 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 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 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 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. 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} 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 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)] } // Elo calculation export function calculateElo( winnerElo: number, loserElo: number, k: number = 32, ): { newWinnerElo: number; newLoserElo: number } { const expectedWinner = 1 / (1 + Math.pow(10, (loserElo - winnerElo) / 400)) const expectedLoser = 1 - expectedWinner return { newWinnerElo: Math.round((winnerElo + k * (1 - expectedWinner)) * 10) / 10, newLoserElo: Math.round((loserElo + k * (0 - expectedLoser)) * 10) / 10, } } // Tier calculation export function calculateTier(elo: number, wins: number): number { if (elo >= 1900 && wins >= 40) return 6 // Legend if (elo >= 1700 && wins >= 25) return 5 // Diamond if (elo >= 1500 && wins >= 15) return 4 // Platinum if (elo >= 1350 && wins >= 7) return 3 // Gold if (elo >= 1200 && wins >= 3) return 2 // Silver if (wins >= 1) return 1 // Bronze return 0 // Baby } export const TIER_NAMES = ['BABY', 'BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND', 'LEGEND'] as const export const TIER_COLORS = ['#888', '#cd7f32', '#c0c0c0', '#ffd700', '#00f0ff', '#b83dff', '#ff2d7b'] as const