Files
gridpilot.gg/core/racing/domain/services/EventScoringService.ts
2025-12-16 11:52:26 +01:00

147 lines
4.6 KiB
TypeScript

import type { ChampionshipConfig } from '../types/ChampionshipConfig';
import type { SessionType } from '../types/SessionType';
import type { ParticipantRef } from '../types/ParticipantRef';
import type { Result } from '../entities/Result';
import type { Penalty } from '../entities/Penalty';
import type { BonusRule } from '../types/BonusRule';
import type { ChampionshipType } from '../types/ChampionshipType';
import type { PointsTable } from '../value-objects/PointsTable';
import type { IDomainCalculationService } from '@core/shared/domain';
export interface ParticipantEventPoints {
participant: ParticipantRef;
basePoints: number;
bonusPoints: number;
penaltyPoints: number;
totalPoints: number;
}
export interface EventScoringInput {
seasonId: string;
championship: ChampionshipConfig;
sessionType: SessionType;
results: Result[];
penalties: Penalty[];
}
function createDriverParticipant(driverId: string): ParticipantRef {
return {
type: 'driver' as ChampionshipType,
id: driverId,
};
}
export class EventScoringService
implements IDomainCalculationService<EventScoringInput, ParticipantEventPoints[]>
{
calculate(input: EventScoringInput): ParticipantEventPoints[] {
return this.scoreSession(input);
}
scoreSession(params: EventScoringInput): ParticipantEventPoints[] {
const { championship, sessionType, results } = params;
const pointsTable = this.getPointsTableForSession(championship, sessionType);
const bonusRules = this.getBonusRulesForSession(championship, sessionType);
const baseByDriver = new Map<string, number>();
const bonusByDriver = new Map<string, number>();
const penaltyByDriver = new Map<string, number>();
for (const result of results) {
const driverId = result.driverId;
const currentBase = baseByDriver.get(driverId) ?? 0;
const added = pointsTable.getPointsForPosition(result.position);
baseByDriver.set(driverId, currentBase + added);
}
const fastestLapRule = bonusRules.find((r) => r.type === 'fastestLap');
if (fastestLapRule) {
this.applyFastestLapBonus(fastestLapRule, results, bonusByDriver);
}
const penaltyMap = this.aggregatePenalties(params.penalties);
for (const [driverId, value] of penaltyMap.entries()) {
penaltyByDriver.set(driverId, value);
}
const allDriverIds = new Set<string>();
for (const id of baseByDriver.keys()) allDriverIds.add(id);
for (const id of bonusByDriver.keys()) allDriverIds.add(id);
for (const id of penaltyByDriver.keys()) allDriverIds.add(id);
const participants: ParticipantEventPoints[] = [];
for (const driverId of allDriverIds) {
const basePoints = baseByDriver.get(driverId) ?? 0;
const bonusPoints = bonusByDriver.get(driverId) ?? 0;
const penaltyPoints = penaltyByDriver.get(driverId) ?? 0;
const totalPoints = basePoints + bonusPoints - penaltyPoints;
participants.push({
participant: createDriverParticipant(driverId),
basePoints,
bonusPoints,
penaltyPoints,
totalPoints,
});
}
return participants;
}
private getPointsTableForSession(
championship: ChampionshipConfig,
sessionType: SessionType,
): PointsTable {
return championship.pointsTableBySessionType[sessionType];
}
private getBonusRulesForSession(
championship: ChampionshipConfig,
sessionType: SessionType,
): BonusRule[] {
const all = championship.bonusRulesBySessionType ?? {};
return (all as Record<SessionType, BonusRule[]>)[sessionType] ?? [];
}
private applyFastestLapBonus(
rule: BonusRule,
results: Result[],
bonusByDriver: Map<string, number>,
): void {
if (results.length === 0) return;
const sortedByLap = [...results].sort((a, b) => a.fastestLap - b.fastestLap);
const best = sortedByLap[0];
if (!best) {
return;
}
const requiresTop = rule.requiresFinishInTopN;
if (typeof requiresTop === 'number') {
if (best.position <= 0 || best.position > requiresTop) {
return;
}
}
const current = bonusByDriver.get(best.driverId) ?? 0;
bonusByDriver.set(best.driverId, current + rule.points);
}
private aggregatePenalties(penalties: Penalty[]): Map<string, number> {
const map = new Map<string, number>();
for (const penalty of penalties) {
// Only count applied points_deduction penalties
if (penalty.status !== 'applied' || penalty.type !== 'points_deduction') {
continue;
}
const current = map.get(penalty.driverId) ?? 0;
const delta = penalty.value ?? 0;
map.set(penalty.driverId, current + delta);
}
return map;
}
}