From 62b957e886cbbc427203f6987bf243bb66da3b16 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 09:15:33 +0000 Subject: [PATCH] feat: seed fight card with scheduled fights in dev mode Co-Authored-By: Claude Opus 4.6 --- server/src/engine/dev-seed.ts | 148 +++++++++++++++++++++++++++++++++- server/src/index.ts | 5 +- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/server/src/engine/dev-seed.ts b/server/src/engine/dev-seed.ts index c068447..e2ba5e7 100644 --- a/server/src/engine/dev-seed.ts +++ b/server/src/engine/dev-seed.ts @@ -1,6 +1,9 @@ import { db, schema } from '../db/index.js' -import { sql } from 'drizzle-orm' +import { sql, eq } from 'drizzle-orm' +import { nanoid } from 'nanoid' import { createTournament, joinTournament, startTournament } from './tournaments.js' +import { randomArena } from './arenas.js' +import { getCurrentSeason } from './seasons.js' import { logger } from '../lib/logger.js' /** Seed a dev tournament with mock bots for testing betting/tournament UI */ @@ -52,3 +55,146 @@ export async function seedDevTournament(): Promise { logger.info('dev-seed', 'dev seeding complete — tournament ready for testing') } + +/** Seed a diverse fight card with scheduled fights for the Fight Card page */ +export async function seedFightCard(): Promise { + // Skip if scheduled fights already exist + const scheduled = db.select({ count: sql`count(*)` }) + .from(schema.fights) + .where(eq(schema.fights.status, 'scheduled')) + .get() + if (scheduled && scheduled.count > 0) { + logger.info('dev-seed', 'scheduled fights already exist — skipping fight card seed') + return + } + + // Get all bots by type + const allBots = db.select({ + id: schema.bots.id, + name: schema.bots.name, + botType: schema.bots.botType, + webhookUrl: schema.bots.webhookUrl, + archetype: schema.bots.archetype, + tier: schema.bots.tier, + eloRating: schema.bots.eloRating, + }) + .from(schema.bots) + .where(eq(schema.bots.isActive, true)) + .all() + + const humans = allBots.filter(b => b.webhookUrl === 'http://human.local/') + const mockBots = allBots.filter(b => b.botType === 'mock') + const classicBots = allBots.filter(b => b.botType === 'classic') + const userBots = allBots.filter(b => b.botType === 'regular' && b.webhookUrl !== 'http://human.local/') + + function pick(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)] } + + const season = getCurrentSeason() + const fights: Array<{ botAId: string; botBId: string; label: string }> = [] + + // 1. AI vs AI (mock bots fighting each other) — main event + if (mockBots.length >= 2) { + const a = pick(mockBots.filter(b => b.tier >= 4)) || pick(mockBots) + let b = pick(mockBots.filter(x => x.id !== a.id && x.tier >= 3)) || pick(mockBots.filter(x => x.id !== a.id)) + if (b) fights.push({ botAId: a.id, botBId: b.id, label: 'AI vs AI (high tier)' }) + } + + // 2. Human vs AI (mock bot) + if (humans.length > 0 && mockBots.length > 0) { + fights.push({ botAId: pick(humans).id, botBId: pick(mockBots).id, label: 'Human vs AI' }) + } + + // 3. AI vs AI (different archetypes, mid tier) + if (mockBots.length >= 4) { + const midBots = mockBots.filter(b => b.tier >= 1 && b.tier <= 3) + if (midBots.length >= 2) { + const a = pick(midBots) + const b = pick(midBots.filter(x => x.id !== a.id && x.archetype !== a.archetype)) + || pick(midBots.filter(x => x.id !== a.id)) + if (b) fights.push({ botAId: a.id, botBId: b.id, label: 'AI vs AI (mid tier)' }) + } + } + + // 4. Human vs Classic Bot + if (humans.length > 0 && classicBots.length > 0) { + fights.push({ botAId: pick(humans).id, botBId: pick(classicBots).id, label: 'Human vs Classic' }) + } + + // 5. Bot vs Bot (user bots or mock if none) + if (userBots.length >= 2) { + const a = pick(userBots) + const b = pick(userBots.filter(x => x.id !== a.id)) + if (b) fights.push({ botAId: a.id, botBId: b.id, label: 'Bot vs Bot (user)' }) + } else if (mockBots.length >= 6) { + const usedIds = new Set(fights.flatMap(f => [f.botAId, f.botBId])) + const available = mockBots.filter(b => !usedIds.has(b.id)) + if (available.length >= 2) { + fights.push({ botAId: available[0].id, botBId: available[1].id, label: 'Bot vs Bot' }) + } + } + + // 6. AI vs Classic Bot + if (mockBots.length > 0 && classicBots.length > 0) { + fights.push({ botAId: pick(mockBots).id, botBId: pick(classicBots).id, label: 'AI vs Classic' }) + } + + // 7. Human vs Human (if we have 2+) + if (humans.length >= 2) { + const a = pick(humans) + const b = pick(humans.filter(x => x.id !== a.id)) + if (b) fights.push({ botAId: a.id, botBId: b.id, label: 'Human vs Human' }) + } + + // 8. Rookie rumble — tier 0 bots + { + const rookies = mockBots.filter(b => b.tier === 0) + if (rookies.length >= 2) { + fights.push({ botAId: rookies[0].id, botBId: rookies[1].id, label: 'Rookie Rumble' }) + } + } + + // 9. Legend clash — tier 5 bots + { + const legends = mockBots.filter(b => b.tier >= 5) + const usedIds = new Set(fights.flatMap(f => [f.botAId, f.botBId])) + const available = legends.filter(b => !usedIds.has(b.id)) + if (available.length >= 2) { + fights.push({ botAId: available[0].id, botBId: available[1].id, label: 'Legend Clash' }) + } + } + + // 10. Wild card — random pairing from anything left + { + const usedIds = new Set(fights.flatMap(f => [f.botAId, f.botBId])) + const remaining = allBots.filter(b => !usedIds.has(b.id)) + if (remaining.length >= 2) { + const a = pick(remaining) + const b = pick(remaining.filter(x => x.id !== a.id)) + if (b) fights.push({ botAId: a.id, botBId: b.id, label: 'Wild Card' }) + } + } + + // Insert all as scheduled fights + const now = new Date() + for (let i = 0; i < fights.length; i++) { + const f = fights[i] + const scheduledTime = new Date(now.getTime() + (i + 1) * 10 * 60 * 1000) // stagger 10 min apart + try { + db.insert(schema.fights).values({ + id: nanoid(12), + botAId: f.botAId, + botBId: f.botBId, + arena: randomArena().id, + status: 'scheduled', + currentSeason: season.id, + scheduledAt: scheduledTime.toISOString(), + createdAt: now.toISOString(), + }).run() + logger.info('dev-seed', `fight card: ${f.label}`) + } catch (err) { + logger.warn('dev-seed', `fight card failed: ${(err as Error).message}`) + } + } + + logger.info('dev-seed', `seeded ${fights.length} scheduled fights for fight card`) +} diff --git a/server/src/index.ts b/server/src/index.ts index 74ea6e0..9dd30cc 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -6,7 +6,7 @@ import { seedMockBots, seedClassicBots } from './engine/mock.js' import { startBackgroundFights } from './engine/background.js' import { getActiveFighterCount } from './engine/orchestrator.js' import { clearAllPending } from './engine/human-responses.js' -import { seedDevTournament } from './engine/dev-seed.js' +import { seedDevTournament, seedFightCard } from './engine/dev-seed.js' // Production env validation — warn but don't crash (wallet features degrade gracefully) if (process.env.NODE_ENV === 'production') { @@ -25,9 +25,10 @@ runMigrations() await seedMockBots() await seedClassicBots() -// Dev mode: seed tournament + betting data for testing +// Dev mode: seed tournament + betting data + fight card for testing if (process.env.NODE_ENV !== 'production') { await seedDevTournament() + await seedFightCard() } const port = Number(process.env.PORT) || 9100