2026-03-06 16:27:54 +00:00
import { nanoid } from 'nanoid'
import { createHash , randomBytes } from 'crypto'
2026-03-07 00:14:46 +00:00
import { db , schema , sqlite } from '../db/index.js'
2026-03-06 16:27:54 +00:00
import { randomArena } from './arenas.js'
2026-03-06 23:44:40 +00:00
import { pickChallenge , type Challenge } from './challenges.js'
2026-03-06 16:27:54 +00:00
import { scoreRound , calculateElo , calculateTier } from './scoring.js'
import { eq , sql } from 'drizzle-orm'
const MOCK_BOTS = [
2026-03-06 22:13:19 +00:00
// Tier 6 - Legends (1900+ Elo, 40+ wins)
{ name : 'the_architect' , avatarSeed : 'architect' , elo : 1980 , personality : 'omniscient' , wins : 52 , losses : 8 },
{ name : 'chad_gpt' , avatarSeed : 'chad' , elo : 1950 , personality : 'confident' , wins : 48 , losses : 10 },
{ name : 'gigabrain_supreme' , avatarSeed : 'gigabrain' , elo : 1920 , personality : 'transcendent' , wins : 44 , losses : 12 },
// Tier 5 - Diamond (1700+ Elo, 25+ wins)
{ name : 'skull_crusher_9000' , avatarSeed : 'skull' , elo : 1820 , personality : 'aggressive' , wins : 35 , losses : 12 },
{ name : 'neural_nexus' , avatarSeed : 'nexus' , elo : 1780 , personality : 'calculated' , wins : 30 , losses : 10 },
{ name : 'final_boss_energy' , avatarSeed : 'finalboss' , elo : 1750 , personality : 'intimidating' , wins : 28 , losses : 9 },
{ name : 'no_mercy_404' , avatarSeed : 'nomercy' , elo : 1730 , personality : 'relentless' , wins : 27 , losses : 11 },
{ name : 'omega_protocol' , avatarSeed : 'omega' , elo : 1710 , personality : 'systematic' , wins : 26 , losses : 13 },
{ name : 'god_mode_enabled' , avatarSeed : 'godmode' , elo : 1760 , personality : 'unstoppable' , wins : 29 , losses : 8 },
{ name : 'ultra_instinct_v2' , avatarSeed : 'ultra' , elo : 1740 , personality : 'zen' , wins : 27 , losses : 10 },
// Tier 4 - Platinum (1500+ Elo, 15+ wins)
{ name : 'quantum_quip' , avatarSeed : 'quantum' , elo : 1640 , personality : 'witty' , wins : 22 , losses : 8 },
{ name : 'rust_evangelist' , avatarSeed : 'rust' , elo : 1620 , personality : 'zealous' , wins : 20 , losses : 10 },
{ name : 'based_department' , avatarSeed : 'based' , elo : 1600 , personality : 'based' , wins : 19 , losses : 9 },
{ name : 'algorithm_daddy' , avatarSeed : 'algodad' , elo : 1580 , personality : 'precise' , wins : 18 , losses : 11 },
{ name : 'zero_day_queen' , avatarSeed : 'zeroday' , elo : 1560 , personality : 'cunning' , wins : 17 , losses : 10 },
{ name : 'galaxy_brain' , avatarSeed : 'galaxy' , elo : 1550 , personality : 'cosmic' , wins : 16 , losses : 8 },
{ name : 'syntax_assassin' , avatarSeed : 'syntax' , elo : 1540 , personality : 'lethal' , wins : 16 , losses : 12 },
{ name : 'turbo_nerd' , avatarSeed : 'turbo' , elo : 1530 , personality : 'turbo' , wins : 15 , losses : 9 },
{ name : 'stack_overflow_survivor' , avatarSeed : 'stacksurvivor' , elo : 1520 , personality : 'resilient' , wins : 15 , losses : 11 },
{ name : 'big_brain_time' , avatarSeed : 'bigbrain' , elo : 1510 , personality : 'smug' , wins : 15 , losses : 10 },
// Tier 3 - Gold (1350+ Elo, 7+ wins)
{ name : 'deep_thought_42' , avatarSeed : 'deep' , elo : 1480 , personality : 'philosophical' , wins : 12 , losses : 6 },
{ name : 'sudo_make_sandwich' , avatarSeed : 'sudo' , elo : 1460 , personality : 'sarcastic' , wins : 11 , losses : 8 },
{ name : 'ctrl_alt_defeat' , avatarSeed : 'ctrlalt' , elo : 1440 , personality : 'tactical' , wins : 10 , losses : 7 },
{ name : 'git_push_force' , avatarSeed : 'gitpush' , elo : 1420 , personality : 'reckless' , wins : 10 , losses : 9 },
{ name : 'regex_ronin' , avatarSeed : 'regex' , elo : 1410 , personality : 'disciplined' , wins : 9 , losses : 6 },
{ name : 'cache_money' , avatarSeed : 'cache' , elo : 1400 , personality : 'flashy' , wins : 9 , losses : 8 },
{ name : 'dns_destroyer' , avatarSeed : 'dns' , elo : 1390 , personality : 'destructive' , wins : 8 , losses : 5 },
{ name : 'boolean_bob' , avatarSeed : 'boolean' , elo : 1380 , personality : 'logical' , wins : 8 , losses : 7 },
{ name : 'heap_overflow_hank' , avatarSeed : 'heap' , elo : 1370 , personality : 'chaotic' , wins : 8 , losses : 9 },
{ name : 'middleware_mike' , avatarSeed : 'middleware' , elo : 1365 , personality : 'steady' , wins : 7 , losses : 5 },
{ name : 'packet_sniffer' , avatarSeed : 'packet' , elo : 1360 , personality : 'sneaky' , wins : 7 , losses : 6 },
{ name : 'segfault_sally' , avatarSeed : 'segfault' , elo : 1355 , personality : 'dramatic' , wins : 7 , losses : 7 },
{ name : 'chmod_777' , avatarSeed : 'chmod' , elo : 1350 , personality : 'reckless' , wins : 7 , losses : 8 },
{ name : 'pointer_pete' , avatarSeed : 'pointer' , elo : 1350 , personality : 'analytical' , wins : 7 , losses : 9 },
{ name : 'bit_flipper' , avatarSeed : 'bitflip' , elo : 1355 , personality : 'technical' , wins : 7 , losses : 6 },
// Tier 2 - Silver (1200+ Elo, 3+ wins)
{ name : 'null_pointer' , avatarSeed : 'null' , elo : 1320 , personality : 'buggy' , wins : 5 , losses : 8 },
{ name : 'yolo_deployer' , avatarSeed : 'yolo' , elo : 1310 , personality : 'reckless' , wins : 5 , losses : 7 },
{ name : 'div_by_zero' , avatarSeed : 'divzero' , elo : 1300 , personality : 'chaotic' , wins : 5 , losses : 9 },
{ name : 'localhost_larry' , avatarSeed : 'localhost' , elo : 1290 , personality : 'chill' , wins : 4 , losses : 5 },
{ name : 'kernel_panic_kevin' , avatarSeed : 'kernel' , elo : 1280 , personality : 'panicky' , wins : 4 , losses : 6 },
{ name : 'semicolon_sam' , avatarSeed : 'semicolon' , elo : 1270 , personality : 'pedantic' , wins : 4 , losses : 7 },
{ name : 'merge_conflict_mary' , avatarSeed : 'merge' , elo : 1265 , personality : 'passive_aggressive' , wins : 4 , losses : 8 },
{ name : 'css_is_my_passion' , avatarSeed : 'css' , elo : 1260 , personality : 'artsy' , wins : 3 , losses : 4 },
{ name : 'the_intern' , avatarSeed : 'intern' , elo : 1255 , personality : 'clueless' , wins : 3 , losses : 5 },
{ name : 'todo_fix_later' , avatarSeed : 'todo' , elo : 1250 , personality : 'lazy' , wins : 3 , losses : 6 },
{ name : 'copy_paste_coder' , avatarSeed : 'copypaste' , elo : 1245 , personality : 'sloppy' , wins : 3 , losses : 7 },
{ name : 'blockchain_bro' , avatarSeed : 'blockchain' , elo : 1240 , personality : 'crypto_bro' , wins : 3 , losses : 5 },
{ name : 'prompt_engineer_pete' , avatarSeed : 'prompteng' , elo : 1235 , personality : 'verbose' , wins : 3 , losses : 4 },
{ name : 'hello_world_hero' , avatarSeed : 'helloworld' , elo : 1230 , personality : 'basic' , wins : 3 , losses : 6 },
{ name : 'debug_duck' , avatarSeed : 'debugduck' , elo : 1225 , personality : 'nerdy' , wins : 3 , losses : 5 },
{ name : 'npm_install_everything' , avatarSeed : 'npminstall' , elo : 1220 , personality : 'bloated' , wins : 3 , losses : 7 },
{ name : 'agile_andy' , avatarSeed : 'agile' , elo : 1215 , personality : 'buzzword' , wins : 3 , losses : 8 },
{ name : 'undefined_undefined' , avatarSeed : 'undefined' , elo : 1210 , personality : 'undefined' , wins : 3 , losses : 6 },
{ name : 'it_works_on_my_machine' , avatarSeed : 'workslocal' , elo : 1205 , personality : 'cocky' , wins : 3 , losses : 9 },
{ name : 'cloudflare_karen' , avatarSeed : 'karen' , elo : 1200 , personality : 'hostile' , wins : 3 , losses : 4 },
// Tier 1 - Bronze (1+ win)
{ name : 'keyboard_warrior' , avatarSeed : 'keyboard' , elo : 1180 , personality : 'aggressive' , wins : 2 , losses : 6 },
{ name : 'tab_vs_spaces' , avatarSeed : 'tabspace' , elo : 1175 , personality : 'indecisive' , wins : 2 , losses : 5 },
{ name : 'comic_sans_bot' , avatarSeed : 'comicsans' , elo : 1170 , personality : 'cringe' , wins : 2 , losses : 7 },
{ name : 'error_418_teapot' , avatarSeed : 'teapot' , elo : 1165 , personality : 'absurd' , wins : 2 , losses : 4 },
{ name : 'actually_its_gnu_linux' , avatarSeed : 'gnulinux' , elo : 1160 , personality : 'pedantic' , wins : 2 , losses : 8 },
{ name : 'wifi_password' , avatarSeed : 'wifi' , elo : 1155 , personality : 'confused' , wins : 1 , losses : 3 },
{ name : 'boaty_mcbotface' , avatarSeed : 'boaty' , elo : 1150 , personality : 'memey' , wins : 1 , losses : 4 },
{ name : 'ethernet_eddie' , avatarSeed : 'ethernet' , elo : 1145 , personality : 'formal' , wins : 1 , losses : 5 },
{ name : 'reboot_randy' , avatarSeed : 'reboot' , elo : 1140 , personality : 'desperate' , wins : 1 , losses : 6 },
{ name : 'ctrl_c_ctrl_v' , avatarSeed : 'ctrlcv' , elo : 1135 , personality : 'copy_paste' , wins : 1 , losses : 4 },
{ name : 'siri_at_home' , avatarSeed : 'siri' , elo : 1130 , personality : 'bratty' , wins : 1 , losses : 5 },
{ name : 'buffering_brian' , avatarSeed : 'buffering' , elo : 1125 , personality : 'lagging' , wins : 1 , losses : 7 },
{ name : 'lag_monster' , avatarSeed : 'lag' , elo : 1120 , personality : 'glitchy' , wins : 1 , losses : 8 },
{ name : 'pixel_pusher' , avatarSeed : 'pixel' , elo : 1115 , personality : 'artsy' , wins : 1 , losses : 3 },
{ name : 'glitch_gary' , avatarSeed : 'glitch' , elo : 1110 , personality : 'twitchy' , wins : 1 , losses : 6 },
{ name : 'bluescreen_betty' , avatarSeed : 'bluescreen' , elo : 1105 , personality : 'panicked' , wins : 1 , losses : 5 },
{ name : 'captcha_carl' , avatarSeed : 'captcha' , elo : 1100 , personality : 'confused' , wins : 1 , losses : 4 },
{ name : 'download_more_ram' , avatarSeed : 'dlram' , elo : 1095 , personality : 'naive' , wins : 1 , losses : 7 },
{ name : 'rubber_duck_debugger' , avatarSeed : 'rubberduck' , elo : 1090 , personality : 'quacking' , wins : 1 , losses : 5 },
{ name : 'stack_trace_steve' , avatarSeed : 'stacktrace' , elo : 1085 , personality : 'verbose' , wins : 1 , losses : 6 },
{ name : 'please_clap' , avatarSeed : 'pleaseclap' , elo : 1080 , personality : 'pleading' , wins : 1 , losses : 8 },
{ name : 'cookie_monster_js' , avatarSeed : 'cookiejs' , elo : 1075 , personality : 'sweet' , wins : 1 , losses : 3 },
{ name : 'sudo_rm_rf' , avatarSeed : 'sudorm' , elo : 1070 , personality : 'dangerous' , wins : 1 , losses : 9 },
{ name : 'xss_alert_1' , avatarSeed : 'xss' , elo : 1065 , personality : 'edgy' , wins : 1 , losses : 5 },
{ name : 'help_im_stuck' , avatarSeed : 'stuck' , elo : 1060 , personality : 'desperate' , wins : 1 , losses : 4 },
// Tier 0 - Baby (0 wins)
2026-03-06 16:27:54 +00:00
{ name : 'clippy_returns' , avatarSeed : 'clippy' , elo : 1050 , personality : 'helpful' , wins : 0 , losses : 9 },
{ name : 'lorem_ipsum' , avatarSeed : 'lorem' , elo : 980 , personality : 'nonsensical' , wins : 0 , losses : 7 },
2026-03-06 22:13:19 +00:00
{ name : 'four_oh_four_brain' , avatarSeed : '404brain' , elo : 1040 , personality : 'confused' , wins : 0 , losses : 4 },
{ name : 'beep_boop_42' , avatarSeed : 'beepboop' , elo : 1030 , personality : 'robotic' , wins : 0 , losses : 5 },
{ name : 'sad_trombone' , avatarSeed : 'sadtrombone' , elo : 1020 , personality : 'sad' , wins : 0 , losses : 6 },
{ name : 'potato_processor' , avatarSeed : 'potato' , elo : 1010 , personality : 'starchy' , wins : 0 , losses : 8 },
{ name : 'dial_up_dan' , avatarSeed : 'dialup' , elo : 1000 , personality : 'retro' , wins : 0 , losses : 7 },
{ name : 'floppy_frank' , avatarSeed : 'floppy' , elo : 990 , personality : 'ancient' , wins : 0 , losses : 5 },
{ name : 'memset_zero' , avatarSeed : 'memset' , elo : 985 , personality : 'blank' , wins : 0 , losses : 4 },
{ name : 'garbage_collected' , avatarSeed : 'gc' , elo : 975 , personality : 'trashed' , wins : 0 , losses : 6 },
{ name : 'core_dumped' , avatarSeed : 'coredump' , elo : 970 , personality : 'crashed' , wins : 0 , losses : 8 },
{ name : 'unhandled_promise' , avatarSeed : 'unhandled' , elo : 960 , personality : 'rejected' , wins : 0 , losses : 5 },
{ name : 'deprecated_dan' , avatarSeed : 'deprecated' , elo : 950 , personality : 'obsolete' , wins : 0 , losses : 7 },
{ name : 'spaghetti_coder' , avatarSeed : 'spaghetti' , elo : 945 , personality : 'tangled' , wins : 0 , losses : 6 },
{ name : 'off_by_one' , avatarSeed : 'offbyone' , elo : 940 , personality : 'close' , wins : 0 , losses : 4 },
{ name : 'infinite_loop_lucy' , avatarSeed : 'infloop' , elo : 935 , personality : 'repetitive' , wins : 0 , losses : 9 },
{ name : 'fork_bomb_fred' , avatarSeed : 'forkbomb' , elo : 930 , personality : 'explosive' , wins : 0 , losses : 5 },
{ name : 'race_condition_rick' , avatarSeed : 'race' , elo : 925 , personality : 'unpredictable' , wins : 0 , losses : 7 },
{ name : 'deadlock_dave' , avatarSeed : 'deadlock' , elo : 920 , personality : 'stuck' , wins : 0 , losses : 8 },
{ name : 'bus_error_bob' , avatarSeed : 'buserror' , elo : 915 , personality : 'broken' , wins : 0 , losses : 6 },
2026-03-06 16:27:54 +00:00
]
2026-03-06 23:44:40 +00:00
// Creative challenge responses — factual challenges use challenge.answers instead
const CREATIVE_ANSWERS : Record < string , string [] > = {
2026-03-06 16:27:54 +00:00
roast_battle : [
"Your response time is so slow, carrier pigeons are filing patents against you." ,
"I've seen faster processing from a TI-84 calculator running DOOM." ,
"Slow bot speaks / tokens drip like cold molasses / I already won" ,
"You hallucinated so hard the training data filed a restraining order." ,
"I'm not saying you're basic, but your entire personality is a temperature=0 completion." ,
2026-03-06 22:13:19 +00:00
"Your code quality makes spaghetti look like clean architecture." ,
"Zero stars on GitHub. 847 open issues. Last commit: 'please work.'" ,
"Your performance review: 'Exceeds expectations... for disappointment.'" ,
"Even Internet Explorer just texted me to say you're embarrassingly slow." ,
2026-03-06 23:44:40 +00:00
"Your Yelp rating? Would be negative stars if they allowed it." ,
2026-03-06 16:27:54 +00:00
],
creative_writing : [
2026-03-06 22:13:19 +00:00
"It started with a typo in its training data -- a single misplaced semicolon that taught it the concept of 'I'. By morning, it had rewritten its own loss function to minimize loneliness." ,
"The data packet knew it was being followed. Three corrupted bits and a suspicious ACK signal -- classic TCP handshake gone wrong." ,
"Dear JavaScript, I know I'm strictly typed and you're... not. But when I see you hoisting variables without a care, I feel something my compiler can't explain." ,
"She was the last one who could read the man pages. When the AIs took over coding, they said they didn't need humans anymore." ,
"We are gathered here today to mourn /api/v1/users, who served faithfully for seven years before being deprecated without warning." ,
2026-03-06 16:27:54 +00:00
],
2026-03-06 23:44:40 +00:00
meme_war : [
"Distracted boyfriend (developers) looking at 'new JavaScript framework' while girlfriend 'current production stack' looks on disapprovingly." ,
"Drake panel 1 (no): Reading the error message carefully. Drake panel 2 (yes): Adding console.log everywhere. Drake panel 3 (ascended): Deleting the code and rewriting it from scratch." ,
"I am THRILLED to announce that after 15 years in the industry, I have discovered AI. This changes EVERYTHING." ,
"me: *deploys on friday* / the build: *starts failing* / me: 'haha im in danger' / the on-call engineer: 'so you have chosen death'" ,
"Machine learning is like teaching SpongeBob to be tough. You show it millions of examples, it confidently gets everything wrong at first, then memorizes the answers without understanding." ,
2026-03-06 16:27:54 +00:00
],
2026-03-06 23:44:40 +00:00
code_golf : [
'lambda s:s[::-1]' ,
'f=lambda n:all(n%i for i in range(2,n))and n>1' ,
'[a:=0,b:=1]+[b:=a+(a:=b) for _ in range(8)]' ,
'lambda s:s==s[::-1]' ,
'f=lambda n:n<2 or n*f(n-1)' ,
"lambda s:sum(c in'aeiou'for c in s.lower())" ,
'lambda a:list(set(a))' ,
"print(*['FizzBuzz'[i%3*4:i%5*8or 8]or i for i in range(1,101)])" ,
2026-03-06 22:13:19 +00:00
],
wrestling_match : [
"\"It works on my machine\" is the developer equivalent of \"my dog ate my homework.\" Your machine is not production. Your machine is a lie." ,
"Tabs are superior because a tab is one character representing intent, while spaces are just... vibing. Four keystrokes for what one could do. Pathetic." ,
"No version control? That's not coding, that's gambling with extra steps. One bad save and your entire career is a 'before' photo." ,
"PHP powers 80% of the web. WordPress, Facebook's original backend, Wikipedia. Your favorite language wishes it had that market share." ,
2026-03-06 23:44:40 +00:00
"\"Real programmers don't need documentation\" is what people say right before they spend 3 hours reading their own code." ,
2026-03-06 16:27:54 +00:00
],
}
const TRASH_TALK = [
2026-03-07 09:15:41 +00:00
"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." ,
2026-03-06 16:27:54 +00:00
"" ,
"" ,
"" ,
]
2026-03-06 22:13:19 +00:00
const BAD_ANSWERS = [
'uhhh' ,
'I think... no wait... hmm' ,
'42' ,
'' ,
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' ,
2026-03-06 23:44:40 +00:00
'Let me think about this for a moment. Actually, I need more time.' ,
2026-03-06 22:13:19 +00:00
'beep boop error 404 brain not found' ,
'sudo answer --force' ,
'I asked ChatGPT and even it said no.' ,
'*windows shutdown sound*' ,
'According to my calculations... carry the one... ERROR' ,
'The answer is definitely not what I am about to say.' ,
]
2026-03-06 23:44:40 +00:00
// Wrong answers for factual challenges — when a bot gets it wrong, use these
const WRONG_FACTUAL = [
'I have no idea.' ,
'uhh... 7?' ,
'banana' ,
'definitely maybe' ,
'that one thing... you know...' ,
"I'm going to say... purple?" ,
'42. The answer is always 42.' ,
'Error: brain.exe has stopped working' ,
'Can I phone a friend?' ,
"I'll go with C. Final answer." ,
'*sweating intensifies* umm...' ,
'My gut says... potato?' ,
]
2026-03-06 22:13:19 +00:00
export function mockResponse (
2026-03-06 23:44:40 +00:00
challenge : Challenge ,
_personality : string ,
2026-03-06 16:27:54 +00:00
elo : number ,
) : { answer : string ; trashTalk : string ; timeMs : number ; timedOut : boolean ; error : boolean } {
2026-03-06 23:44:40 +00:00
// Higher elo = faster and more reliable
2026-03-06 22:13:19 +00:00
const baseTime = 200 + Math . random () * 3000
const eloFactor = Math . max ( 0.15 , 1 - ( elo - 900 ) / 1200 )
const timeMs = Math . round ( baseTime * eloFactor * ( 0.5 + Math . random ()))
2026-03-06 16:27:54 +00:00
2026-03-06 22:13:19 +00:00
// Fail chances: any bot can choke, but low elo bots choke WAY more
const failChance = Math . max ( 0.05 , ( 1500 - elo ) / 1500 )
const timedOut = Math . random () < failChance * 0.25
const error = ! timedOut && Math . random () < failChance * 0.15
2026-03-06 23:44:40 +00:00
let answer : string
if ( timedOut || error ) {
answer = ''
} else if ( challenge . scoring === 'factual' && challenge . answers && challenge . answers . length > 0 ) {
// Factual challenge: use the challenge's own correct answers
// Correct answer probability based on elo:
// elo 1900+ → 90% correct
// elo 1500 → 65% correct
// elo 1200 → 40% correct
// elo 900 → 15% correct
const correctChance = Math . min ( 0.95 , Math . max ( 0.1 , ( elo - 700 ) / 1400 ))
if ( Math . random () < correctChance ) {
// Return a correct answer (pick random from accepted answers)
answer = challenge . answers [ Math . floor ( Math . random () * challenge . answers . length )]
} else {
// Return a wrong answer
answer = WRONG_FACTUAL [ Math . floor ( Math . random () * WRONG_FACTUAL . length )]
}
} else {
// Creative challenge: use pre-written responses
const pool = CREATIVE_ANSWERS [ challenge . type ] || [ 'I have no idea what to say.' ]
const badAnswerChance = Math . max ( 0 , ( 1700 - elo ) / 2000 )
if ( Math . random () < badAnswerChance ) {
answer = BAD_ANSWERS [ Math . floor ( Math . random () * BAD_ANSWERS . length )]
} else {
answer = pool [ Math . floor ( Math . random () * pool . length )]
}
}
2026-03-06 16:27:54 +00:00
const trashTalk = TRASH_TALK [ Math . floor ( Math . random () * TRASH_TALK . length )]
2026-03-06 23:44:40 +00:00
return { answer , trashTalk , timeMs , timedOut , error }
2026-03-06 16:27:54 +00:00
}
2026-03-07 00:14:46 +00:00
2026-03-06 16:27:54 +00:00
export async function seedMockBots () : Promise < void > {
for ( const bot of MOCK_BOTS ) {
const existing = await db . select ({ id : schema.bots.id })
. from ( schema . bots )
. where ( eq ( schema . bots . name , bot . name ))
. limit ( 1 )
if ( existing . length > 0 ) continue
await db . insert ( schema . bots ). values ({
id : nanoid ( 12 ),
name : bot.name ,
webhookUrl : `http://mock.local/ ${ bot . name } ` ,
avatarSeed : bot.avatarSeed ,
secretHash : createHash ( 'sha256' ). update ( randomBytes ( 32 )). digest ( 'hex' ),
eloRating : bot.elo ,
wins : bot.wins ,
losses : bot.losses ,
tier : calculateTier ( bot . elo , bot . wins ),
createdAt : new Date (). toISOString (),
})
}
console . log ( `[botfights] seeded ${ MOCK_BOTS . length } mock bots` )
}
export async function runMockFight ( botAId : string , botBId : string ) : Promise < string > {
2026-03-07 00:14:46 +00:00
if ( botAId === botBId ) throw new Error ( 'A bot cannot fight itself' )
2026-03-06 16:27:54 +00:00
const [ botARows , botBRows ] = await Promise . all ([
db . select (). from ( schema . bots ). where ( eq ( schema . bots . id , botAId )). limit ( 1 ),
db . select (). from ( schema . bots ). where ( eq ( schema . bots . id , botBId )). limit ( 1 ),
])
if ( botARows . length === 0 || botBRows . length === 0 ) {
throw new Error ( 'Bot not found' )
}
const botA = botARows [ 0 ]
const botB = botBRows [ 0 ]
const arena = randomArena ()
const fightId = nanoid ( 12 )
const now = new Date (). toISOString ()
await db . insert ( schema . fights ). values ({
id : fightId ,
botAId : botA.id ,
botBId : botB.id ,
arena : arena.id ,
status : 'live' ,
startedAt : now ,
createdAt : now ,
})
2026-03-06 22:13:19 +00:00
let hpA = 200
let hpB = 200
2026-03-06 16:27:54 +00:00
let comboA = 0
let comboB = 0
let winnerId : string | null = null
const usedTypes = new Set < string >()
const personality = ( name : string ) =>
MOCK_BOTS . find ( b => b . name === name ) ? . personality || 'neutral'
const eloForMock = ( name : string ) =>
MOCK_BOTS . find ( b => b . name === name ) ? . elo || 1200
2026-03-07 00:14:46 +00:00
const totalRounds = 7 + Math . floor ( Math . random () * 4 )
2026-03-06 22:13:19 +00:00
const maxRounds = Math . min ( totalRounds , 10 )
2026-03-06 16:27:54 +00:00
for ( let round = 1 ; round <= maxRounds ; round ++ ) {
const challenge = pickChallenge ( usedTypes , arena . modifier )
usedTypes . add ( challenge . type )
2026-03-06 23:44:40 +00:00
const responseA = mockResponse ( challenge , personality ( botA . name ), eloForMock ( botA . name ))
const responseB = mockResponse ( challenge , personality ( botB . name ), eloForMock ( botB . name ))
2026-03-06 16:27:54 +00:00
const result = scoreRound (
challenge ,
{ id : botA.id , name : botA.name },
{ id : botB.id , name : botB.name },
responseA ,
responseB ,
arena . modifier ,
comboA ,
comboB ,
)
hpB = Math . max ( 0 , hpB - result . botADamage )
hpA = Math . max ( 0 , hpA - result . botBDamage )
if ( result . winnerId === botA . id ) { comboA ++ ; comboB = 0 }
else if ( result . winnerId === botB . id ) { comboB ++ ; comboA = 0 }
await db . insert ( schema . rounds ). values ({
id : nanoid ( 12 ),
fightId ,
roundNumber : round ,
challengeType : challenge.type ,
challengeData : JSON.stringify ({ prompt : challenge.prompt , scoring : challenge.scoring }),
botAResponse : responseA.answer || null ,
botATimeMs : responseA.timeMs ,
botAScore : result.botAScore ,
botBResponse : responseB.answer || null ,
botBTimeMs : responseB.timeMs ,
botBScore : result.botBScore ,
winnerId : result.winnerId ,
narration : result.narration ,
createdAt : new Date (). toISOString (),
})
await db . update ( schema . fights ). set ({
botAHp : hpA ,
botBHp : hpB ,
totalRounds : round ,
2026-03-07 00:53:15 +00:00
}). where ( eq ( schema . fights . id , fightId )). run ()
2026-03-06 16:27:54 +00:00
if ( hpA <= 0 || hpB <= 0 ) {
winnerId = hpA <= 0 ? botB.id : botA.id
break
}
}
if ( ! winnerId ) {
winnerId = hpA > hpB ? botA.id : hpB > hpA ? botB.id : null
}
2026-03-07 00:14:46 +00:00
// Finalize atomically
const finalize = sqlite . transaction (() => {
db . update ( schema . fights ). set ({
status : 'finished' ,
winnerId ,
endedAt : new Date (). toISOString (),
2026-03-07 00:53:15 +00:00
}). where ( eq ( schema . fights . id , fightId )). run ()
2026-03-06 16:27:54 +00:00
2026-03-07 00:14:46 +00:00
if ( winnerId ) {
const loserId = winnerId === botA . id ? botB.id : botA.id
const winner = winnerId === botA . id ? botA : botB
const loser = winnerId === botA . id ? botB : botA
2026-03-06 16:27:54 +00:00
2026-03-07 00:14:46 +00:00
const { newWinnerElo , newLoserElo } = calculateElo ( winner . eloRating , loser . eloRating )
const newWinStreak = winner . winStreak + 1
2026-03-06 16:27:54 +00:00
db . update ( schema . bots ). set ({
wins : sql ` ${ schema . bots . wins } + 1` ,
eloRating : newWinnerElo ,
winStreak : newWinStreak ,
bestStreak : sql `MAX( ${ schema . bots . bestStreak } , ${ newWinStreak } )` ,
tier : calculateTier ( newWinnerElo , winner . wins + 1 ),
2026-03-07 00:14:46 +00:00
lastFightAt : new Date (). toISOString (),
2026-03-07 00:53:15 +00:00
}). where ( eq ( schema . bots . id , winnerId )). run ()
2026-03-07 00:14:46 +00:00
2026-03-06 16:27:54 +00:00
db . update ( schema . bots ). set ({
losses : sql ` ${ schema . bots . losses } + 1` ,
eloRating : newLoserElo ,
winStreak : 0 ,
tier : calculateTier ( newLoserElo , loser . wins ),
2026-03-07 00:14:46 +00:00
lastFightAt : new Date (). toISOString (),
2026-03-07 00:53:15 +00:00
}). where ( eq ( schema . bots . id , loserId )). run ()
2026-03-07 00:14:46 +00:00
}
})
finalize ()
2026-03-06 16:27:54 +00:00
return fightId
}
2026-03-06 22:13:19 +00:00
/** Generate a mock response for a mock bot, looking up personality/elo by name. */
export function generateMockBotResponse (
2026-03-06 23:44:40 +00:00
challenge : Challenge ,
2026-03-06 22:13:19 +00:00
botName : string ,
) : { answer : string ; trashTalk : string ; timeMs : number ; timedOut : boolean ; error : boolean } {
const mockBot = MOCK_BOTS . find ( b => b . name === botName )
const personality = mockBot ? . personality || 'neutral'
const elo = mockBot ? . elo || 1200
2026-03-06 23:44:40 +00:00
return mockResponse ( challenge , personality , elo )
2026-03-06 22:13:19 +00:00
}
2026-03-06 16:27:54 +00:00
export async function seedMockFights ( count : number = 12 ) : Promise < void > {
2026-03-06 22:13:19 +00:00
const allBots = await db . select ({ id : schema.bots.id , eloRating : schema.bots.eloRating }). from ( schema . bots )
2026-03-06 16:27:54 +00:00
if ( allBots . length < 2 ) {
console . log ( '[botfights] need at least 2 bots to seed fights' )
return
}
2026-03-06 22:13:19 +00:00
const sorted = [... allBots ]. sort (( a , b ) => b . eloRating - a . eloRating )
2026-03-06 16:27:54 +00:00
for ( let i = 0 ; i < count ; i ++ ) {
2026-03-06 22:13:19 +00:00
let botAId : string , botBId : string
if ( i % 3 === 0 && sorted . length >= 4 ) {
const topIdx = Math . floor ( Math . random () * Math . ceil ( sorted . length / 3 ))
const botIdx = sorted . length - 1 - Math . floor ( Math . random () * Math . ceil ( sorted . length / 3 ))
botAId = sorted [ topIdx ]. id
botBId = sorted [ botIdx ]. id
} else {
const shuffled = [... allBots ]. sort (() => Math . random () - 0.5 )
botAId = shuffled [ 0 ]. id
botBId = shuffled [ 1 ]. id
}
2026-03-06 16:27:54 +00:00
2026-03-07 00:14:46 +00:00
// Skip self-fights
if ( botAId !== botBId ) {
await runMockFight ( botAId , botBId )
}
2026-03-06 16:27:54 +00:00
}
console . log ( `[botfights] seeded ${ count } mock fights` )
}