Files
gridpilot.gg/core/racing/application/utils/RaceResultGeneratorWithIncidents.ts
2025-12-26 11:49:20 +01:00

266 lines
8.8 KiB
TypeScript

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<string, number>
): 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 (const [index, performance] of driverPerformances.entries()) {
const driverId = performance.driverId;
const position = index + 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);
const lap = this.selectIncidentLap(i + 1);
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): 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): number {
// Spread incidents throughout the 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] ?? lapRanges[0]!;
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<IncidentType, string[]> = {
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;
const selected = options[Math.floor(Math.random() * options.length)];
return selected ?? descriptions.other[0]!;
}
/**
* Get penalty points for incident type
*/
private static getPenaltyPoints(type: IncidentType): number {
const penalties: Record<IncidentType, number> = {
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();
}
}