2026-03-09 08:21:16 +00:00
|
|
|
import { db, schema } from '../db/index.js'
|
2026-03-09 09:15:33 +00:00
|
|
|
import { sql, eq } from 'drizzle-orm'
|
|
|
|
|
import { nanoid } from 'nanoid'
|
2026-03-09 08:21:16 +00:00
|
|
|
import { createTournament, joinTournament, startTournament } from './tournaments.js'
|
2026-03-09 09:15:33 +00:00
|
|
|
import { randomArena } from './arenas.js'
|
|
|
|
|
import { getCurrentSeason } from './seasons.js'
|
2026-03-09 08:21:16 +00:00
|
|
|
import { logger } from '../lib/logger.js'
|
|
|
|
|
|
|
|
|
|
/** Seed a dev tournament with mock bots for testing betting/tournament UI */
|
|
|
|
|
export async function seedDevTournament(): Promise<void> {
|
|
|
|
|
// Skip if a tournament already exists
|
|
|
|
|
const existing = db.select({ count: sql<number>`count(*)` })
|
|
|
|
|
.from(schema.tournaments)
|
|
|
|
|
.get()
|
|
|
|
|
if (existing && existing.count > 0) {
|
|
|
|
|
logger.info('dev-seed', 'tournaments already exist — skipping seed')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Grab 8 mock bots for the tournament
|
|
|
|
|
const bots = db.select({ id: schema.bots.id, name: schema.bots.name })
|
|
|
|
|
.from(schema.bots)
|
|
|
|
|
.limit(8)
|
|
|
|
|
.all()
|
|
|
|
|
|
|
|
|
|
if (bots.length < 4) {
|
|
|
|
|
logger.warn('dev-seed', `only ${bots.length} bots — need at least 4 for tournament seed`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create tournament
|
|
|
|
|
const tournamentId = createTournament(
|
|
|
|
|
'SATOSHI SHOWDOWN #1',
|
|
|
|
|
'single_elim',
|
|
|
|
|
8,
|
|
|
|
|
100,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Join bots
|
|
|
|
|
for (const bot of bots.slice(0, 8)) {
|
|
|
|
|
try {
|
|
|
|
|
joinTournament(tournamentId, bot.id)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.warn('dev-seed', `failed to join bot ${bot.name}: ${(err as Error).message}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start the tournament (seeds by elo, generates bracket)
|
|
|
|
|
try {
|
|
|
|
|
startTournament(tournamentId)
|
|
|
|
|
logger.info('dev-seed', `seeded tournament "${tournamentId}" with ${Math.min(bots.length, 8)} bots`)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.warn('dev-seed', `failed to start tournament: ${(err as Error).message}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info('dev-seed', 'dev seeding complete — tournament ready for testing')
|
|
|
|
|
}
|
2026-03-09 09:15:33 +00:00
|
|
|
|
|
|
|
|
/** Seed a diverse fight card with scheduled fights for the Fight Card page */
|
|
|
|
|
export async function seedFightCard(): Promise<void> {
|
|
|
|
|
// Skip if scheduled fights already exist
|
|
|
|
|
const scheduled = db.select({ count: sql<number>`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<T>(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`)
|
|
|
|
|
}
|