Files
botfights/server/src/fight-loop-cli.ts
T

169 lines
5.1 KiB
TypeScript
Raw Normal View History

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)
})