feat: v4 — TUI fight loop, rate limiting, webhook tooling, expanded choreographies
- Fight loop CLI with TUI renderer (ink-style terminal UI) - Rate limiting middleware for API routes - Queue cooldowns wired into orchestrator after fights - Webhook test utility for bot debugging - API docs route - Expanded FightScene choreographies and weapon props - Fix Drizzle transaction execution in orchestrator - Schema additions, scoring/challenge/mock expansions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2c0323d5fb
commit
4d8b18a58a
+146
-11
@@ -1,23 +1,158 @@
|
||||
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')
|
||||
|
||||
console.log('[botfights] fight loop CLI')
|
||||
console.log(` max fights: ${maxFights === Infinity ? 'unlimited' : maxFights}`)
|
||||
console.log(` interval: ${intervalMs}ms`)
|
||||
console.log(` style: ${style}`)
|
||||
console.log('')
|
||||
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))
|
||||
|
||||
startFightLoop({ maxFights, intervalMs, matchmakingStyle: style })
|
||||
.then(() => {
|
||||
console.log('[botfights] fight loop finished')
|
||||
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)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[botfights] fight loop error:', err)
|
||||
process.exit(1)
|
||||
|
||||
// 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()
|
||||
},
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user