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:
co-authored by
Claude Opus 4.6
parent
335c148866
commit
47d20fbe66
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user