import type { IValueObject } from '@core/shared/domain'; /** * Incident types that can occur during a race */ export type IncidentType = | 'track_limits' // Driver went off track and gained advantage | 'contact' // Physical contact with another car | 'unsafe_rejoin' // Unsafe rejoining of the track | 'aggressive_driving' // Aggressive defensive or overtaking maneuvers | 'false_start' // Started before green flag | 'collision' // Major collision involving multiple cars | 'spin' // Driver spun out | 'mechanical' // Mechanical failure (not driver error) | 'other'; // Other incident types /** * Individual incident record */ export interface IncidentRecord { type: IncidentType; lap: number; description?: string; penaltyPoints?: number; // Points deducted for this incident } /** * Value Object: RaceIncidents * * Encapsulates all incidents that occurred during a driver's race. * Provides methods to calculate total penalty points and incident severity. */ export class RaceIncidents implements IValueObject { private readonly incidents: IncidentRecord[]; constructor(incidents: IncidentRecord[] = []) { this.incidents = [...incidents]; } get props(): IncidentRecord[] { return [...this.incidents]; } /** * Add a new incident */ addIncident(incident: IncidentRecord): RaceIncidents { return new RaceIncidents([...this.incidents, incident]); } /** * Get all incidents */ getAllIncidents(): IncidentRecord[] { return [...this.incidents]; } /** * Get total number of incidents */ getTotalCount(): number { return this.incidents.length; } /** * Get total penalty points from all incidents */ getTotalPenaltyPoints(): number { return this.incidents.reduce((total, incident) => total + (incident.penaltyPoints || 0), 0); } /** * Get incidents by type */ getIncidentsByType(type: IncidentType): IncidentRecord[] { return this.incidents.filter(incident => incident.type === type); } /** * Check if driver had any incidents */ hasIncidents(): boolean { return this.incidents.length > 0; } /** * Check if driver had a clean race (no incidents) */ isClean(): boolean { return this.incidents.length === 0; } /** * Get incident severity score (0-100, higher = more severe) */ getSeverityScore(): number { if (this.incidents.length === 0) return 0; const severityWeights: Record = { track_limits: 10, contact: 20, unsafe_rejoin: 25, aggressive_driving: 15, false_start: 30, collision: 40, spin: 35, mechanical: 5, // Lower weight as it's not driver error other: 15, }; const totalSeverity = this.incidents.reduce((total, incident) => { return total + severityWeights[incident.type]; }, 0); // Normalize to 0-100 scale (cap at 100 for very incident-heavy races) return Math.min(100, totalSeverity); } /** * Get human-readable incident summary */ getSummary(): string { if (this.incidents.length === 0) { return 'Clean race'; } const typeCounts = this.incidents.reduce((counts, incident) => { counts[incident.type] = (counts[incident.type] || 0) + 1; return counts; }, {} as Record); const summaryParts = Object.entries(typeCounts).map(([type, count]) => { const typeLabel = this.getIncidentTypeLabel(type as IncidentType); return count > 1 ? `${count}x ${typeLabel}` : typeLabel; }); return summaryParts.join(', '); } /** * Get human-readable label for incident type */ private getIncidentTypeLabel(type: IncidentType): string { const labels: Record = { track_limits: 'Track Limits', contact: 'Contact', unsafe_rejoin: 'Unsafe Rejoin', aggressive_driving: 'Aggressive Driving', false_start: 'False Start', collision: 'Collision', spin: 'Spin', mechanical: 'Mechanical', other: 'Other', }; return labels[type]; } equals(other: IValueObject): boolean { const otherIncidents = other.props; if (this.incidents.length !== otherIncidents.length) { return false; } // Sort both arrays and compare const sortedThis = [...this.incidents].sort((a, b) => a.lap - b.lap); const sortedOther = [...otherIncidents].sort((a, b) => a.lap - b.lap); return sortedThis.every((incident, index) => { const otherIncident = sortedOther[index]; return incident.type === otherIncident.type && incident.lap === otherIncident.lap && incident.description === otherIncident.description && incident.penaltyPoints === otherIncident.penaltyPoints; }); } /** * Create RaceIncidents from legacy incidents count */ static fromLegacyIncidentsCount(count: number): RaceIncidents { if (count === 0) { return new RaceIncidents(); } // Distribute legacy incidents across different types based on probability const incidents: IncidentRecord[] = []; for (let i = 0; i < count; i++) { const type = RaceIncidents.getRandomIncidentType(); incidents.push({ type, lap: Math.floor(Math.random() * 20) + 1, // Random lap 1-20 penaltyPoints: RaceIncidents.getDefaultPenaltyPoints(type), }); } return new RaceIncidents(incidents); } /** * Get random incident type for legacy data conversion */ private static getRandomIncidentType(): IncidentType { const types: IncidentType[] = [ 'track_limits', 'contact', 'unsafe_rejoin', 'aggressive_driving', 'collision', 'spin', 'other' ]; const weights = [0.4, 0.25, 0.15, 0.1, 0.05, 0.04, 0.01]; // Probability weights const random = Math.random(); let cumulativeWeight = 0; for (let i = 0; i < types.length; i++) { cumulativeWeight += weights[i]; if (random <= cumulativeWeight) { return types[i]; } } return 'other'; } /** * Get default penalty points for incident type */ private static getDefaultPenaltyPoints(type: IncidentType): number { const penalties: Record = { track_limits: 0, // Usually just a warning contact: 2, unsafe_rejoin: 3, aggressive_driving: 2, false_start: 5, collision: 5, spin: 0, // Usually no penalty if no contact mechanical: 0, other: 2, }; return penalties[type]; } }