Files
gridpilot.gg/adapters/bootstrap/inmemory/InMemoryRacingSeed.ts
2025-12-26 22:22:39 +01:00

557 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<InMemoryRacingSeedOptions>
> = {
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<string, Record<number, number>>,
): Record<number, number> {
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<string, Set<string>>();
for (const race of races) {
if (race.status !== 'completed') continue;
const set = racesByLeague.get(race.leagueId) ?? new Set<string>();
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<string>();
if (completedRaceIds.size === 0) continue;
const pointsTable = this.resolvePointsSystem(league, pointsSystems);
const byDriver = new Map<string, Standing>();
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();
}