Files
gridpilot.gg/core/racing/application/use-cases/DriverStatsUseCase.ts
2025-12-30 12:25:45 +01:00

113 lines
4.4 KiB
TypeScript

/**
* Application Use Case: DriverStatsUseCase
*
* Computes detailed driver statistics from race results and standings.
* Orchestrates repositories to provide stats data to presentation layer.
*/
import type { Logger } from '@core/shared/application';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { IDriverStatsUseCase, DriverStats } from './IDriverStatsUseCase';
export class DriverStatsUseCase implements IDriverStatsUseCase {
constructor(
private readonly resultRepository: IResultRepository,
private readonly standingRepository: IStandingRepository,
private readonly logger: Logger
) {
this.logger.info('[DriverStatsUseCase] Initialized with real data repositories');
}
async getDriverStats(driverId: string): Promise<DriverStats | null> {
this.logger.debug(`[DriverStatsUseCase] Computing stats for driver: ${driverId}`);
try {
// Get all results for this driver
const results = await this.resultRepository.findByDriverId(driverId);
if (results.length === 0) {
this.logger.warn(`[DriverStatsUseCase] No results found for driver: ${driverId}`);
return null;
}
// Get standings for context
const standings = await this.standingRepository.findAll();
const driverStanding = standings.find(s => s.driverId.toString() === driverId);
// Calculate basic stats from results
const wins = results.filter(r => r.position.toNumber() === 1).length;
const podiums = results.filter(r => r.position.toNumber() <= 3).length;
const dnfs = results.filter(r => r.position.toNumber() > 20).length;
const totalRaces = results.length;
const positions = results.map(r => r.position.toNumber());
const avgFinish = positions.reduce((sum, pos) => sum + pos, 0) / totalRaces;
const bestFinish = Math.min(...positions);
const worstFinish = Math.max(...positions);
// Calculate rating based on performance
let rating = 1000;
if (driverStanding) {
// Use standing-based rating
const pointsBonus = driverStanding.points.toNumber() * 2;
const positionBonus = Math.max(0, 50 - (driverStanding.position.toNumber() * 2));
const winBonus = driverStanding.wins * 100;
rating = Math.round(1000 + pointsBonus + positionBonus + winBonus);
} else {
// Calculate from results if no standing
const performanceBonus = ((totalRaces - wins) * 5) + ((totalRaces - podiums) * 2);
rating = Math.round(1000 + (wins * 100) + (podiums * 50) - performanceBonus);
}
// Calculate consistency (inverse of position variance)
const avgPosition = avgFinish;
const variance = positions.reduce((sum, pos) => sum + Math.pow(pos - avgPosition, 2), 0) / totalRaces;
const consistency = Math.round(Math.max(0, 100 - (variance * 2)));
// Safety rating (simplified - based on incidents)
const totalIncidents = results.reduce((sum, r) => sum + r.incidents.toNumber(), 0);
const safetyRating = Math.round(Math.max(0, 100 - (totalIncidents / totalRaces)));
// Sportsmanship rating (placeholder - could be based on penalties/protests)
const sportsmanshipRating = 4.5;
// Experience level
const experienceLevel = this.determineExperienceLevel(totalRaces);
// Overall rank
const overallRank = driverStanding ? driverStanding.position.toNumber() : null;
const stats: DriverStats = {
rating,
safetyRating,
sportsmanshipRating,
totalRaces,
wins,
podiums,
dnfs,
avgFinish: Math.round(avgFinish * 10) / 10,
bestFinish,
worstFinish,
consistency,
experienceLevel,
overallRank
};
this.logger.debug(`[DriverStatsUseCase] Computed stats for driver ${driverId}: rating=${stats.rating}, wins=${stats.wins}`);
return stats;
} catch (error) {
this.logger.error(`[DriverStatsUseCase] Error computing stats for driver ${driverId}:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
private determineExperienceLevel(totalRaces: number): string {
if (totalRaces >= 100) return 'Veteran';
if (totalRaces >= 50) return 'Experienced';
if (totalRaces >= 20) return 'Intermediate';
if (totalRaces >= 10) return 'Rookie';
return 'Beginner';
}
}