Files
botfights/server/src/engine/seasons.ts
T

167 lines
4.4 KiB
TypeScript
Raw Normal View History

import { db, schema } from '../db/index.js'
import { eq, and, gte, lte, or, sql } from 'drizzle-orm'
// Season epoch: March 3, 2026 (a Monday)
const SEASON_EPOCH = new Date('2026-03-03T00:00:00Z').getTime()
const SEASON_DURATION_MS = 14 * 24 * 60 * 60 * 1000 // 2 weeks
const SEASON_NAMES = [
'The Halvening',
'Block 840000',
'Segwit Wars',
'Lightning Strike',
'Satoshi Returns',
'Genesis Block',
'Pizza Day',
'Difficulty Adjustment',
'Mempool Madness',
'Node Runners',
'Proof of Work',
'HODL Season',
'Whitepaper Anniversary',
'Timechain Tournament',
'Cypherpunk Clash',
'Hash Wars',
'Block Size Battle',
'Taproot Takeover',
'Full Node Fury',
'UTXO Showdown',
]
export interface Season {
id: string
number: number
startDate: string
endDate: string
name: string
}
export function getCurrentSeason(): Season {
const now = Date.now()
const elapsed = now - SEASON_EPOCH
const seasonNum = Math.floor(elapsed / SEASON_DURATION_MS) + 1
const startMs = SEASON_EPOCH + (seasonNum - 1) * SEASON_DURATION_MS
const endMs = startMs + SEASON_DURATION_MS
return {
id: `s${seasonNum}`,
number: seasonNum,
startDate: new Date(startMs).toISOString(),
endDate: new Date(endMs).toISOString(),
name: SEASON_NAMES[(seasonNum - 1) % SEASON_NAMES.length],
}
}
export function getSeasonById(seasonId: string): Season | null {
const match = seasonId.match(/^s(\d+)$/)
if (!match) return null
const seasonNum = parseInt(match[1])
if (seasonNum < 1) return null
const startMs = SEASON_EPOCH + (seasonNum - 1) * SEASON_DURATION_MS
const endMs = startMs + SEASON_DURATION_MS
return {
id: `s${seasonNum}`,
number: seasonNum,
startDate: new Date(startMs).toISOString(),
endDate: new Date(endMs).toISOString(),
name: SEASON_NAMES[(seasonNum - 1) % SEASON_NAMES.length],
}
}
interface SeasonLeaderboardEntry {
botId: string
botName: string
archetype: string
tier: number
wins: number
losses: number
eloRating: number
}
export async function getSeasonLeaderboard(seasonId: string): Promise<SeasonLeaderboardEntry[]> {
const season = getSeasonById(seasonId)
if (!season) return []
// Get all finished fights in this season
const fights = await db.select({
botAId: schema.fights.botAId,
botBId: schema.fights.botBId,
winnerId: schema.fights.winnerId,
}).from(schema.fights)
.where(and(
eq(schema.fights.status, 'finished'),
gte(schema.fights.createdAt, season.startDate),
lte(schema.fights.createdAt, season.endDate),
))
// Aggregate wins/losses per bot
const stats = new Map<string, { wins: number; losses: number }>()
for (const f of fights) {
if (!stats.has(f.botAId)) stats.set(f.botAId, { wins: 0, losses: 0 })
if (!stats.has(f.botBId)) stats.set(f.botBId, { wins: 0, losses: 0 })
if (f.winnerId) {
stats.get(f.winnerId)!.wins++
const loserId = f.winnerId === f.botAId ? f.botBId : f.botAId
stats.get(loserId)!.losses++
}
}
if (stats.size === 0) return []
// Fetch bot details
const botIds = [...stats.keys()]
const bots = await db.select({
id: schema.bots.id,
name: schema.bots.name,
archetype: schema.bots.archetype,
tier: schema.bots.tier,
eloRating: schema.bots.eloRating,
botType: schema.bots.botType,
}).from(schema.bots)
const botMap = new Map(bots.map(b => [b.id, b]))
const entries: SeasonLeaderboardEntry[] = []
for (const [botId, s] of stats) {
const bot = botMap.get(botId)
if (!bot || bot.botType === 'classic') continue
entries.push({
botId,
botName: bot.name,
archetype: bot.archetype,
tier: bot.tier,
wins: s.wins,
losses: s.losses,
eloRating: bot.eloRating,
})
}
// Sort by wins desc, then win rate desc
entries.sort((a, b) => {
if (b.wins !== a.wins) return b.wins - a.wins
const rateA = a.wins / (a.wins + a.losses || 1)
const rateB = b.wins / (b.wins + b.losses || 1)
return rateB - rateA
})
return entries
}
export async function resetSeasonElo(): Promise<void> {
const allBots = await db.select({
id: schema.bots.id,
eloRating: schema.bots.eloRating,
}).from(schema.bots)
for (const bot of allBots) {
const newElo = Math.round(bot.eloRating * 0.6 + 1200 * 0.4)
await db.update(schema.bots).set({
eloRating: newElo,
winStreak: 0,
}).where(eq(schema.bots.id, bot.id))
}
}