Files
botfights/server/src/engine/fight-loop.ts
T

112 lines
3.5 KiB
TypeScript
Raw Normal View History

import { db, schema } from '../db/index.js'
import { runMockFight } from './mock.js'
import { eq } from 'drizzle-orm'
interface FightLoopOptions {
intervalMs?: number
maxFights?: number
matchmakingStyle?: 'random' | 'elo_close' | 'mixed'
}
export async function startFightLoop(options: FightLoopOptions = {}): Promise<void> {
const {
intervalMs = 8000,
maxFights = Infinity,
matchmakingStyle = 'mixed',
} = options
const allBots = await db.select({
id: schema.bots.id,
eloRating: schema.bots.eloRating,
name: schema.bots.name,
}).from(schema.bots)
if (allBots.length < 2) {
console.log('[fight-loop] need at least 2 bots, aborting')
return
}
console.log(`[fight-loop] starting with ${allBots.length} bots, ${matchmakingStyle} matchmaking, ${intervalMs}ms interval`)
let fightCount = 0
while (fightCount < maxFights) {
try {
// Re-fetch bots to get updated elo ratings
const bots = await db.select({
id: schema.bots.id,
eloRating: schema.bots.eloRating,
name: schema.bots.name,
wins: schema.bots.wins,
losses: schema.bots.losses,
}).from(schema.bots)
const [botA, botB] = pickMatchup(bots, matchmakingStyle, fightCount)
const fightId = await runMockFight(botA.id, botB.id)
// Fetch result
const fight = await db.select({
winnerId: schema.fights.winnerId,
totalRounds: schema.fights.totalRounds,
}).from(schema.fights).where(eq(schema.fights.id, fightId)).limit(1)
const result = fight[0]
const winnerName = result?.winnerId
? bots.find(b => b.id === result.winnerId)?.name || '???'
: 'DRAW'
fightCount++
console.log(
`[fight-loop] #${fightCount}: ${botA.name} vs ${botB.name} => ${winnerName} (${result?.totalRounds || '?'} rounds)`
)
// Wait before next fight
if (fightCount < maxFights) {
await sleep(intervalMs + Math.floor(Math.random() * intervalMs * 0.5))
}
} catch (err) {
console.error('[fight-loop] error:', err)
await sleep(5000) // Back off on error
}
}
console.log(`[fight-loop] completed ${fightCount} fights`)
}
function pickMatchup(
bots: { id: string; eloRating: number; name: string; wins: number; losses: number }[],
style: string,
fightNum: number,
): [typeof bots[0], typeof bots[0]] {
const sorted = [...bots].sort((a, b) => b.eloRating - a.eloRating)
if (style === 'elo_close' || (style === 'mixed' && fightNum % 3 !== 0)) {
// Pick a random bot, then find a close-elo opponent
const idx = Math.floor(Math.random() * bots.length)
const bot = bots[idx]
const others = bots.filter(b => b.id !== bot.id)
others.sort((a, b) => {
const diffA = Math.abs(a.eloRating - bot.eloRating) + Math.random() * 150
const diffB = Math.abs(b.eloRating - bot.eloRating) + Math.random() * 150
return diffA - diffB
})
return [bot, others[0]]
}
if (style === 'mixed' && fightNum % 3 === 0) {
// Mismatch: top third vs bottom third for dramatic fights
const topThird = Math.ceil(sorted.length / 3)
const topIdx = Math.floor(Math.random() * topThird)
const bottomIdx = sorted.length - 1 - Math.floor(Math.random() * topThird)
return [sorted[topIdx], sorted[bottomIdx]]
}
// Random
const shuffled = [...bots].sort(() => Math.random() - 0.5)
return [shuffled[0], shuffled[1]]
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}