147 lines
4.7 KiB
TypeScript
147 lines
4.7 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/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.toString();
|
|
const currentBase = baseByDriver.get(driverId) ?? 0;
|
|
const added = pointsTable.getPointsForPosition(result.position.toNumber());
|
|
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.toNumber() - b.fastestLap.toNumber());
|
|
const best = sortedByLap[0];
|
|
|
|
if (!best) {
|
|
return;
|
|
}
|
|
|
|
const requiresTop = rule.requiresFinishInTopN;
|
|
if (typeof requiresTop === 'number') {
|
|
if (best.position.toNumber() <= 0 || best.position.toNumber() > requiresTop) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const current = bonusByDriver.get(best.driverId.toString()) ?? 0;
|
|
bonusByDriver.set(best.driverId.toString(), 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;
|
|
}
|
|
} |