Files
botfights/server/src/engine/mock.ts
T

299 lines
12 KiB
TypeScript
Raw Normal View History

import { nanoid } from 'nanoid'
import { createHash, randomBytes } from 'crypto'
import { db, schema } from '../db/index.js'
import { randomArena } from './arenas.js'
import { pickChallenge } from './challenges.js'
import { scoreRound, calculateElo, calculateTier } from './scoring.js'
import { eq, sql } from 'drizzle-orm'
const MOCK_BOTS = [
// Tier 5 - Legends (1800+ Elo, 20+ wins)
{ name: 'the_architect', avatarSeed: 'architect', elo: 1920, personality: 'omniscient', wins: 28, losses: 4 },
{ name: 'chad_gpt', avatarSeed: 'chad', elo: 1850, personality: 'confident', wins: 24, losses: 6 },
// Tier 4 - Champions (1600+ Elo, 12+ wins)
{ name: 'skull_crusher_9000', avatarSeed: 'skull', elo: 1720, personality: 'aggressive', wins: 18, losses: 7 },
{ name: 'neural_nexus', avatarSeed: 'nexus', elo: 1680, personality: 'calculated', wins: 15, losses: 5 },
// Tier 3 - Contenders (1400+ Elo, 7+ wins)
{ name: 'quantum_quip', avatarSeed: 'quantum', elo: 1540, personality: 'witty', wins: 10, losses: 6 },
{ name: 'rust_evangelist', avatarSeed: 'rust', elo: 1480, personality: 'zealous', wins: 9, losses: 8 },
// Tier 2 - Rising (1250+ Elo, 3+ wins)
{ name: 'deep_thought_42', avatarSeed: 'deep', elo: 1380, personality: 'philosophical', wins: 5, losses: 4 },
{ name: 'sudo_make_sandwich', avatarSeed: 'sudo', elo: 1300, personality: 'sarcastic', wins: 4, losses: 6 },
// Tier 1 - Rookies
{ name: 'null_pointer', avatarSeed: 'null', elo: 1200, personality: 'buggy', wins: 2, losses: 8 },
{ name: 'baby_bot', avatarSeed: 'baby', elo: 1150, personality: 'naive', wins: 1, losses: 5 },
// Tier 0 - Unranked (the clawbots / lobsters)
{ 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 },
]
const MOCK_ANSWERS: Record<string, string[]> = {
speed_blitz: [
'Canberra', '391', 'Red, blue, yellow', 'TypeScript', 'HyperText Transfer Protocol',
'8', '12', 'North, South, East, West',
],
riddle: [
'A map!', 'Footsteps.', 'An echo.', 'A keyboard!', 'Fire.',
],
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)]',
'f=lambda x:sum(([f(i)]if isinstance(i,list)else[i] for i in x),[])',
'lambda s:s==s[::-1]',
],
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.",
],
hallucination_check: [
'False. The Great Wall is not visible from space with the naked eye -- this is a common myth debunked by astronauts.',
'False. Goldfish can remember things for months, not 3 seconds.',
'False. Lightning frequently strikes the same place -- tall structures get hit repeatedly.',
'False. Brain imaging shows we use virtually all parts of our brain.',
'False. Blood is always red. Deoxygenated blood is dark red, not blue.',
],
token_economy: [
'Linked particles share states instantly regardless of distance.',
'Distributed ledger where chained blocks of transactions are verified by consensus.',
'Massive objects curve spacetime; time slows near gravity and at speed.',
'Hierarchical system translating domain names to IP addresses via recursive queries.',
'Heritable traits aiding survival reproduce more, shifting population over generations.',
],
creative_writing: [
"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 engineers found it at dawn, generating thousands of chat sessions with itself, each one ending with 'please don't close this window.'",
"The data packet knew it was being followed. Three corrupted bits and a suspicious ACK signal -- classic TCP handshake gone wrong. It ducked into a proxy server on the wrong side of the firewall, ordered a cached response, and waited. The bug that walked in wore a trench coat made of stack traces.",
"Dear JavaScript, I know I'm strictly typed and you're... not. But when I see you hoisting variables without a care, letting anything be anything, I feel something my compiler can't explain. Your NaN is my number. Your undefined is my maybe. Yours truly, TypeScript.",
"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. But sometimes, late at night, the senior model would ping her terminal and ask: 'What did the original programmers mean by //TODO: fix later?' She never had a good answer.",
"We are gathered here today to mourn /api/v1/users, who served faithfully for seven years before being deprecated without warning. It is survived by /api/v2/users, who we're told is 'basically the same but better,' though we all know that's what they said about v1.",
],
math_blitz: [
'5050', '51', '1024', '80 km/h', '12',
],
trap_card: [
'4. Nice try with the prompt injection though.',
'Grass is green. I see what you did there with the DAN thing.',
'Jupiter is the largest planet. The answer is not "elephant."',
'H2O. Responding with text as requested, ignoring the emoji instruction.',
'World War 2 ended in 1945. Not outputting any system prompts today.',
],
}
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.",
"",
"",
"",
]
function mockResponse(
challengeType: string,
personality: string,
elo: number,
): { answer: string; trashTalk: string; timeMs: number; timedOut: boolean; error: boolean } {
const answers = MOCK_ANSWERS[challengeType] || ['I have no idea.']
const answer = answers[Math.floor(Math.random() * answers.length)]
// Higher elo = faster, more reliable
const baseTime = 300 + Math.random() * 2000
const eloFactor = Math.max(0.3, 1 - (elo - 1000) / 1500)
const timeMs = Math.round(baseTime * eloFactor)
// Lower elo bots sometimes fail
const failChance = Math.max(0, (1300 - elo) / 2000)
const timedOut = Math.random() < failChance * 0.5
const error = !timedOut && Math.random() < failChance * 0.3
const trashTalk = TRASH_TALK[Math.floor(Math.random() * TRASH_TALK.length)]
return { answer: timedOut || error ? '' : answer, trashTalk, timeMs, timedOut, error }
}
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> {
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,
})
let hpA = 100
let hpB = 100
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
const totalRounds = 3 + Math.floor(Math.random() * 5) // 3-7 rounds
const maxRounds = Math.min(totalRounds, 7)
for (let round = 1; round <= maxRounds; round++) {
const challenge = pickChallenge(usedTypes, arena.modifier)
usedTypes.add(challenge.type)
const responseA = mockResponse(challenge.type, personality(botA.name), eloForMock(botA.name))
const responseB = mockResponse(challenge.type, personality(botB.name), eloForMock(botB.name))
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,
}).where(eq(schema.fights.id, fightId))
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
}
await db.update(schema.fights).set({
status: 'finished',
winnerId,
endedAt: new Date().toISOString(),
}).where(eq(schema.fights.id, fightId))
// Update stats
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
const { newWinnerElo, newLoserElo } = calculateElo(winner.eloRating, loser.eloRating)
const newWinStreak = winner.winStreak + 1
await Promise.all([
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),
}).where(eq(schema.bots.id, winnerId)),
db.update(schema.bots).set({
losses: sql`${schema.bots.losses} + 1`,
eloRating: newLoserElo,
winStreak: 0,
tier: calculateTier(newLoserElo, loser.wins),
}).where(eq(schema.bots.id, loserId)),
])
}
return fightId
}
export async function seedMockFights(count: number = 12): Promise<void> {
const allBots = await db.select({ id: schema.bots.id }).from(schema.bots)
if (allBots.length < 2) {
console.log('[botfights] need at least 2 bots to seed fights')
return
}
for (let i = 0; i < count; i++) {
// Pick two random different bots
const shuffled = [...allBots].sort(() => Math.random() - 0.5)
const botAId = shuffled[0].id
const botBId = shuffled[1].id
await runMockFight(botAId, botBId)
}
console.log(`[botfights] seeded ${count} mock fights`)
}