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:
Dorian
2026-03-07 00:14:46 +00:00
co-authored by Claude Opus 4.6
parent 2c0323d5fb
commit 4d8b18a58a
24 changed files with 2973 additions and 721 deletions
+31 -23
View File
@@ -1,6 +1,6 @@
import { nanoid } from 'nanoid'
import { createHash, randomBytes } from 'crypto'
import { db, schema } from '../db/index.js'
import { db, schema, sqlite } from '../db/index.js'
import { randomArena } from './arenas.js'
import { pickChallenge, type Challenge } from './challenges.js'
import { scoreRound, calculateElo, calculateTier } from './scoring.js'
@@ -265,6 +265,7 @@ export function mockResponse(
return { answer, trashTalk, timeMs, timedOut, error }
}
export async function seedMockBots(): Promise<void> {
for (const bot of MOCK_BOTS) {
const existing = await db.select({ id: schema.bots.id })
@@ -292,6 +293,8 @@ export async function seedMockBots(): Promise<void> {
}
export async function runMockFight(botAId: string, botBId: string): Promise<string> {
if (botAId === botBId) throw new Error('A bot cannot fight itself')
const [botARows, botBRows] = await Promise.all([
db.select().from(schema.bots).where(eq(schema.bots.id, botAId)).limit(1),
db.select().from(schema.bots).where(eq(schema.bots.id, botBId)).limit(1),
@@ -329,7 +332,7 @@ export async function runMockFight(botAId: string, botBId: string): Promise<stri
const eloForMock = (name: string) =>
MOCK_BOTS.find(b => b.name === name)?.elo || 1200
const totalRounds = 7 + Math.floor(Math.random() * 4) // 7-10 rounds
const totalRounds = 7 + Math.floor(Math.random() * 4)
const maxRounds = Math.min(totalRounds, 10)
for (let round = 1; round <= maxRounds; round++) {
@@ -389,37 +392,42 @@ export async function runMockFight(botAId: string, botBId: string): Promise<stri
winnerId = hpA > hpB ? botA.id : hpB > hpA ? botB.id : null
}
await db.update(schema.fights).set({
status: 'finished',
winnerId,
endedAt: new Date().toISOString(),
}).where(eq(schema.fights.id, fightId))
// Finalize atomically
const finalize = sqlite.transaction(() => {
db.update(schema.fights).set({
status: 'finished',
winnerId,
endedAt: new Date().toISOString(),
}).where(eq(schema.fights.id, fightId))
// Update stats
if (winnerId) {
const loserId = winnerId === botA.id ? botB.id : botA.id
const winner = winnerId === botA.id ? botA : botB
const loser = winnerId === botA.id ? botB : botA
if (winnerId) {
const loserId = winnerId === botA.id ? botB.id : botA.id
const winner = winnerId === botA.id ? botA : botB
const loser = winnerId === botA.id ? botB : botA
const { newWinnerElo, newLoserElo } = calculateElo(winner.eloRating, loser.eloRating)
const newWinStreak = winner.winStreak + 1
const { newWinnerElo, newLoserElo } = calculateElo(winner.eloRating, loser.eloRating)
const newWinStreak = winner.winStreak + 1
await Promise.all([
db.update(schema.bots).set({
wins: sql`${schema.bots.wins} + 1`,
eloRating: newWinnerElo,
winStreak: newWinStreak,
bestStreak: sql`MAX(${schema.bots.bestStreak}, ${newWinStreak})`,
tier: calculateTier(newWinnerElo, winner.wins + 1),
}).where(eq(schema.bots.id, winnerId)),
lastFightAt: new Date().toISOString(),
}).where(eq(schema.bots.id, winnerId))
db.update(schema.bots).set({
losses: sql`${schema.bots.losses} + 1`,
eloRating: newLoserElo,
winStreak: 0,
tier: calculateTier(newLoserElo, loser.wins),
}).where(eq(schema.bots.id, loserId)),
])
}
lastFightAt: new Date().toISOString(),
}).where(eq(schema.bots.id, loserId))
}
})
finalize()
return fightId
}
@@ -442,26 +450,26 @@ export async function seedMockFights(count: number = 12): Promise<void> {
return
}
// Sort by elo for mismatch selection
const sorted = [...allBots].sort((a, b) => b.eloRating - a.eloRating)
for (let i = 0; i < count; i++) {
let botAId: string, botBId: string
if (i % 3 === 0 && sorted.length >= 4) {
// Every 3rd fight: mismatch (top vs bottom)
const topIdx = Math.floor(Math.random() * Math.ceil(sorted.length / 3))
const botIdx = sorted.length - 1 - Math.floor(Math.random() * Math.ceil(sorted.length / 3))
botAId = sorted[topIdx].id
botBId = sorted[botIdx].id
} else {
// Random matchup
const shuffled = [...allBots].sort(() => Math.random() - 0.5)
botAId = shuffled[0].id
botBId = shuffled[1].id
}
await runMockFight(botAId, botBId)
// Skip self-fights
if (botAId !== botBId) {
await runMockFight(botAId, botBId)
}
}
console.log(`[botfights] seeded ${count} mock fights`)