2026-03-06 23:44:40 +00:00
|
|
|
import './db/index.js'
|
2026-03-07 00:14:46 +00:00
|
|
|
import { db, schema } from './db/index.js'
|
2026-03-06 23:44:40 +00:00
|
|
|
import { startFightLoop } from './engine/fight-loop.js'
|
2026-03-07 00:14:46 +00:00
|
|
|
import { createTuiState } from './tui/state.js'
|
|
|
|
|
import { TuiRenderer } from './tui/renderer.js'
|
|
|
|
|
import { desc } from 'drizzle-orm'
|
2026-03-06 23:44:40 +00:00
|
|
|
|
|
|
|
|
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'
|
2026-03-07 00:14:46 +00:00
|
|
|
const noTui = args.includes('--no-tui')
|
2026-03-06 23:44:40 +00:00
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
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))
|
2026-03-06 23:44:40 +00:00
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
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()
|
2026-03-06 23:44:40 +00:00
|
|
|
process.exit(0)
|
|
|
|
|
})
|
2026-03-07 00:14:46 +00:00
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
},
|
|
|
|
|
|
2026-03-08 16:23:47 +00:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
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()
|
|
|
|
|
},
|
2026-03-06 23:44:40 +00:00
|
|
|
})
|
2026-03-07 00:14:46 +00:00
|
|
|
|
|
|
|
|
renderer.showFinalSummary()
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main().catch((err) => {
|
|
|
|
|
console.error('[botfights] fight loop error:', err)
|
|
|
|
|
process.exit(1)
|
|
|
|
|
})
|