413 lines
15 KiB
TypeScript
413 lines
15 KiB
TypeScript
import { League, LeagueSettings } from '@core/racing/domain/entities/League';
|
|
import { Driver } from '@core/racing/domain/entities/Driver';
|
|
import { MediaReference } from '@core/domain/media/MediaReference';
|
|
import { faker } from '@faker-js/faker';
|
|
import { seedId } from './SeedIdHelper';
|
|
|
|
export class RacingLeagueFactory {
|
|
constructor(
|
|
private readonly baseDate: Date,
|
|
private readonly drivers: Driver[],
|
|
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
|
) {}
|
|
|
|
create(): League[] {
|
|
const leagueCount = 120; // Expanded to 100+ leagues
|
|
|
|
// Create diverse league configurations covering ALL enum combinations
|
|
// Points systems: f1-2024, indycar, custom (3)
|
|
// Qualifying formats: single-lap, open (2)
|
|
// Visibility: ranked, unranked (2)
|
|
// Decision modes: admin_only, steward_decides, steward_vote, member_vote, steward_veto, member_veto (6)
|
|
// Total combinations: 3 * 2 * 2 * 6 = 72, but we'll sample 30 covering extremes
|
|
|
|
// Category types for leagues
|
|
|
|
const leagueConfigs = [
|
|
// 1-5: Ranked, F1-2024, various stewarding
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 40,
|
|
sessionDuration: 60,
|
|
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 32,
|
|
sessionDuration: 45,
|
|
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 3, requireDefense: true, defenseTimeLimit: 24 }
|
|
},
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 50,
|
|
sessionDuration: 90,
|
|
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 5, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 24,
|
|
sessionDuration: 30,
|
|
stewarding: { decisionMode: 'steward_veto' as const, requiredVotes: 2, requireDefense: true }
|
|
},
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 60,
|
|
sessionDuration: 120,
|
|
stewarding: { decisionMode: 'member_veto' as const, requiredVotes: 4, requireDefense: false }
|
|
},
|
|
|
|
// 6-10: Ranked, IndyCar, various stewarding
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 28,
|
|
sessionDuration: 75,
|
|
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true, defenseTimeLimit: 48 }
|
|
},
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 36,
|
|
sessionDuration: 60,
|
|
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 44,
|
|
sessionDuration: 100,
|
|
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 4, requireDefense: true, voteTimeLimit: 72 }
|
|
},
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 20,
|
|
sessionDuration: 45,
|
|
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 6, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 48,
|
|
sessionDuration: 110,
|
|
stewarding: { decisionMode: 'steward_veto' as const, requiredVotes: 3, requireDefense: true }
|
|
},
|
|
|
|
// 11-15: Ranked, Custom, various stewarding
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 32,
|
|
sessionDuration: 50,
|
|
stewarding: { decisionMode: 'member_veto' as const, requiredVotes: 5, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 40,
|
|
sessionDuration: 85,
|
|
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 52,
|
|
sessionDuration: 95,
|
|
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true, defenseTimeLimit: 36 }
|
|
},
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 26,
|
|
sessionDuration: 65,
|
|
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 2, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'ranked' as const,
|
|
maxDrivers: 56,
|
|
sessionDuration: 105,
|
|
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 7, requireDefense: true, protestDeadlineHours: 24 }
|
|
},
|
|
|
|
// 16-20: Unranked, F1-2024, various stewarding (smaller, no participant requirements)
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 8,
|
|
sessionDuration: 30,
|
|
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 12,
|
|
sessionDuration: 45,
|
|
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true }
|
|
},
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 16,
|
|
sessionDuration: 60,
|
|
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 2, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 10,
|
|
sessionDuration: 35,
|
|
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 3, requireDefense: true }
|
|
},
|
|
{
|
|
pointsSystem: 'f1-2024' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 20,
|
|
sessionDuration: 55,
|
|
stewarding: { decisionMode: 'steward_veto' as const, requiredVotes: 2, requireDefense: false }
|
|
},
|
|
|
|
// 21-25: Unranked, IndyCar, various stewarding
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 14,
|
|
sessionDuration: 40,
|
|
stewarding: { decisionMode: 'member_veto' as const, requiredVotes: 3, requireDefense: true }
|
|
},
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 18,
|
|
sessionDuration: 50,
|
|
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 22,
|
|
sessionDuration: 65,
|
|
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true, defenseTimeLimit: 12 }
|
|
},
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 6,
|
|
sessionDuration: 25,
|
|
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 2, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'indycar' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 24,
|
|
sessionDuration: 70,
|
|
stewarding: { decisionMode: 'member_vote' as const, requiredVotes: 4, requireDefense: true }
|
|
},
|
|
|
|
// 26-30: Unranked, Custom, various stewarding
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 12,
|
|
sessionDuration: 35,
|
|
stewarding: { decisionMode: 'steward_veto' as const, requiredVotes: 2, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 16,
|
|
sessionDuration: 45,
|
|
stewarding: { decisionMode: 'member_veto' as const, requiredVotes: 3, requireDefense: true }
|
|
},
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 20,
|
|
sessionDuration: 55,
|
|
stewarding: { decisionMode: 'admin_only' as const, requireDefense: false }
|
|
},
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'single-lap' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 8,
|
|
sessionDuration: 30,
|
|
stewarding: { decisionMode: 'steward_decides' as const, requireDefense: true, defenseTimeLimit: 18 }
|
|
},
|
|
{
|
|
pointsSystem: 'custom' as const,
|
|
qualifyingFormat: 'open' as const,
|
|
visibility: 'unranked' as const,
|
|
maxDrivers: 24,
|
|
sessionDuration: 60,
|
|
stewarding: { decisionMode: 'steward_vote' as const, requiredVotes: 3, requireDefense: false }
|
|
},
|
|
];
|
|
|
|
return Array.from({ length: leagueCount }, (_, idx) => {
|
|
const i = idx + 1;
|
|
const owner = faker.helpers.arrayElement(this.drivers);
|
|
// Cycle through the 30 configs for variety
|
|
const config = leagueConfigs[idx % 30]!;
|
|
|
|
const createdAt =
|
|
// Ensure some "New" leagues (created within 7 days) so `/leagues` featured categories are populated.
|
|
idx % 6 === 0
|
|
? faker.date.recent({ days: 6, refDate: this.baseDate })
|
|
: faker.date.past({ years: 2, refDate: this.baseDate });
|
|
|
|
// Calculate participant count based on visibility and league
|
|
let participantCount: number;
|
|
if (config.visibility === 'ranked') {
|
|
// Ranked leagues need >= 10 participants
|
|
// Some are full (max), some have minimum, some have none
|
|
if (idx % 5 === 0) {
|
|
participantCount = config.maxDrivers; // Full league
|
|
} else if (idx % 3 === 0) {
|
|
participantCount = 12; // Just meets minimum
|
|
} else if (idx % 7 === 0) {
|
|
participantCount = 0; // Empty but valid
|
|
} else {
|
|
participantCount = faker.number.int({ min: 10, max: config.maxDrivers - 5 });
|
|
}
|
|
} else {
|
|
// Unranked can have any count, but must have at least 2 if not empty
|
|
if (idx % 4 === 0) {
|
|
participantCount = config.maxDrivers; // Full
|
|
} else if (idx % 5 === 0) {
|
|
participantCount = 0; // Empty
|
|
} else {
|
|
participantCount = faker.number.int({ min: 2, max: Math.min(config.maxDrivers, 15) });
|
|
}
|
|
}
|
|
|
|
// Determine category based on scoring configuration
|
|
let category: string | undefined;
|
|
if (config.pointsSystem === 'f1-2024') {
|
|
if (config.qualifyingFormat === 'single-lap') {
|
|
category = 'driver';
|
|
} else {
|
|
category = 'team';
|
|
}
|
|
} else if (config.pointsSystem === 'indycar') {
|
|
category = 'nations';
|
|
} else if (config.pointsSystem === 'custom') {
|
|
category = 'trophy';
|
|
}
|
|
|
|
// Override some leagues to have endurance or sprint categories
|
|
if (idx % 8 === 0) {
|
|
category = 'endurance';
|
|
} else if (idx % 7 === 0) {
|
|
category = 'sprint';
|
|
}
|
|
|
|
// Build the league data object
|
|
const leagueData = {
|
|
id: seedId(`league-${i}`, this.persistence),
|
|
name: faker.company.name() + ' Racing League',
|
|
description: faker.lorem.sentences(2),
|
|
ownerId: owner.id.toString(),
|
|
settings: {
|
|
pointsSystem: config.pointsSystem,
|
|
maxDrivers: config.maxDrivers,
|
|
sessionDuration: config.sessionDuration,
|
|
qualifyingFormat: config.qualifyingFormat,
|
|
visibility: config.visibility,
|
|
stewarding: {
|
|
...config.stewarding,
|
|
voteTimeLimit: 72,
|
|
protestDeadlineHours: 48,
|
|
stewardingClosesHours: 168,
|
|
notifyAccusedOnProtest: true,
|
|
notifyOnVoteRequired: true,
|
|
},
|
|
},
|
|
category,
|
|
createdAt,
|
|
participantCount,
|
|
};
|
|
|
|
// Add social links with varying completeness
|
|
const socialLinks: { discordUrl?: string; youtubeUrl?: string; websiteUrl?: string } = {};
|
|
const socialLinkTypes = ['discord', 'youtube', 'website'] as const;
|
|
|
|
// Ensure some leagues have no social links, some have partial, some have all
|
|
const numSocialLinks = idx % 4; // 0, 1, 2, or 3
|
|
const selectedTypes = faker.helpers.arrayElements(socialLinkTypes, numSocialLinks);
|
|
|
|
selectedTypes.forEach(type => {
|
|
if (type === 'discord') socialLinks.discordUrl = faker.internet.url();
|
|
if (type === 'youtube') socialLinks.youtubeUrl = faker.internet.url();
|
|
if (type === 'website') socialLinks.websiteUrl = faker.internet.url();
|
|
});
|
|
|
|
// Create the final league data with optional fields
|
|
const finalLeagueData: {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
ownerId: string;
|
|
settings?: Partial<LeagueSettings>;
|
|
category?: string | undefined;
|
|
createdAt?: Date;
|
|
socialLinks?: {
|
|
discordUrl?: string;
|
|
youtubeUrl?: string;
|
|
websiteUrl?: string;
|
|
};
|
|
participantCount?: number;
|
|
logoRef?: MediaReference;
|
|
} = {
|
|
id: leagueData.id,
|
|
name: leagueData.name,
|
|
description: leagueData.description,
|
|
ownerId: leagueData.ownerId,
|
|
settings: leagueData.settings,
|
|
category: leagueData.category,
|
|
createdAt: leagueData.createdAt,
|
|
participantCount: leagueData.participantCount,
|
|
logoRef: MediaReference.generated('league', leagueData.id),
|
|
};
|
|
|
|
if (Object.keys(socialLinks).length > 0) {
|
|
finalLeagueData.socialLinks = socialLinks;
|
|
}
|
|
|
|
return League.create(finalLeagueData);
|
|
});
|
|
}
|
|
} |