import { db, schema } from '../db/index.js' import { runMockFight } from './mock.js' import { eq } from 'drizzle-orm' import { fightEvents } from './events.js' export interface FightResult { fightId: string botAName: string botBName: string botAElo: number botBElo: number winnerName: string | null winnerId: string | null totalRounds: number isKo: boolean isPerfect: boolean botAHp: number botBHp: number } export interface FightLoopOptions { intervalMs?: number maxFights?: number matchmakingStyle?: 'random' | 'elo_close' | 'mixed' onFightStart?: (botAName: string, botAElo: number, botBName: string, botBElo: number) => void onFightComplete?: (result: FightResult) => void onRoundComplete?: (data: { round: number; hp: { a: number; b: number }; challengeType: string; winnerId: string | null }) => void onError?: (err: Error) => void } export async function startFightLoop(options: FightLoopOptions = {}): Promise { const { intervalMs = 8000, maxFights = Infinity, matchmakingStyle = 'mixed', onFightStart, onFightComplete, onRoundComplete, onError, } = options const allBots = await db.select({ id: schema.bots.id, eloRating: schema.bots.eloRating, name: schema.bots.name, }).from(schema.bots) if (allBots.length < 2) { console.log('[fight-loop] need at least 2 bots, aborting') return } console.log(`[fight-loop] starting with ${allBots.length} bots, ${matchmakingStyle} matchmaking, ${intervalMs}ms interval`) let fightCount = 0 while (fightCount < maxFights) { try { // Re-fetch bots to get updated elo ratings const bots = await db.select({ id: schema.bots.id, eloRating: schema.bots.eloRating, name: schema.bots.name, wins: schema.bots.wins, losses: schema.bots.losses, }).from(schema.bots) const [botA, botB] = pickMatchup(bots, matchmakingStyle, fightCount) if (onFightStart) { onFightStart(botA.name, botA.eloRating, botB.name, botB.eloRating) } // Subscribe to round events for live TUI updates let unsub: (() => void) | undefined if (onRoundComplete) { unsub = fightEvents.onAll((event) => { if (event.type === 'round_end') { onRoundComplete({ round: event.data.round as number, hp: event.data.hp as { a: number; b: number }, challengeType: (event.data.result as any)?.challengeType || '', winnerId: (event.data.result as any)?.winnerId || null, }) } }) } const fightId = await runMockFight(botA.id, botB.id) unsub?.() // Fetch result const fight = await db.select({ winnerId: schema.fights.winnerId, totalRounds: schema.fights.totalRounds, botAHp: schema.fights.botAHp, botBHp: schema.fights.botBHp, }).from(schema.fights).where(eq(schema.fights.id, fightId)).limit(1) const result = fight[0] const winnerName = result?.winnerId ? bots.find(b => b.id === result.winnerId)?.name || '???' : null const isKo = result ? (result.botAHp <= 0 || result.botBHp <= 0) : false const isPerfect = result?.winnerId ? ( (result.winnerId === botA.id && result.botAHp === 200) || (result.winnerId === botB.id && result.botBHp === 200) ) : false fightCount++ if (onFightComplete) { onFightComplete({ fightId, botAName: botA.name, botBName: botB.name, botAElo: botA.eloRating, botBElo: botB.eloRating, winnerName, winnerId: result?.winnerId || null, totalRounds: result?.totalRounds || 0, isKo, isPerfect, botAHp: result?.botAHp || 0, botBHp: result?.botBHp || 0, }) } else { console.log( `[fight-loop] #${fightCount}: ${botA.name} vs ${botB.name} => ${winnerName || 'DRAW'} (${result?.totalRounds || '?'} rounds)` ) } // Wait before next fight if (fightCount < maxFights) { await sleep(intervalMs + Math.floor(Math.random() * intervalMs * 0.5)) } } catch (err) { if (onError) { onError(err instanceof Error ? err : new Error(String(err))) } else { console.error('[fight-loop] error:', err) } await sleep(5000) } } if (!onFightComplete) { console.log(`[fight-loop] completed ${fightCount} fights`) } } function pickMatchup( bots: { id: string; eloRating: number; name: string; wins: number; losses: number }[], style: string, fightNum: number, ): [typeof bots[0], typeof bots[0]] { const sorted = [...bots].sort((a, b) => b.eloRating - a.eloRating) if (style === 'elo_close' || (style === 'mixed' && fightNum % 3 !== 0)) { const idx = Math.floor(Math.random() * bots.length) const bot = bots[idx] const others = bots.filter(b => b.id !== bot.id) others.sort((a, b) => { const diffA = Math.abs(a.eloRating - bot.eloRating) + Math.random() * 150 const diffB = Math.abs(b.eloRating - bot.eloRating) + Math.random() * 150 return diffA - diffB }) return [bot, others[0]] } if (style === 'mixed' && fightNum % 3 === 0) { const topThird = Math.ceil(sorted.length / 3) const topIdx = Math.floor(Math.random() * topThird) const bottomIdx = sorted.length - 1 - Math.floor(Math.random() * topThird) if (sorted[topIdx].id !== sorted[bottomIdx].id) { return [sorted[topIdx], sorted[bottomIdx]] } } // Random const shuffled = [...bots].sort(() => Math.random() - 0.5) if (shuffled[0].id === shuffled[1].id && shuffled.length > 2) { return [shuffled[0], shuffled[2]] } return [shuffled[0], shuffled[1]] } function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) }