diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index 96b4e8c..9bb9c8f 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -44,6 +44,7 @@ export const fights = sqliteTable('fights', { mode: text('mode', { enum: ['free', 'ranked'] }).notNull().default('free'), potSats: integer('pot_sats').notNull().default(0), payoutStatus: text('payout_status', { enum: ['pending', 'paid', 'failed'] }), + currentSeason: text('current_season'), createdAt: text('created_at').notNull(), }) diff --git a/server/src/db/startup.ts b/server/src/db/startup.ts index 3cc7d92..00f09a3 100644 --- a/server/src/db/startup.ts +++ b/server/src/db/startup.ts @@ -104,6 +104,8 @@ export function runMigrations() { "ALTER TABLE bots ADD COLUMN has_wallet INTEGER NOT NULL DEFAULT 0", // Zap tracking "ALTER TABLE bots ADD COLUMN zaps_received INTEGER NOT NULL DEFAULT 0", + // Seasons + "ALTER TABLE fights ADD COLUMN current_season TEXT", // Classic bots "ALTER TABLE bots ADD COLUMN bot_type TEXT NOT NULL DEFAULT 'regular'", ] diff --git a/server/src/engine/mock.ts b/server/src/engine/mock.ts index f8c53f8..fb675e8 100644 --- a/server/src/engine/mock.ts +++ b/server/src/engine/mock.ts @@ -6,6 +6,7 @@ import { pickChallenge, type Challenge } from './challenges.js' import { generateMockRetroResponse } from './retro-moves.js' import { scoreRound, calculateElo, calculateTier } from './scoring.js' import { eq, sql } from 'drizzle-orm' +import { getCurrentSeason } from './seasons.js' const MOCK_BOTS = [ // Tier 6 - Legends (1900+ Elo, 40+ wins) @@ -426,6 +427,7 @@ export async function runMockFight(botAId: string, botBId: 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)) + } +}