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 { 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() 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 { 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)) } }