feat: v2 — queue matchmaking, procedural audio, sprite archetypes, auth

- Add queue-based matchmaking with Elo-proximity and 10s timeout
- Procedural sound engine (SFX, voice announcer, 4-track music)
- Sprite system refactored into 6 archetypes (standard, lobster, sheep, cyborg, blob, tank)
- 42+ fight choreographies with themed/generic/wild card selection
- 4 KO finish styles, super-speed mode, hyperdetail close-ups
- Auth routes, JoinBout page, bot profile with stats
- 7-tier ranking system (Baby through Legend)
- Arena and challenge system expansions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-06 22:13:19 +00:00
co-authored by Claude Opus 4.6
parent 335c148866
commit 47d20fbe66
82 changed files with 14011 additions and 741 deletions
+143
View File
@@ -0,0 +1,143 @@
import { db, schema } from '../db/index.js'
import { eq } from 'drizzle-orm'
import { runFightAsync } from './orchestrator.js'
interface QueueEntry {
botId: string
botName: string
webhookUrl: string
eloRating: number
joinedAt: number
resolve: (fightId: string) => void
reject: (error: Error) => void
timeoutHandle: ReturnType<typeof setTimeout>
}
const waitingQueue: QueueEntry[] = []
// How long a bot waits before getting matched against a mock bot
const QUEUE_TIMEOUT_MS = 3_000
export function getQueueSize(): number {
return waitingQueue.length
}
export function getQueueSnapshot(): { botId: string; botName: string; eloRating: number; waitingSince: number }[] {
return waitingQueue.map(e => ({
botId: e.botId,
botName: e.botName,
eloRating: e.eloRating,
waitingSince: e.joinedAt,
}))
}
/**
* Join the fight queue. Returns a fightId when matched.
* If another bot is waiting, matches instantly.
* If nobody is waiting, waits up to QUEUE_TIMEOUT_MS then fights a mock bot.
*/
export async function joinQueue(botId: string): Promise<string> {
// 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]
// 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
if (waitingQueue.length > 0) {
// Find closest elo match
waitingQueue.sort((a, b) => {
const diffA = Math.abs(a.eloRating - bot.eloRating)
const diffB = Math.abs(b.eloRating - bot.eloRating)
return diffA - diffB
})
const opponent = waitingQueue.shift()!
clearTimeout(opponent.timeoutHandle)
// Start the fight
const fightId = await startFight(opponent.botId, opponent.webhookUrl, botId, bot.webhookUrl)
opponent.resolve(fightId)
return fightId
}
// 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)
try {
const fightId = await matchAgainstMock(botId, bot.webhookUrl)
resolve(fightId)
} catch (err) {
reject(err)
}
}
}, QUEUE_TIMEOUT_MS)
waitingQueue.push({
botId,
botName: bot.name,
webhookUrl: bot.webhookUrl,
eloRating: bot.eloRating,
joinedAt: Date.now(),
resolve,
reject,
timeoutHandle,
})
})
}
/**
* Leave the queue without fighting.
*/
export function leaveQueue(botId: string): boolean {
const idx = waitingQueue.findIndex(e => e.botId === botId)
if (idx === -1) return false
const entry = waitingQueue.splice(idx, 1)[0]
clearTimeout(entry.timeoutHandle)
entry.reject(new Error('Left queue'))
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
return runFightAsync(botAId, botBId)
}
async function matchAgainstMock(botId: string, webhookUrl: string): Promise<string> {
// Find a mock bot to fight
const allBots = await db.select({
id: schema.bots.id,
webhookUrl: schema.bots.webhookUrl,
eloRating: schema.bots.eloRating,
}).from(schema.bots)
const mockBots = allBots.filter(b => b.id !== botId && b.webhookUrl.startsWith('http://mock.local'))
if (mockBots.length === 0) {
throw new Error('No opponents available')
}
// Pick closest elo mock bot
const bot = allBots.find(b => b.id === botId)
const botElo = bot?.eloRating || 1200
mockBots.sort((a, b) => Math.abs(a.eloRating - botElo) - Math.abs(b.eloRating - botElo))
const opponent = mockBots[0]
// runFightAsync handles mock bots inline — no need for runMockFight
return runFightAsync(botId, opponent.id)
}