188 lines
7.6 KiB
TypeScript
188 lines
7.6 KiB
TypeScript
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
|
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
|
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
|
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
|
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
|
|
import { Result } from '../../domain/entities/Result';
|
|
import { Standing } from '../../domain/entities/Standing';
|
|
import type { AsyncUseCase } from '@core/shared/application';
|
|
import type { Logger } from '@core/shared/application';
|
|
|
|
/**
|
|
* Use Case: CompleteRaceUseCase
|
|
*
|
|
* Encapsulates the workflow for completing a race:
|
|
* - loads the race by id
|
|
* - throws if the race does not exist
|
|
* - delegates completion rules to the Race domain entity
|
|
* - automatically generates realistic results for registered drivers
|
|
* - updates league standings
|
|
* - persists all changes via repositories.
|
|
*/
|
|
export interface CompleteRaceCommandDTO {
|
|
raceId: string;
|
|
}
|
|
|
|
export class CompleteRaceUseCase
|
|
implements AsyncUseCase<CompleteRaceCommandDTO, void> {
|
|
constructor(
|
|
private readonly raceRepository: IRaceRepository,
|
|
private readonly raceRegistrationRepository: IRaceRegistrationRepository,
|
|
private readonly resultRepository: IResultRepository,
|
|
private readonly standingRepository: IStandingRepository,
|
|
private readonly driverRatingProvider: DriverRatingProvider,
|
|
private readonly logger: Logger,
|
|
) {}
|
|
|
|
async execute(command: CompleteRaceCommandDTO): Promise<void> {
|
|
this.logger.debug(`Executing CompleteRaceUseCase for raceId: ${command.raceId}`);
|
|
const { raceId } = command;
|
|
|
|
try {
|
|
const race = await this.raceRepository.findById(raceId);
|
|
if (!race) {
|
|
this.logger.error(`Race with id ${raceId} not found.`);
|
|
throw new Error('Race not found');
|
|
}
|
|
this.logger.debug(`Race ${raceId} found. Status: ${race.status}`);
|
|
|
|
// Get registered drivers for this race
|
|
const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId);
|
|
if (registeredDriverIds.length === 0) {
|
|
this.logger.warn(`No registered drivers found for race ${raceId}.`);
|
|
throw new Error('Cannot complete race with no registered drivers');
|
|
}
|
|
this.logger.info(`${registeredDriverIds.length} drivers registered for race ${raceId}. Generating results.`);
|
|
|
|
// Get driver ratings
|
|
const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds);
|
|
this.logger.debug(`Driver ratings fetched for ${registeredDriverIds.length} drivers.`);
|
|
|
|
// Generate realistic race results
|
|
const results = this.generateRaceResults(raceId, registeredDriverIds, driverRatings);
|
|
this.logger.debug(`Generated ${results.length} race results for race ${raceId}.`);
|
|
|
|
// Save results
|
|
for (const result of results) {
|
|
await this.resultRepository.create(result);
|
|
}
|
|
this.logger.info(`Persisted ${results.length} race results for race ${raceId}.`);
|
|
|
|
// Update standings
|
|
await this.updateStandings(race.leagueId, results);
|
|
this.logger.info(`Standings updated for league ${race.leagueId}.`);
|
|
|
|
// Complete the race
|
|
const completedRace = race.complete();
|
|
await this.raceRepository.update(completedRace);
|
|
this.logger.info(`Race ${raceId} successfully completed and updated.`);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to complete race ${raceId}: ${error.message}`, error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private generateRaceResults(
|
|
raceId: string,
|
|
driverIds: string[],
|
|
driverRatings: Map<string, number>
|
|
): Result[] {
|
|
this.logger.debug(`Generating race results for race ${raceId} with ${driverIds.length} drivers.`);
|
|
// 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
|
|
});
|
|
this.logger.debug(`Driver performances sorted for race ${raceId}.`);
|
|
|
|
// 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;
|
|
});
|
|
this.logger.debug(`Qualifying performances generated for race ${raceId}.`);
|
|
|
|
// 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 incidents (0-3, higher for lower positions)
|
|
const incidentProbability = Math.min(0.8, position / driverPerformances.length);
|
|
const incidents = Math.random() < incidentProbability ? Math.floor(Math.random() * 3) + 1 : 0;
|
|
|
|
results.push(
|
|
Result.create({
|
|
id: `${raceId}-${driverId}`,
|
|
raceId,
|
|
driverId,
|
|
position,
|
|
startPosition,
|
|
fastestLap,
|
|
incidents,
|
|
})
|
|
);
|
|
}
|
|
this.logger.debug(`Individual results created for race ${raceId}.`);
|
|
|
|
return results;
|
|
}
|
|
|
|
private async updateStandings(leagueId: string, results: Result[]): Promise<void> {
|
|
this.logger.debug(`Updating standings for league ${leagueId} with ${results.length} results.`);
|
|
// Group results by driver
|
|
const resultsByDriver = new Map<string, Result[]>();
|
|
for (const result of results) {
|
|
const existing = resultsByDriver.get(result.driverId) || [];
|
|
existing.push(result);
|
|
resultsByDriver.set(result.driverId, existing);
|
|
}
|
|
this.logger.debug(`Results grouped by driver for league ${leagueId}.`);
|
|
|
|
// Update or create standings for each driver
|
|
for (const [driverId, driverResults] of resultsByDriver) {
|
|
let standing = await this.standingRepository.findByDriverIdAndLeagueId(driverId, leagueId);
|
|
|
|
if (!standing) {
|
|
standing = Standing.create({
|
|
leagueId,
|
|
driverId,
|
|
});
|
|
this.logger.debug(`Created new standing for driver ${driverId} in league ${leagueId}.`);
|
|
} else {
|
|
this.logger.debug(`Found existing standing for driver ${driverId} in league ${leagueId}.`);
|
|
}
|
|
|
|
// Add all results for this driver (should be just one for this race)
|
|
for (const result of driverResults) {
|
|
standing = standing.addRaceResult(result.position, {
|
|
1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1
|
|
});
|
|
}
|
|
|
|
await this.standingRepository.save(standing);
|
|
this.logger.debug(`Standing saved for driver ${driverId} in league ${leagueId}.`);
|
|
}
|
|
this.logger.info(`Standings update complete for league ${leagueId}.`);
|
|
}
|
|
} |