411 lines
11 KiB
TypeScript
411 lines
11 KiB
TypeScript
import { Driver } from '@core/racing/domain/entities/Driver';
|
||
import { League } from '@core/racing/domain/entities/League';
|
||
import { Race } from '@core/racing/domain/entities/Race';
|
||
import { Result } from '@core/racing/domain/entities/Result';
|
||
import { Standing } from '@core/racing/domain/entities/Standing';
|
||
import { SessionType } from '@core/racing/domain/value-objects/SessionType';
|
||
import { faker } from '../../helpers/faker/faker';
|
||
|
||
/**
|
||
* Core racing seed types and generators (drivers, leagues, teams, races, standings).
|
||
* Extracted from the legacy StaticRacingSeed module to keep files smaller and focused.
|
||
*/
|
||
export type RacingMembership = {
|
||
driverId: string;
|
||
leagueId: string;
|
||
teamId?: string;
|
||
};
|
||
|
||
export type Friendship = {
|
||
driverId: string;
|
||
friendId: string;
|
||
};
|
||
|
||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||
|
||
export interface DemoTeamDTO {
|
||
id: string;
|
||
name: string;
|
||
tag: string;
|
||
description: string;
|
||
logoRef: MediaReference;
|
||
primaryLeagueId: string;
|
||
memberCount: number;
|
||
}
|
||
|
||
/**
|
||
* Championship points table used when aggregating standings.
|
||
*/
|
||
export const POINTS_TABLE: Record<number, number> = {
|
||
1: 25,
|
||
2: 18,
|
||
3: 15,
|
||
4: 12,
|
||
5: 10,
|
||
6: 8,
|
||
7: 6,
|
||
8: 4,
|
||
9: 2,
|
||
10: 1,
|
||
};
|
||
|
||
export function pickOne<T>(items: readonly T[]): T {
|
||
if (items.length === 0) {
|
||
throw new Error('pickOne: empty items array');
|
||
}
|
||
const index = faker.number.int({ min: 0, max: items.length - 1 });
|
||
return items[index]!;
|
||
}
|
||
|
||
export function createDrivers(count: number): Driver[] {
|
||
const drivers: Driver[] = [];
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const id = `driver-${i + 1}`;
|
||
const name = faker.person.fullName();
|
||
const country = faker.location.countryCode('alpha-2');
|
||
const iracingId = faker.string.numeric(6);
|
||
|
||
drivers.push(
|
||
Driver.create({
|
||
id,
|
||
iracingId,
|
||
name,
|
||
country,
|
||
bio: faker.lorem.sentence(),
|
||
joinedAt: faker.date.past(),
|
||
}),
|
||
);
|
||
}
|
||
|
||
return drivers;
|
||
}
|
||
|
||
export function createLeagues(ownerIds: string[]): League[] {
|
||
const leagueNames = [
|
||
'GridPilot Sprint Series',
|
||
'GridPilot Endurance Cup',
|
||
'GridPilot Club Ladder',
|
||
'Sprint Challenge League',
|
||
'Club Racers Collective',
|
||
'Sim Racing Alliance',
|
||
'Pacific Time Attack',
|
||
'Nordic Night Series',
|
||
];
|
||
|
||
const leagues: League[] = [];
|
||
const leagueCount = 6 + faker.number.int({ min: 0, max: 2 });
|
||
|
||
for (let i = 0; i < leagueCount; i++) {
|
||
const id = `league-${i + 1}`;
|
||
const name = leagueNames[i] ?? faker.company.name();
|
||
// Ensure league-5 (demo league with running race) is owned by driver-1
|
||
const ownerId = i === 4 ? 'driver-1' : pickOne(ownerIds);
|
||
|
||
const maxDriversOptions = [24, 32, 48, 64];
|
||
let settings = {
|
||
pointsSystem: faker.helpers.arrayElement(['f1-2024', 'indycar']),
|
||
sessionDuration: faker.helpers.arrayElement([45, 60, 90, 120]),
|
||
qualifyingFormat: faker.helpers.arrayElement(['open', 'single-lap']),
|
||
maxDrivers: faker.helpers.arrayElement(maxDriversOptions),
|
||
} as const;
|
||
|
||
if (i === 0) {
|
||
settings = {
|
||
...settings,
|
||
maxDrivers: 24,
|
||
};
|
||
} else if (i === 1) {
|
||
settings = {
|
||
...settings,
|
||
maxDrivers: 24,
|
||
};
|
||
} else if (i === 2) {
|
||
settings = {
|
||
...settings,
|
||
maxDrivers: 40,
|
||
};
|
||
}
|
||
|
||
const socialLinks =
|
||
i === 0
|
||
? {
|
||
discordUrl: 'https://discord.gg/gridpilot-demo',
|
||
youtubeUrl: 'https://youtube.com/@gridpilot-demo',
|
||
websiteUrl: 'https://gridpilot-demo.example.com',
|
||
}
|
||
: i === 1
|
||
? {
|
||
discordUrl: 'https://discord.gg/gridpilot-endurance',
|
||
youtubeUrl: 'https://youtube.com/@gridpilot-endurance',
|
||
}
|
||
: i === 2
|
||
? {
|
||
websiteUrl: 'https://virtual-touring.example.com',
|
||
}
|
||
: undefined;
|
||
|
||
if (socialLinks) {
|
||
leagues.push(
|
||
League.create({
|
||
id,
|
||
name,
|
||
description: faker.lorem.sentence(),
|
||
ownerId,
|
||
settings,
|
||
createdAt: faker.date.past(),
|
||
socialLinks,
|
||
}),
|
||
);
|
||
} else {
|
||
leagues.push(
|
||
League.create({
|
||
id,
|
||
name,
|
||
description: faker.lorem.sentence(),
|
||
ownerId,
|
||
settings,
|
||
createdAt: faker.date.past(),
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
|
||
return leagues;
|
||
}
|
||
|
||
export function createTeams(leagues: League[]): DemoTeamDTO[] {
|
||
const teams: DemoTeamDTO[] = [];
|
||
const teamCount = 24 + faker.number.int({ min: 0, max: 12 });
|
||
|
||
for (let i = 0; i < teamCount; i++) {
|
||
const id = `team-${i + 1}`;
|
||
const primaryLeague = pickOne(leagues);
|
||
const name = faker.company.name();
|
||
const tag = faker.string.alpha({ length: 4 }).toUpperCase();
|
||
const memberCount = faker.number.int({ min: 2, max: 8 });
|
||
|
||
teams.push({
|
||
id,
|
||
name,
|
||
tag,
|
||
description: faker.lorem.sentence(),
|
||
logoRef: MediaReference.systemDefault('logo'),
|
||
primaryLeagueId: primaryLeague.id,
|
||
memberCount,
|
||
});
|
||
}
|
||
|
||
return teams;
|
||
}
|
||
|
||
export function createMemberships(
|
||
drivers: Driver[],
|
||
leagues: League[],
|
||
teams: DemoTeamDTO[],
|
||
): RacingMembership[] {
|
||
const memberships: RacingMembership[] = [];
|
||
|
||
const teamsByLeague = new Map<string, DemoTeamDTO[]>();
|
||
teams.forEach((team) => {
|
||
const list = teamsByLeague.get(team.primaryLeagueId) ?? [];
|
||
list.push(team);
|
||
teamsByLeague.set(team.primaryLeagueId, list);
|
||
});
|
||
|
||
|
||
drivers.forEach((driver) => {
|
||
// Each driver participates in 1–3 leagues
|
||
const leagueSampleSize = faker.number.int({ min: 1, max: Math.min(3, leagues.length) });
|
||
const shuffledLeagues = faker.helpers.shuffle(leagues).slice(0, leagueSampleSize);
|
||
|
||
shuffledLeagues.forEach((league) => {
|
||
const leagueTeams = teamsByLeague.get(league.id) ?? [];
|
||
const team =
|
||
leagueTeams.length > 0 && faker.datatype.boolean()
|
||
? pickOne(leagueTeams)
|
||
: undefined;
|
||
|
||
const membership: RacingMembership = {
|
||
driverId: driver.id,
|
||
leagueId: league.id,
|
||
};
|
||
|
||
if (team) {
|
||
membership.teamId = team.id;
|
||
}
|
||
|
||
memberships.push(membership);
|
||
});
|
||
});
|
||
|
||
return memberships;
|
||
}
|
||
|
||
export function createRaces(leagues: League[]): Race[] {
|
||
const races: Race[] = [];
|
||
const raceCount = 60 + faker.number.int({ min: 0, max: 20 });
|
||
|
||
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 baseDate = new Date();
|
||
|
||
for (let i = 0; i < raceCount; i++) {
|
||
const id = `race-${i + 1}`;
|
||
let league = pickOne(leagues);
|
||
const offsetDays = faker.number.int({ min: -30, max: 45 });
|
||
const scheduledAt = new Date(baseDate.getTime() + offsetDays * 24 * 60 * 60 * 1000);
|
||
let status: 'scheduled' | 'completed' | 'running' = scheduledAt.getTime() < baseDate.getTime() ? 'completed' : 'scheduled';
|
||
let strengthOfField: number | undefined;
|
||
|
||
// Special case: Make race-1 a running race in league-5 (user's admin league)
|
||
if (i === 0) {
|
||
const league5 = leagues.find(l => l.id === 'league-5');
|
||
if (league5) {
|
||
league = league5;
|
||
status = 'running';
|
||
// Calculate SOF for the running race (simulate 12-20 drivers with average rating ~1500)
|
||
const participantCount = faker.number.int({ min: 12, max: 20 });
|
||
const averageRating = 1500 + faker.number.int({ min: -200, max: 300 });
|
||
strengthOfField = Math.round(averageRating);
|
||
}
|
||
}
|
||
|
||
races.push(
|
||
Race.create({
|
||
id,
|
||
leagueId: league.id,
|
||
scheduledAt,
|
||
track: faker.helpers.arrayElement(tracks),
|
||
car: faker.helpers.arrayElement(cars),
|
||
sessionType: SessionType.main(),
|
||
status,
|
||
...(strengthOfField !== undefined ? { strengthOfField } : {}),
|
||
...(status === 'running' ? { registeredCount: faker.number.int({ min: 12, max: 20 }) } : {}),
|
||
}),
|
||
);
|
||
}
|
||
|
||
return races;
|
||
}
|
||
|
||
export function createResults(drivers: Driver[], races: Race[]): Result[] {
|
||
const results: Result[] = [];
|
||
|
||
const completedRaces = races.filter((race) => race.status === 'completed');
|
||
|
||
completedRaces.forEach((race) => {
|
||
const participantCount = faker.number.int({ min: 20, max: 32 });
|
||
const shuffledDrivers = faker.helpers.shuffle(drivers).slice(0, participantCount);
|
||
|
||
shuffledDrivers.forEach((driver, index) => {
|
||
const position = index + 1;
|
||
const startPosition = faker.number.int({ min: 1, max: participantCount });
|
||
const fastestLap = 90_000 + index * 250 + faker.number.int({ min: 0, max: 2_000 });
|
||
const incidents = faker.number.int({ min: 0, max: 6 });
|
||
|
||
results.push(
|
||
Result.create({
|
||
id: `${race.id}-${driver.id}`,
|
||
raceId: race.id,
|
||
driverId: driver.id,
|
||
position,
|
||
startPosition,
|
||
fastestLap,
|
||
incidents,
|
||
}),
|
||
);
|
||
});
|
||
});
|
||
|
||
return results;
|
||
}
|
||
|
||
export function createStandings(leagues: League[], results: Result[]): Standing[] {
|
||
const standingsByLeague = new Map<string, Standing[]>();
|
||
|
||
leagues.forEach((league) => {
|
||
const leagueRaceIds = new Set(
|
||
results
|
||
.filter((result) => {
|
||
return result.raceId.startsWith('race-');
|
||
})
|
||
.map((result) => result.raceId),
|
||
);
|
||
|
||
const leagueResults = results.filter((result) => leagueRaceIds.has(result.raceId));
|
||
|
||
const standingsMap = new Map<string, Standing>();
|
||
|
||
leagueResults.forEach((result) => {
|
||
const key = result.driverId;
|
||
let standing = standingsMap.get(key);
|
||
|
||
if (!standing) {
|
||
standing = Standing.create({
|
||
leagueId: league.id,
|
||
driverId: result.driverId,
|
||
});
|
||
}
|
||
|
||
standing = standing.addRaceResult(result.position, POINTS_TABLE);
|
||
standingsMap.set(key, standing);
|
||
});
|
||
|
||
const sortedStandings = Array.from(standingsMap.values()).sort((a, b) => {
|
||
if (b.points !== a.points) {
|
||
return b.points - a.points;
|
||
}
|
||
if (b.wins !== a.wins) {
|
||
return b.wins - a.wins;
|
||
}
|
||
return b.racesCompleted - a.racesCompleted;
|
||
});
|
||
|
||
const finalizedStandings = sortedStandings.map((standing, index) =>
|
||
standing.updatePosition(index + 1),
|
||
);
|
||
|
||
standingsByLeague.set(league.id, finalizedStandings);
|
||
});
|
||
|
||
return Array.from(standingsByLeague.values()).flat();
|
||
}
|
||
|
||
export function createFriendships(drivers: Driver[]): Friendship[] {
|
||
const friendships: Friendship[] = [];
|
||
|
||
drivers.forEach((driver, index) => {
|
||
const friendCount = faker.number.int({ min: 3, max: 8 });
|
||
for (let offset = 1; offset <= friendCount; offset++) {
|
||
const friendIndex = (index + offset) % drivers.length;
|
||
const friend = drivers[friendIndex];
|
||
if (!friend) continue;
|
||
if (friend.id === driver.id) continue;
|
||
|
||
friendships.push({
|
||
driverId: driver.id,
|
||
friendId: friend.id,
|
||
});
|
||
}
|
||
});
|
||
|
||
return friendships;
|
||
} |