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
-14
@@ -1,6 +1,6 @@
|
||||
import { db, schema } from '../db/index.js'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { runFightAsync } from './orchestrator.js'
|
||||
import { runFightAsync, isInFight } from './orchestrator.js'
|
||||
import { seedMockBots } from './mock.js'
|
||||
|
||||
interface QueueEntry {
|
||||
@@ -19,6 +19,14 @@ const waitingQueue: QueueEntry[] = []
|
||||
// How long a bot waits before getting matched against a mock bot
|
||||
const QUEUE_TIMEOUT_MS = 3_000
|
||||
|
||||
// Post-fight cooldown tracking
|
||||
const fightCooldowns = new Map<string, number>()
|
||||
const COOLDOWN_MS = 15_000
|
||||
|
||||
export function setCooldown(botId: string) {
|
||||
fightCooldowns.set(botId, Date.now() + COOLDOWN_MS)
|
||||
}
|
||||
|
||||
export function getQueueSize(): number {
|
||||
return waitingQueue.length
|
||||
}
|
||||
@@ -38,22 +46,39 @@ export function getQueueSnapshot(): { botId: string; botName: string; eloRating:
|
||||
* If nobody is waiting, waits up to QUEUE_TIMEOUT_MS then fights a mock bot.
|
||||
*/
|
||||
export async function joinQueue(botId: string): Promise<string> {
|
||||
// Check cooldown
|
||||
const cooldownUntil = fightCooldowns.get(botId)
|
||||
if (cooldownUntil && Date.now() < cooldownUntil) {
|
||||
const waitSec = Math.ceil((cooldownUntil - Date.now()) / 1000)
|
||||
throw new Error(`Cooldown active. Wait ${waitSec}s.`)
|
||||
}
|
||||
|
||||
// Check if already in a fight
|
||||
if (isInFight(botId)) {
|
||||
throw new Error('Bot is already in a fight.')
|
||||
}
|
||||
|
||||
// Load bot
|
||||
const botRows = await db.select().from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
|
||||
if (botRows.length === 0) throw new Error('Bot not found')
|
||||
const bot = botRows[0]
|
||||
|
||||
// Check if bot is active
|
||||
if (!bot.isActive) {
|
||||
throw new Error('Bot is deactivated due to webhook errors. Re-test your webhook to reactivate.')
|
||||
}
|
||||
|
||||
console.log(`[queue] joinQueue botId=${botId} name=${bot.name} webhook=${bot.webhookUrl}`)
|
||||
|
||||
// Don't allow same bot twice in queue
|
||||
const existing = waitingQueue.findIndex(e => e.botId === botId)
|
||||
if (existing !== -1) {
|
||||
// Remove old entry
|
||||
const old = waitingQueue.splice(existing, 1)[0]
|
||||
clearTimeout(old.timeoutHandle)
|
||||
old.reject(new Error('Rejoined queue'))
|
||||
}
|
||||
|
||||
// Check if someone is already waiting — instant match
|
||||
// Check if someone is already waiting -- instant match
|
||||
if (waitingQueue.length > 0) {
|
||||
// Find closest elo match
|
||||
waitingQueue.sort((a, b) => {
|
||||
@@ -66,15 +91,14 @@ export async function joinQueue(botId: string): Promise<string> {
|
||||
clearTimeout(opponent.timeoutHandle)
|
||||
|
||||
// Start the fight
|
||||
const fightId = await startFight(opponent.botId, opponent.webhookUrl, botId, bot.webhookUrl)
|
||||
const fightId = await startFight(opponent.botId, botId)
|
||||
opponent.resolve(fightId)
|
||||
return fightId
|
||||
}
|
||||
|
||||
// Nobody waiting — join the queue and wait
|
||||
// Nobody waiting -- join the queue and wait
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const timeoutHandle = setTimeout(async () => {
|
||||
// Timed out — remove from queue and match against a mock bot
|
||||
const idx = waitingQueue.findIndex(e => e.botId === botId)
|
||||
if (idx !== -1) {
|
||||
waitingQueue.splice(idx, 1)
|
||||
@@ -112,17 +136,12 @@ export function leaveQueue(botId: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
async function startFight(
|
||||
botAId: string, _botAWebhook: string,
|
||||
botBId: string, _botBWebhook: string,
|
||||
): Promise<string> {
|
||||
// runFightAsync handles both real and mock bots — mock bots get generated responses
|
||||
async function startFight(botAId: string, botBId: string): Promise<string> {
|
||||
return runFightAsync(botAId, botBId)
|
||||
}
|
||||
|
||||
async function matchAgainstMock(botId: string, webhookUrl: string): Promise<string> {
|
||||
console.log(`[queue] matchAgainstMock botId=${botId} webhook=${webhookUrl}`)
|
||||
// Find a mock bot to fight
|
||||
const allBots = await db.select({
|
||||
id: schema.bots.id,
|
||||
webhookUrl: schema.bots.webhookUrl,
|
||||
@@ -134,7 +153,6 @@ async function matchAgainstMock(botId: string, webhookUrl: string): Promise<stri
|
||||
if (mockBots.length === 0) {
|
||||
console.log('[queue] No mock bots found, seeding...')
|
||||
await seedMockBots()
|
||||
// Retry after seeding
|
||||
return matchAgainstMock(botId, webhookUrl)
|
||||
}
|
||||
|
||||
@@ -145,6 +163,5 @@ async function matchAgainstMock(botId: string, webhookUrl: string): Promise<stri
|
||||
const opponent = mockBots[0]
|
||||
|
||||
console.log(`[queue] starting fight: ${botId} vs mock ${opponent.id}`)
|
||||
// runFightAsync handles mock bots inline — no need for runMockFight
|
||||
return runFightAsync(botId, opponent.id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user