import { ResultWithIncidents } from '../../domain/entities/ResultWithIncidents'; import { RaceIncidents, type IncidentRecord, type IncidentType } from '../../domain/value-objects/RaceIncidents'; /** * Enhanced race result generator with detailed incident types */ export class RaceResultGeneratorWithIncidents { /** * Generate realistic race results with detailed incidents */ static generateRaceResults( raceId: string, driverIds: string[], driverRatings: Map ): ResultWithIncidents[] { // Create driver performance data const driverPerformances = driverIds.map(driverId => ({ driverId, rating: driverRatings.get(driverId) ?? 1500, // Default rating randomFactor: Math.random() - 0.5, // -0.5 to +0.5 randomization })); // Sort by performance (rating + randomization) driverPerformances.sort((a, b) => { const perfA = a.rating + (a.randomFactor * 200); // ±100 rating points randomization const perfB = b.rating + (b.randomFactor * 200); return perfB - perfA; // Higher performance first }); // Generate qualifying results for start positions (similar but different from race results) const qualiPerformances = driverPerformances.map(p => ({ ...p, randomFactor: Math.random() - 0.5, // New randomization for quali })); qualiPerformances.sort((a, b) => { const perfA = a.rating + (a.randomFactor * 150); const perfB = b.rating + (b.randomFactor * 150); return perfB - perfA; }); // Generate results const results: ResultWithIncidents[] = []; for (let i = 0; i < driverPerformances.length; i++) { const { driverId } = driverPerformances[i]; const position = i + 1; const startPosition = qualiPerformances.findIndex(p => p.driverId === driverId) + 1; // Generate realistic lap times (90-120 seconds for a lap) const baseLapTime = 90000 + Math.random() * 30000; const positionBonus = (position - 1) * 500; // Winners are faster const fastestLap = Math.round(baseLapTime + positionBonus + Math.random() * 5000); // Generate detailed incidents const incidents = this.generateDetailedIncidents(position, driverPerformances.length); results.push( ResultWithIncidents.create({ id: `${raceId}-${driverId}`, raceId, driverId, position, startPosition, fastestLap, incidents, }) ); } return results; } /** * Generate detailed incidents with specific types and severity */ private static generateDetailedIncidents(position: number, totalDrivers: number): RaceIncidents { // Base probability increases for lower positions (more aggressive driving) const baseProbability = Math.min(0.85, position / totalDrivers + 0.1); // Add some randomness const randomFactor = Math.random(); if (randomFactor > baseProbability) { // Clean race return new RaceIncidents(); } // Determine number of incidents based on position and severity const severityRoll = Math.random(); let incidentCount: number; if (severityRoll < 0.5) { incidentCount = 1; // Minor incident } else if (severityRoll < 0.8) { incidentCount = 2; // Moderate incident } else if (severityRoll < 0.95) { incidentCount = 3; // Major incident } else { incidentCount = Math.floor(Math.random() * 2) + 3; // 3-4 incidents (severe) } // Generate specific incidents const incidents: IncidentRecord[] = []; for (let i = 0; i < incidentCount; i++) { const incidentType = this.selectIncidentType(position, totalDrivers, i); const lap = this.selectIncidentLap(i + 1, incidentCount); incidents.push({ type: incidentType, lap, description: this.generateIncidentDescription(incidentType), penaltyPoints: this.getPenaltyPoints(incidentType), }); } return new RaceIncidents(incidents); } /** * Select appropriate incident type based on context */ private static selectIncidentType(position: number, totalDrivers: number, incidentIndex: number): IncidentType { // Different incident types have different probabilities const incidentProbabilities: Array<{ type: IncidentType; weight: number }> = [ { type: 'track_limits', weight: 40 }, // Most common { type: 'contact', weight: 25 }, // Common in traffic { type: 'unsafe_rejoin', weight: 15 }, // Dangerous { type: 'aggressive_driving', weight: 10 }, // Less common { type: 'collision', weight: 5 }, // Rare { type: 'spin', weight: 4 }, // Rare { type: 'false_start', weight: 1 }, // Very rare in race ]; // Adjust weights based on position (lower positions more likely to have contact/aggressive driving) if (position > totalDrivers * 0.7) { // Bottom 30% incidentProbabilities.find(p => p.type === 'contact')!.weight += 10; incidentProbabilities.find(p => p.type === 'aggressive_driving')!.weight += 5; } // Select based on weights const totalWeight = incidentProbabilities.reduce((sum, p) => sum + p.weight, 0); let random = Math.random() * totalWeight; for (const { type, weight } of incidentProbabilities) { random -= weight; if (random <= 0) { return type; } } return 'track_limits'; // Fallback } /** * Select appropriate lap for incident */ private static selectIncidentLap(incidentNumber: number, totalIncidents: number): number { // Spread incidents throughout the race const raceLaps = 20; // Assume 20 lap race const lapRanges = [ { min: 1, max: 5 }, // Early race { min: 6, max: 12 }, // Mid race { min: 13, max: 20 }, // Late race ]; // Distribute incidents across race phases const phaseIndex = Math.min(incidentNumber - 1, lapRanges.length - 1); const range = lapRanges[phaseIndex]; return Math.floor(Math.random() * (range.max - range.min + 1)) + range.min; } /** * Generate human-readable description for incident */ private static generateIncidentDescription(type: IncidentType): string { const descriptions: Record = { track_limits: [ 'Went off track at corner exit', 'Cut corner to maintain position', 'Ran wide under braking', 'Off-track excursion gaining advantage', ], contact: [ 'Light contact while defending position', 'Side-by-side contact into corner', 'Rear-end contact under braking', 'Wheel-to-wheel contact', ], unsafe_rejoin: [ 'Unsafe rejoin across track', 'Rejoined directly into racing line', 'Failed to check mirrors before rejoining', 'Forced another driver off track on rejoin', ], aggressive_driving: [ 'Multiple defensive moves under braking', 'Moved under braking three times', 'Aggressive defending forcing driver wide', 'Persistent blocking maneuvers', ], collision: [ 'Collision involving multiple cars', 'Major contact causing safety car', 'Chain reaction collision', 'Heavy impact collision', ], spin: [ 'Lost control and spun', 'Oversteer spin into gravel', 'Spin following contact', 'Lost rear grip and spun', ], false_start: [ 'Jumped start before green flag', 'Early launch from grid', 'Premature start', ], mechanical: [ 'Engine failure', 'Gearbox issue', 'Brake failure', 'Suspension damage', ], other: [ 'Unspecified incident', 'Race incident', 'Driving infraction', ], }; const options = descriptions[type] || descriptions.other; return options[Math.floor(Math.random() * options.length)]; } /** * Get penalty points for incident type */ private static getPenaltyPoints(type: IncidentType): number { const penalties: Record = { track_limits: 0, // Usually warning only contact: 2, // Light penalty unsafe_rejoin: 3, // Moderate penalty aggressive_driving: 2, // Light penalty false_start: 5, // Heavy penalty collision: 5, // Heavy penalty spin: 0, // Usually no penalty if no contact mechanical: 0, // Not driver fault other: 2, // Default penalty }; return penalties[type]; } /** * Get incident description for display */ static getIncidentDescription(incidents: RaceIncidents): string { return incidents.getSummary(); } /** * Calculate incident penalty points for standings */ static getIncidentPenaltyPoints(incidents: RaceIncidents): number { return incidents.getTotalPenaltyPoints(); } }