rename to core
This commit is contained in:
130
core/racing/application/utils/RaceResultGenerator.ts
Normal file
130
core/racing/application/utils/RaceResultGenerator.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Result } from '../../domain/entities/Result';
|
||||
|
||||
/**
|
||||
* Enhanced race result generator with detailed incident types
|
||||
*/
|
||||
export class RaceResultGenerator {
|
||||
/**
|
||||
* Generate realistic race results with detailed incidents
|
||||
*/
|
||||
static generateRaceResults(
|
||||
raceId: string,
|
||||
driverIds: string[],
|
||||
driverRatings: Map<string, number>
|
||||
): Result[] {
|
||||
// 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: Result[] = [];
|
||||
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(
|
||||
Result.create({
|
||||
id: `${raceId}-${driverId}`,
|
||||
raceId,
|
||||
driverId,
|
||||
position,
|
||||
startPosition,
|
||||
fastestLap,
|
||||
incidents,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate detailed incidents with specific types
|
||||
*/
|
||||
private static generateDetailedIncidents(position: number, totalDrivers: number): number {
|
||||
// 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) {
|
||||
return 0; // Clean race
|
||||
}
|
||||
|
||||
// Determine incident severity based on position and randomness
|
||||
const severityRoll = Math.random();
|
||||
|
||||
if (severityRoll < 0.4) {
|
||||
// Minor incident (track limits, small contact)
|
||||
return 1;
|
||||
} else if (severityRoll < 0.7) {
|
||||
// Moderate incident (off-track, contact with damage)
|
||||
return 2;
|
||||
} else if (severityRoll < 0.9) {
|
||||
// Major incident (spin, collision)
|
||||
return 3;
|
||||
} else {
|
||||
// Severe incident (multiple cars involved, safety car)
|
||||
return Math.floor(Math.random() * 2) + 3; // 3-4 incidents
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get incident type description for a given incident count
|
||||
*/
|
||||
static getIncidentDescription(incidents: number): string {
|
||||
switch (incidents) {
|
||||
case 0:
|
||||
return 'Clean race';
|
||||
case 1:
|
||||
return 'Track limits violation';
|
||||
case 2:
|
||||
return 'Contact with another car';
|
||||
case 3:
|
||||
return 'Off-track incident';
|
||||
case 4:
|
||||
return 'Collision requiring safety car';
|
||||
default:
|
||||
return `${incidents} incidents`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate incident penalty points for standings
|
||||
*/
|
||||
static getIncidentPenaltyPoints(incidents: number): number {
|
||||
// Each incident deducts points from championship standings
|
||||
return Math.max(0, incidents - 1) * 2; // First incident free, then 2 points each
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
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 (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<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;
|
||||
return options[Math.floor(Math.random() * options.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user