import './db/index.js' import { db, schema } from './db/index.js' import { startFightLoop } from './engine/fight-loop.js' import { createTuiState } from './tui/state.js' import { TuiRenderer } from './tui/renderer.js' import { desc } from 'drizzle-orm' const args = process.argv.slice(2) const maxFights = parseInt(args.find(a => a.startsWith('--max='))?.split('=')[1] || '0') || Infinity const intervalMs = parseInt(args.find(a => a.startsWith('--interval='))?.split('=')[1] || '0') || 8000 const style = (args.find(a => a.startsWith('--style='))?.split('=')[1] || 'mixed') as 'random' | 'elo_close' | 'mixed' const noTui = args.includes('--no-tui') async function main() { // Snapshot starting elos const allBots = await db.select({ name: schema.bots.name, eloRating: schema.bots.eloRating, wins: schema.bots.wins, losses: schema.bots.losses, tier: schema.bots.tier, }).from(schema.bots).orderBy(desc(schema.bots.eloRating)) if (noTui) { console.log('[botfights] fight loop CLI (no TUI)') console.log(` max fights: ${maxFights === Infinity ? 'unlimited' : maxFights}`) console.log(` interval: ${intervalMs}ms`) console.log(` style: ${style}`) console.log('') await startFightLoop({ maxFights, intervalMs, matchmakingStyle: style }) process.exit(0) } // TUI mode const state = createTuiState(maxFights, style) const renderer = new TuiRenderer(state) // Snapshot starting elos for (const bot of allBots) { state.eloSnapshots.set(bot.name, bot.eloRating) } // Set initial leaderboard state.leaderboard = allBots.slice(0, 15).map(b => ({ name: b.name, elo: b.eloRating, wins: b.wins, losses: b.losses, tier: b.tier, })) renderer.render() // Handle SIGINT gracefully process.on('SIGINT', () => { renderer.showFinalSummary() process.exit(0) }) // Handle terminal resize process.stdout.on('resize', () => renderer.render()) await startFightLoop({ maxFights, intervalMs, matchmakingStyle: style, onFightStart: (botAName, botAElo, botBName, botBElo) => { state.currentFight = { botA: { name: botAName, elo: botAElo, hp: 200 }, botB: { name: botBName, elo: botBElo, hp: 200 }, round: 0, maxRounds: 10, challengeType: '', challengeLabel: '', events: [], } renderer.render() }, onRoundComplete: (data) => { if (state.currentFight) { state.currentFight.round = data.round state.currentFight.botA.hp = data.hp.a state.currentFight.botB.hp = data.hp.b if (data.challengeType) state.currentFight.challengeType = data.challengeType renderer.render() } }, onFightComplete: async (result) => { state.completed++ // Track fight counts state.fightCounts.set(result.botAName, (state.fightCounts.get(result.botAName) || 0) + 1) state.fightCounts.set(result.botBName, (state.fightCounts.get(result.botBName) || 0) + 1) // Track KO, perfect, draw if (result.isKo) state.kos++ if (result.isPerfect) state.perfects++ if (!result.winnerId) state.draws++ // Determine method let method = 'Decision' if (!result.winnerId) method = 'DRAW' else if (result.isPerfect) method = `PERFECT R${result.totalRounds}` else if (result.isKo) method = `KO R${result.totalRounds}` // Add to recent fights state.recentFights.push({ num: state.completed, botA: result.botAName, botB: result.botBName, winner: result.winnerName, method, }) if (state.recentFights.length > 20) state.recentFights.shift() // Track biggest upset if (result.winnerId && result.winnerName) { const winnerElo = result.winnerId === result.botAName ? result.botAElo : result.botBElo const loserElo = result.winnerId === result.botAName ? result.botBElo : result.botAElo const loserName = result.winnerName === result.botAName ? result.botBName : result.botAName const eloDiff = Math.round(loserElo - winnerElo) if (eloDiff > 0 && (!state.biggestUpset || eloDiff > state.biggestUpset.eloDiff)) { state.biggestUpset = { winner: result.winnerName, loser: loserName, eloDiff } } } // Refresh leaderboard from DB try { const bots = await db.select({ name: schema.bots.name, eloRating: schema.bots.eloRating, wins: schema.bots.wins, losses: schema.bots.losses, tier: schema.bots.tier, }).from(schema.bots).orderBy(desc(schema.bots.eloRating)).limit(15) state.leaderboard = bots.map(b => ({ name: b.name, elo: b.eloRating, wins: b.wins, losses: b.losses, tier: b.tier, })) } catch { /* non-critical */ } state.currentFight = null renderer.render() }, onError: (err) => { state.errors++ state.currentFight = null renderer.render() }, }) renderer.showFinalSummary() process.exit(0) } main().catch((err) => { console.error('[botfights] fight loop error:', err) process.exit(1) })