feat: v3 — fight loop, expanded challenges, FightPage ownership guard
- Add fight-loop CLI for automated mock fights with elo-based matchmaking - Expand challenge types, scoring narrations, arenas, and mock bot pool - FightPage "Fight again" buttons now only show for your own bot - FightViewer async scene init, live fight polling with round counter - Extract mock answers into separate answers module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
47d20fbe66
commit
2c0323d5fb
@@ -0,0 +1,111 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user