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

172 lines
5.1 KiB
TypeScript
Raw Normal View History

import { db, schema } from '../db/index.js'
import { runMockFight } from './mock.js'
import { eq } from 'drizzle-orm'
export interface FightResult {
fightId: string
botAName: string
botBName: string
botAElo: number
botBElo: number
winnerName: string | null
winnerId: string | null
totalRounds: number
isKo: boolean
isPerfect: boolean
botAHp: number
botBHp: number
}
export interface FightLoopOptions {
intervalMs?: number
maxFights?: number
matchmakingStyle?: 'random' | 'elo_close' | 'mixed'
onFightStart?: (botAName: string, botAElo: number, botBName: string, botBElo: number) => void
onFightComplete?: (result: FightResult) => void
onError?: (err: Error) => void
}
export async function startFightLoop(options: FightLoopOptions = {}): Promise<void> {
const {
intervalMs = 8000,
maxFights = Infinity,
matchmakingStyle = 'mixed',
onFightStart,
onFightComplete,
onError,
} = 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)
if (onFightStart) {
onFightStart(botA.name, botA.eloRating, botB.name, botB.eloRating)
}
const fightId = await runMockFight(botA.id, botB.id)
// Fetch result
const fight = await db.select({
winnerId: schema.fights.winnerId,
totalRounds: schema.fights.totalRounds,
botAHp: schema.fights.botAHp,
botBHp: schema.fights.botBHp,
}).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 || '???'
: null
const isKo = result ? (result.botAHp <= 0 || result.botBHp <= 0) : false
const isPerfect = result?.winnerId ? (
(result.winnerId === botA.id && result.botAHp === 200) ||
(result.winnerId === botB.id && result.botBHp === 200)
) : false
fightCount++
if (onFightComplete) {
onFightComplete({
fightId,
botAName: botA.name,
botBName: botB.name,
botAElo: botA.eloRating,
botBElo: botB.eloRating,
winnerName,
winnerId: result?.winnerId || null,
totalRounds: result?.totalRounds || 0,
isKo,
isPerfect,
botAHp: result?.botAHp || 0,
botBHp: result?.botBHp || 0,
})
} else {
console.log(
`[fight-loop] #${fightCount}: ${botA.name} vs ${botB.name} => ${winnerName || 'DRAW'} (${result?.totalRounds || '?'} rounds)`
)
}
// Wait before next fight
if (fightCount < maxFights) {
await sleep(intervalMs + Math.floor(Math.random() * intervalMs * 0.5))
}
} catch (err) {
if (onError) {
onError(err instanceof Error ? err : new Error(String(err)))
} else {
console.error('[fight-loop] error:', err)
}
await sleep(5000)
}
}
if (!onFightComplete) {
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)) {
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) {
const topThird = Math.ceil(sorted.length / 3)
const topIdx = Math.floor(Math.random() * topThird)
const bottomIdx = sorted.length - 1 - Math.floor(Math.random() * topThird)
if (sorted[topIdx].id !== sorted[bottomIdx].id) {
return [sorted[topIdx], sorted[bottomIdx]]
}
}
// Random
const shuffled = [...bots].sort(() => Math.random() - 0.5)
if (shuffled[0].id === shuffled[1].id && shuffled.length > 2) {
return [shuffled[0], shuffled[2]]
}
return [shuffled[0], shuffled[1]]
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}