import { Driver } from '@core/racing/domain/entities/Driver'; import { League } from '@core/racing/domain/entities/League'; import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership'; import { Race } from '@core/racing/domain/entities/Race'; import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration'; import { Result as RaceResult } from '@core/racing/domain/entities/result/Result'; import { Standing } from '@core/racing/domain/entities/Standing'; import { Team } from '@core/racing/domain/entities/Team'; import type { TeamMembership } from '@core/racing/domain/types/TeamMembership'; import type { FeedItem } from '@core/social/domain/types/FeedItem'; import { getPointsSystems } from '../PointsSystems'; export type Friendship = { driverId: string; friendId: string; }; export type InMemoryRacingSeed = { drivers: Driver[]; leagues: League[]; races: Race[]; results: RaceResult[]; standings: Standing[]; leagueMemberships: LeagueMembership[]; raceRegistrations: RaceRegistration[]; teams: Team[]; teamMemberships: TeamMembership[]; friendships: Friendship[]; feedEvents: FeedItem[]; }; export type InMemoryRacingSeedOptions = { driverCount?: number; baseDate?: Date; }; export const inMemoryRacingSeedDefaults: Readonly< Required > = { driverCount: 32, baseDate: new Date('2025-01-15T12:00:00.000Z'), }; class InMemoryRacingSeedFactory { private readonly driverCount: number; private readonly baseDate: Date; constructor(options: InMemoryRacingSeedOptions) { this.driverCount = options.driverCount ?? inMemoryRacingSeedDefaults.driverCount; this.baseDate = options.baseDate ?? inMemoryRacingSeedDefaults.baseDate; } create(): InMemoryRacingSeed { const drivers = this.createDrivers(); const leagues = this.createLeagues(); const races = this.createRaces(leagues); const results = this.createResults(drivers, races); const standings = this.createStandings(leagues, races, results); const leagueMemberships = this.createLeagueMemberships(drivers, leagues); const raceRegistrations = this.createRaceRegistrations(races); const teams = this.createTeams(); const teamMemberships = this.createTeamMemberships(drivers, teams); const friendships = this.createFriendships(drivers); const feedEvents = this.createFeedEvents(drivers, friendships, races, leagues); return { drivers, leagues, races, results, standings, leagueMemberships, raceRegistrations, teams, teamMemberships, friendships, feedEvents, }; } private addDays(date: Date, days: number): Date { return new Date(date.getTime() + days * 24 * 60 * 60 * 1000); } private addMinutes(date: Date, minutes: number): Date { return new Date(date.getTime() + minutes * 60 * 1000); } private createDrivers(): Driver[] { const countries = ['DE', 'NL', 'FR', 'GB', 'US', 'CA', 'SE', 'NO', 'IT', 'ES'] as const; return Array.from({ length: this.driverCount }, (_, idx) => { const i = idx + 1; return Driver.create({ id: `driver-${i}`, iracingId: String(100000 + i), name: `Driver ${i}`, country: countries[idx % countries.length]!, bio: `Demo driver #${i} seeded for in-memory mode.`, joinedAt: this.addDays(this.baseDate, -90 + i), }); }); } private createLeagues(): League[] { const createdAtBase = this.baseDate; return [ League.create({ id: 'league-1', name: 'GridPilot Sprint Series', description: 'Weekly sprint races with stable grids.', ownerId: 'driver-1', settings: { pointsSystem: 'f1-2024', maxDrivers: 24, sessionDuration: 60, qualifyingFormat: 'open', }, createdAt: this.addDays(createdAtBase, -200), socialLinks: { discordUrl: 'https://discord.gg/gridpilot-demo', youtubeUrl: 'https://youtube.com/@gridpilot-demo', websiteUrl: 'https://gridpilot-demo.example.com', }, }), League.create({ id: 'league-2', name: 'GridPilot Endurance Cup', description: 'Longer races with strategy and consistency.', ownerId: 'driver-2', settings: { pointsSystem: 'indycar', maxDrivers: 32, sessionDuration: 120, qualifyingFormat: 'open', }, createdAt: this.addDays(createdAtBase, -180), socialLinks: { discordUrl: 'https://discord.gg/gridpilot-endurance' }, }), League.create({ id: 'league-3', name: 'GridPilot Club Ladder', description: 'Casual ladder with fast onboarding.', ownerId: 'driver-3', settings: { pointsSystem: 'f1-2024', maxDrivers: 48, sessionDuration: 45, qualifyingFormat: 'single-lap', }, createdAt: this.addDays(createdAtBase, -160), }), League.create({ id: 'league-4', name: 'Nordic Night Series', description: 'Evening races with tight fields.', ownerId: 'driver-4', settings: { pointsSystem: 'f1-2024', maxDrivers: 32, sessionDuration: 60, qualifyingFormat: 'open', }, createdAt: this.addDays(createdAtBase, -150), }), League.create({ id: 'league-5', name: 'Demo League (Admin)', description: 'Primary demo league owned by driver-1.', ownerId: 'driver-1', settings: { pointsSystem: 'f1-2024', maxDrivers: 24, sessionDuration: 60, qualifyingFormat: 'open', }, createdAt: this.addDays(createdAtBase, -140), }), League.create({ id: 'league-6', name: 'Sim Racing Alliance', description: 'Mixed-format season with community events.', ownerId: 'driver-5', settings: { pointsSystem: 'indycar', maxDrivers: 40, sessionDuration: 90, qualifyingFormat: 'open', }, createdAt: this.addDays(createdAtBase, -130), }), ]; } private createRaces(leagues: League[]): Race[] { const tracks = [ 'Monza GP', 'Spa-Francorchamps', 'Suzuka', 'Mount Panorama', 'Silverstone GP', 'Interlagos', 'Imola', 'Laguna Seca', ]; const cars = ['GT3 – Porsche 911', 'GT3 – BMW M4', 'LMP3 Prototype', 'GT4 – Alpine', 'Touring – Civic']; const leagueIds = leagues.map((l) => l.id.toString()); const demoLeagueId = 'league-5'; const races: Race[] = []; for (let i = 1; i <= 25; i++) { const leagueId = leagueIds[(i - 1) % leagueIds.length] ?? demoLeagueId; const scheduledAt = this.addDays(this.baseDate, i <= 10 ? -35 + i : 1 + (i - 10) * 2); const base = { id: `race-${i}`, leagueId, scheduledAt, track: tracks[(i - 1) % tracks.length]!, car: cars[(i - 1) % cars.length]!, }; if (i === 1) { races.push( Race.create({ ...base, leagueId: demoLeagueId, scheduledAt: this.addMinutes(this.baseDate, -30), status: 'running', strengthOfField: 1530, registeredCount: 16, }), ); continue; } if (scheduledAt < this.baseDate) { races.push( Race.create({ ...base, status: 'completed', }), ); continue; } races.push( Race.create({ ...base, status: 'scheduled', }), ); } return races; } private createResults(drivers: Driver[], races: Race[]): RaceResult[] { const results: RaceResult[] = []; const completed = races.filter((r) => r.status === 'completed'); for (const race of completed) { const participants = drivers.slice(0, Math.min(16, drivers.length)); for (let idx = 0; idx < participants.length; idx++) { const driver = participants[idx]!; const position = idx + 1; const startPosition = ((idx + 3) % participants.length) + 1; results.push( RaceResult.create({ id: `${race.id}:${driver.id}`, raceId: race.id, driverId: driver.id, position, startPosition, fastestLap: 88_000 + idx * 120, incidents: idx % 4 === 0 ? 2 : 0, }), ); } } return results; } private resolvePointsSystem( league: League, pointsSystems: Record>, ): Record { const settings = league.settings; return settings.customPoints ?? pointsSystems[settings.pointsSystem] ?? pointsSystems['f1-2024'] ?? {}; } private createStandings(leagues: League[], races: Race[], results: RaceResult[]): Standing[] { const pointsSystems = getPointsSystems(); const racesByLeague = new Map>(); for (const race of races) { if (race.status !== 'completed') continue; const set = racesByLeague.get(race.leagueId) ?? new Set(); set.add(race.id); racesByLeague.set(race.leagueId, set); } const standings: Standing[] = []; for (const league of leagues) { const leagueId = league.id.toString(); const completedRaceIds = racesByLeague.get(leagueId) ?? new Set(); if (completedRaceIds.size === 0) continue; const pointsTable = this.resolvePointsSystem(league, pointsSystems); const byDriver = new Map(); for (const result of results) { if (!completedRaceIds.has(result.raceId.toString())) continue; const driverId = result.driverId.toString(); const previousStanding = byDriver.get(driverId) ?? Standing.create({ leagueId, driverId, position: 1 }); const nextStanding = previousStanding.addRaceResult(result.position.toNumber(), pointsTable); byDriver.set(driverId, nextStanding); } const sorted = Array.from(byDriver.values()).sort((a, b) => { if (b.points.toNumber() !== a.points.toNumber()) return b.points.toNumber() - a.points.toNumber(); if (b.wins !== a.wins) return b.wins - a.wins; return b.racesCompleted - a.racesCompleted; }); sorted.forEach((standing, index) => standings.push(standing.updatePosition(index + 1))); } return standings; } private createLeagueMemberships(drivers: Driver[], leagues: League[]): LeagueMembership[] { const memberships: LeagueMembership[] = []; for (const driver of drivers) { const driverId = driver.id; memberships.push( LeagueMembership.create({ leagueId: 'league-5', driverId, role: driverId === 'driver-1' ? 'owner' : 'member', status: 'active', joinedAt: this.addDays(this.baseDate, -60), }), ); const driverNumber = Number(driverId.split('-')[1]); const extraLeague = leagues[(driverNumber % (leagues.length - 1)) + 1]; if (extraLeague) { memberships.push( LeagueMembership.create({ leagueId: extraLeague.id.toString(), driverId, role: 'member', status: 'active', joinedAt: this.addDays(this.baseDate, -40), }), ); } } return memberships; } private createRaceRegistrations(races: Race[]): RaceRegistration[] { const registrations: RaceRegistration[] = []; const upcomingDemoLeague = races.filter((r) => r.status === 'scheduled' && r.leagueId === 'league-5').slice(0, 3); for (const race of upcomingDemoLeague) { registrations.push( RaceRegistration.create({ raceId: race.id, driverId: 'driver-1', }), ); } return registrations; } private createTeams(): Team[] { return [ Team.create({ id: 'team-1', name: 'Apex Racing', tag: 'APEX', description: 'Demo team focused on clean racing.', ownerId: 'driver-1', leagues: ['league-5'], createdAt: this.addDays(this.baseDate, -100), }), Team.create({ id: 'team-2', name: 'Night Owls', tag: 'NITE', description: 'Late-night grinders and endurance lovers.', ownerId: 'driver-2', leagues: ['league-4'], createdAt: this.addDays(this.baseDate, -90), }), Team.create({ id: 'team-3', name: 'Club Legends', tag: 'CLUB', description: 'A casual team for ladder climbing.', ownerId: 'driver-3', leagues: ['league-3'], createdAt: this.addDays(this.baseDate, -80), }), ]; } private createTeamMemberships(drivers: Driver[], teams: Team[]): TeamMembership[] { const memberships: TeamMembership[] = []; const team1 = teams.find((t) => t.id === 'team-1'); const team2 = teams.find((t) => t.id === 'team-2'); const team3 = teams.find((t) => t.id === 'team-3'); if (team1) { const members = drivers.slice(0, 6); members.forEach((d, idx) => { memberships.push({ teamId: team1.id, driverId: d.id, role: d.id === team1.ownerId.toString() ? 'owner' : idx === 1 ? 'manager' : 'driver', status: 'active', joinedAt: this.addDays(this.baseDate, -50), }); }); } if (team2) { const members = drivers.slice(6, 12); members.forEach((d) => { memberships.push({ teamId: team2.id, driverId: d.id, role: d.id === team2.ownerId.toString() ? 'owner' : 'driver', status: 'active', joinedAt: this.addDays(this.baseDate, -45), }); }); } if (team3) { const members = drivers.slice(12, 18); members.forEach((d) => { memberships.push({ teamId: team3.id, driverId: d.id, role: d.id === team3.ownerId.toString() ? 'owner' : 'driver', status: 'active', joinedAt: this.addDays(this.baseDate, -40), }); }); } return memberships; } private createFriendships(drivers: Driver[]): Friendship[] { const friendships: Friendship[] = []; for (let i = 0; i < drivers.length; i++) { const driver = drivers[i]!; for (let offset = 1; offset <= 3; offset++) { const friend = drivers[(i + offset) % drivers.length]!; friendships.push({ driverId: driver.id, friendId: friend.id }); } } return friendships; } private createFeedEvents(drivers: Driver[], friendships: Friendship[], races: Race[], leagues: League[]): FeedItem[] { const items: FeedItem[] = []; const friendMap = new Map(friendships.map((f) => [`${f.driverId}:${f.friendId}`, true] as const)); const completedRace = races.find((r) => r.status === 'completed'); const upcomingRace = races.find((r) => r.status === 'scheduled'); const league = leagues.find((l) => l.id.toString() === 'league-5') ?? leagues[0]!; const now = this.addMinutes(this.baseDate, 10); for (let i = 2; i <= 10; i++) { const actor = drivers.find((d) => d.id === `driver-${i}`); if (!actor) continue; if (!friendMap.has(`driver-1:${actor.id}`)) continue; items.push({ id: `feed:${actor.id}:joined:${i}`, type: 'friend-joined-league', timestamp: this.addMinutes(now, -(i * 7)), actorDriverId: actor.id, actorFriendId: actor.id, leagueId: league.id.toString(), headline: `${actor.name} joined ${String(league.name)}`, body: 'Demo activity in in-memory mode.', ctaLabel: 'View league', ctaHref: `/leagues/${league.id.toString()}`, }); if (completedRace) { items.push({ id: `feed:${actor.id}:result:${i}`, type: 'friend-finished-race', timestamp: this.addMinutes(now, -(i * 7 + 3)), actorDriverId: actor.id, actorFriendId: actor.id, leagueId: completedRace.leagueId, raceId: completedRace.id, position: (i % 5) + 1, headline: `${actor.name} finished a race`, body: `Completed at ${completedRace.track}.`, ctaLabel: 'View results', ctaHref: `/races/${completedRace.id}/results`, }); } } if (upcomingRace) { items.push({ id: `feed:system:scheduled:${upcomingRace.id}`, type: 'new-race-scheduled', timestamp: this.addMinutes(now, -3), leagueId: upcomingRace.leagueId, raceId: upcomingRace.id, headline: `New race scheduled at ${upcomingRace.track}`, body: `${upcomingRace.car} • ${upcomingRace.scheduledAt.toISOString()}`, ctaLabel: 'View schedule', ctaHref: `/races/${upcomingRace.id}`, }); } return items.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } } export function createInMemoryRacingSeed(options: InMemoryRacingSeedOptions = {}): InMemoryRacingSeed { return new InMemoryRacingSeedFactory(options).create(); }