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
+31
-23
@@ -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`)
|
||||
|
||||
Reference in New Issue
Block a user