wip
This commit is contained in:
41
packages/racing/domain/entities/ChampionshipStanding.ts
Normal file
41
packages/racing/domain/entities/ChampionshipStanding.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ParticipantRef } from '../value-objects/ParticipantRef';
|
||||
|
||||
export class ChampionshipStanding {
|
||||
readonly seasonId: string;
|
||||
readonly championshipId: string;
|
||||
readonly participant: ParticipantRef;
|
||||
readonly totalPoints: number;
|
||||
readonly resultsCounted: number;
|
||||
readonly resultsDropped: number;
|
||||
readonly position: number;
|
||||
|
||||
constructor(props: {
|
||||
seasonId: string;
|
||||
championshipId: string;
|
||||
participant: ParticipantRef;
|
||||
totalPoints: number;
|
||||
resultsCounted: number;
|
||||
resultsDropped: number;
|
||||
position: number;
|
||||
}) {
|
||||
this.seasonId = props.seasonId;
|
||||
this.championshipId = props.championshipId;
|
||||
this.participant = props.participant;
|
||||
this.totalPoints = props.totalPoints;
|
||||
this.resultsCounted = props.resultsCounted;
|
||||
this.resultsDropped = props.resultsDropped;
|
||||
this.position = props.position;
|
||||
}
|
||||
|
||||
withPosition(position: number): ChampionshipStanding {
|
||||
return new ChampionshipStanding({
|
||||
seasonId: this.seasonId,
|
||||
championshipId: this.championshipId,
|
||||
participant: this.participant,
|
||||
totalPoints: this.totalPoints,
|
||||
resultsCounted: this.resultsCounted,
|
||||
resultsDropped: this.resultsDropped,
|
||||
position,
|
||||
});
|
||||
}
|
||||
}
|
||||
24
packages/racing/domain/entities/Game.ts
Normal file
24
packages/racing/domain/entities/Game.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export class Game {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
|
||||
private constructor(props: { id: string; name: string }) {
|
||||
this.id = props.id;
|
||||
this.name = props.name;
|
||||
}
|
||||
|
||||
static create(props: { id: string; name: string }): Game {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('Game ID is required');
|
||||
}
|
||||
|
||||
if (!props.name || props.name.trim().length === 0) {
|
||||
throw new Error('Game name is required');
|
||||
}
|
||||
|
||||
return new Game({
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,17 @@ export interface LeagueSettings {
|
||||
sessionDuration?: number;
|
||||
qualifyingFormat?: 'single-lap' | 'open';
|
||||
customPoints?: Record<number, number>;
|
||||
/**
|
||||
* Maximum number of drivers allowed in the league.
|
||||
* Used for simple capacity display on the website.
|
||||
*/
|
||||
maxDrivers?: number;
|
||||
}
|
||||
|
||||
export interface LeagueSocialLinks {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
}
|
||||
|
||||
export class League {
|
||||
@@ -19,6 +30,7 @@ export class League {
|
||||
readonly ownerId: string;
|
||||
readonly settings: LeagueSettings;
|
||||
readonly createdAt: Date;
|
||||
readonly socialLinks?: LeagueSocialLinks;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
@@ -27,6 +39,7 @@ export class League {
|
||||
ownerId: string;
|
||||
settings: LeagueSettings;
|
||||
createdAt: Date;
|
||||
socialLinks?: LeagueSocialLinks;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
this.name = props.name;
|
||||
@@ -34,6 +47,7 @@ export class League {
|
||||
this.ownerId = props.ownerId;
|
||||
this.settings = props.settings;
|
||||
this.createdAt = props.createdAt;
|
||||
this.socialLinks = props.socialLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,6 +60,7 @@ export class League {
|
||||
ownerId: string;
|
||||
settings?: Partial<LeagueSettings>;
|
||||
createdAt?: Date;
|
||||
socialLinks?: LeagueSocialLinks;
|
||||
}): League {
|
||||
this.validate(props);
|
||||
|
||||
@@ -53,6 +68,7 @@ export class League {
|
||||
pointsSystem: 'f1-2024',
|
||||
sessionDuration: 60,
|
||||
qualifyingFormat: 'open',
|
||||
maxDrivers: 32,
|
||||
};
|
||||
|
||||
return new League({
|
||||
@@ -62,6 +78,7 @@ export class League {
|
||||
ownerId: props.ownerId,
|
||||
settings: { ...defaultSettings, ...props.settings },
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
socialLinks: props.socialLinks,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,6 +119,7 @@ export class League {
|
||||
name: string;
|
||||
description: string;
|
||||
settings: LeagueSettings;
|
||||
socialLinks: LeagueSocialLinks | undefined;
|
||||
}>): League {
|
||||
return new League({
|
||||
id: this.id,
|
||||
@@ -110,6 +128,7 @@ export class League {
|
||||
ownerId: this.ownerId,
|
||||
settings: props.settings ?? this.settings,
|
||||
createdAt: this.createdAt,
|
||||
socialLinks: props.socialLinks ?? this.socialLinks,
|
||||
});
|
||||
}
|
||||
}
|
||||
7
packages/racing/domain/entities/LeagueScoringConfig.ts
Normal file
7
packages/racing/domain/entities/LeagueScoringConfig.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ChampionshipConfig } from '../value-objects/ChampionshipConfig';
|
||||
|
||||
export interface LeagueScoringConfig {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
championships: ChampionshipConfig[];
|
||||
}
|
||||
29
packages/racing/domain/entities/Penalty.ts
Normal file
29
packages/racing/domain/entities/Penalty.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Domain Entity: Penalty
|
||||
*
|
||||
* Represents a season-long penalty or bonus applied to a driver
|
||||
* within a specific league. This is intentionally simple for the
|
||||
* alpha demo and models points adjustments only.
|
||||
*/
|
||||
export type PenaltyType = 'points-deduction' | 'points-bonus';
|
||||
|
||||
export interface Penalty {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
type: PenaltyType;
|
||||
/**
|
||||
* Signed integer representing points adjustment:
|
||||
* - negative for deductions
|
||||
* - positive for bonuses
|
||||
*/
|
||||
pointsDelta: number;
|
||||
/**
|
||||
* Optional short reason/label (e.g. "Incident penalty", "Fastest laps bonus").
|
||||
*/
|
||||
reason?: string;
|
||||
/**
|
||||
* When this penalty was applied.
|
||||
*/
|
||||
appliedAt: Date;
|
||||
}
|
||||
77
packages/racing/domain/entities/Season.ts
Normal file
77
packages/racing/domain/entities/Season.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
export type SeasonStatus = 'planned' | 'active' | 'completed';
|
||||
|
||||
export class Season {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly gameId: string;
|
||||
readonly name: string;
|
||||
readonly year?: number;
|
||||
readonly order?: number;
|
||||
readonly status: SeasonStatus;
|
||||
readonly startDate?: Date;
|
||||
readonly endDate?: Date;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
gameId: string;
|
||||
name: string;
|
||||
year?: number;
|
||||
order?: number;
|
||||
status: SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
this.leagueId = props.leagueId;
|
||||
this.gameId = props.gameId;
|
||||
this.name = props.name;
|
||||
this.year = props.year;
|
||||
this.order = props.order;
|
||||
this.status = props.status;
|
||||
this.startDate = props.startDate;
|
||||
this.endDate = props.endDate;
|
||||
}
|
||||
|
||||
static create(props: {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
gameId: string;
|
||||
name: string;
|
||||
year?: number;
|
||||
order?: number;
|
||||
status?: SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}): Season {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('Season ID is required');
|
||||
}
|
||||
|
||||
if (!props.leagueId || props.leagueId.trim().length === 0) {
|
||||
throw new Error('Season leagueId is required');
|
||||
}
|
||||
|
||||
if (!props.gameId || props.gameId.trim().length === 0) {
|
||||
throw new Error('Season gameId is required');
|
||||
}
|
||||
|
||||
if (!props.name || props.name.trim().length === 0) {
|
||||
throw new Error('Season name is required');
|
||||
}
|
||||
|
||||
const status: SeasonStatus = props.status ?? 'planned';
|
||||
|
||||
return new Season({
|
||||
id: props.id,
|
||||
leagueId: props.leagueId,
|
||||
gameId: props.gameId,
|
||||
name: props.name,
|
||||
year: props.year,
|
||||
order: props.order,
|
||||
status,
|
||||
startDate: props.startDate,
|
||||
endDate: props.endDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ChampionshipStanding } from '../entities/ChampionshipStanding';
|
||||
|
||||
export interface IChampionshipStandingRepository {
|
||||
findBySeasonAndChampionship(
|
||||
seasonId: string,
|
||||
championshipId: string,
|
||||
): Promise<ChampionshipStanding[]>;
|
||||
|
||||
saveAll(standings: ChampionshipStanding[]): Promise<void>;
|
||||
}
|
||||
6
packages/racing/domain/repositories/IGameRepository.ts
Normal file
6
packages/racing/domain/repositories/IGameRepository.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Game } from '../entities/Game';
|
||||
|
||||
export interface IGameRepository {
|
||||
findById(id: string): Promise<Game | null>;
|
||||
findAll(): Promise<Game[]>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { LeagueScoringConfig } from '../entities/LeagueScoringConfig';
|
||||
|
||||
export interface ILeagueScoringConfigRepository {
|
||||
findBySeasonId(seasonId: string): Promise<LeagueScoringConfig | null>;
|
||||
}
|
||||
25
packages/racing/domain/repositories/IPenaltyRepository.ts
Normal file
25
packages/racing/domain/repositories/IPenaltyRepository.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Application Port: IPenaltyRepository
|
||||
*
|
||||
* Repository interface for season-long penalties and bonuses applied
|
||||
* to drivers within a league. This is intentionally simple for the
|
||||
* alpha demo and operates purely on in-memory data.
|
||||
*/
|
||||
import type { Penalty } from '../entities/Penalty';
|
||||
|
||||
export interface IPenaltyRepository {
|
||||
/**
|
||||
* Get all penalties for a given league.
|
||||
*/
|
||||
findByLeagueId(leagueId: string): Promise<Penalty[]>;
|
||||
|
||||
/**
|
||||
* Get all penalties for a driver in a specific league.
|
||||
*/
|
||||
findByLeagueIdAndDriverId(leagueId: string, driverId: string): Promise<Penalty[]>;
|
||||
|
||||
/**
|
||||
* Get all penalties in the system.
|
||||
*/
|
||||
findAll(): Promise<Penalty[]>;
|
||||
}
|
||||
6
packages/racing/domain/repositories/ISeasonRepository.ts
Normal file
6
packages/racing/domain/repositories/ISeasonRepository.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Season } from '../entities/Season';
|
||||
|
||||
export interface ISeasonRepository {
|
||||
findById(id: string): Promise<Season | null>;
|
||||
findByLeagueId(leagueId: string): Promise<Season[]>;
|
||||
}
|
||||
71
packages/racing/domain/services/ChampionshipAggregator.ts
Normal file
71
packages/racing/domain/services/ChampionshipAggregator.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ChampionshipConfig } from '../value-objects/ChampionshipConfig';
|
||||
import type { ParticipantRef } from '../value-objects/ParticipantRef';
|
||||
import { ChampionshipStanding } from '../entities/ChampionshipStanding';
|
||||
import type { ParticipantEventPoints } from './EventScoringService';
|
||||
import { DropScoreApplier, type EventPointsEntry } from './DropScoreApplier';
|
||||
|
||||
export class ChampionshipAggregator {
|
||||
constructor(private readonly dropScoreApplier: DropScoreApplier) {}
|
||||
|
||||
aggregate(params: {
|
||||
seasonId: string;
|
||||
championship: ChampionshipConfig;
|
||||
eventPointsByEventId: Record<string, ParticipantEventPoints[]>;
|
||||
}): ChampionshipStanding[] {
|
||||
const { seasonId, championship, eventPointsByEventId } = params;
|
||||
|
||||
const perParticipantEvents = new Map<
|
||||
string,
|
||||
{ participant: ParticipantRef; events: EventPointsEntry[] }
|
||||
>();
|
||||
|
||||
for (const [eventId, pointsList] of Object.entries(eventPointsByEventId)) {
|
||||
for (const entry of pointsList) {
|
||||
const key = entry.participant.id;
|
||||
const existing = perParticipantEvents.get(key);
|
||||
const eventEntry: EventPointsEntry = {
|
||||
eventId,
|
||||
points: entry.totalPoints,
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
existing.events.push(eventEntry);
|
||||
} else {
|
||||
perParticipantEvents.set(key, {
|
||||
participant: entry.participant,
|
||||
events: [eventEntry],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const standings: ChampionshipStanding[] = [];
|
||||
|
||||
for (const { participant, events } of perParticipantEvents.values()) {
|
||||
const dropResult = this.dropScoreApplier.apply(
|
||||
championship.dropScorePolicy,
|
||||
events,
|
||||
);
|
||||
|
||||
const totalPoints = dropResult.totalPoints;
|
||||
const resultsCounted = dropResult.counted.length;
|
||||
const resultsDropped = dropResult.dropped.length;
|
||||
|
||||
standings.push(
|
||||
new ChampionshipStanding({
|
||||
seasonId,
|
||||
championshipId: championship.id,
|
||||
participant,
|
||||
totalPoints,
|
||||
resultsCounted,
|
||||
resultsDropped,
|
||||
position: 0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
standings.sort((a, b) => b.totalPoints - a.totalPoints);
|
||||
|
||||
return standings.map((s, index) => s.withPosition(index + 1));
|
||||
}
|
||||
}
|
||||
56
packages/racing/domain/services/DropScoreApplier.ts
Normal file
56
packages/racing/domain/services/DropScoreApplier.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { DropScorePolicy } from '../value-objects/DropScorePolicy';
|
||||
|
||||
export interface EventPointsEntry {
|
||||
eventId: string;
|
||||
points: number;
|
||||
}
|
||||
|
||||
export interface DropScoreResult {
|
||||
counted: EventPointsEntry[];
|
||||
dropped: EventPointsEntry[];
|
||||
totalPoints: number;
|
||||
}
|
||||
|
||||
export class DropScoreApplier {
|
||||
apply(policy: DropScorePolicy, events: EventPointsEntry[]): DropScoreResult {
|
||||
if (policy.strategy === 'none' || events.length === 0) {
|
||||
const totalPoints = events.reduce((sum, e) => sum + e.points, 0);
|
||||
return {
|
||||
counted: [...events],
|
||||
dropped: [],
|
||||
totalPoints,
|
||||
};
|
||||
}
|
||||
|
||||
if (policy.strategy === 'bestNResults') {
|
||||
const count = policy.count ?? events.length;
|
||||
if (count >= events.length) {
|
||||
const totalPoints = events.reduce((sum, e) => sum + e.points, 0);
|
||||
return {
|
||||
counted: [...events],
|
||||
dropped: [],
|
||||
totalPoints,
|
||||
};
|
||||
}
|
||||
|
||||
const sorted = [...events].sort((a, b) => b.points - a.points);
|
||||
const counted = sorted.slice(0, count);
|
||||
const dropped = sorted.slice(count);
|
||||
const totalPoints = counted.reduce((sum, e) => sum + e.points, 0);
|
||||
|
||||
return {
|
||||
counted,
|
||||
dropped,
|
||||
totalPoints,
|
||||
};
|
||||
}
|
||||
|
||||
// For this slice, treat unsupported strategies as 'none'
|
||||
const totalPoints = events.reduce((sum, e) => sum + e.points, 0);
|
||||
return {
|
||||
counted: [...events],
|
||||
dropped: [],
|
||||
totalPoints,
|
||||
};
|
||||
}
|
||||
}
|
||||
128
packages/racing/domain/services/EventScoringService.ts
Normal file
128
packages/racing/domain/services/EventScoringService.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { ChampionshipConfig } from '../value-objects/ChampionshipConfig';
|
||||
import type { SessionType } from '../value-objects/SessionType';
|
||||
import type { ParticipantRef } from '../value-objects/ParticipantRef';
|
||||
import type { Result } from '../entities/Result';
|
||||
import type { Penalty } from '../entities/Penalty';
|
||||
import type { BonusRule } from '../value-objects/BonusRule';
|
||||
import type { ChampionshipType } from '../value-objects/ChampionshipType';
|
||||
|
||||
import type { PointsTable } from '../value-objects/PointsTable';
|
||||
|
||||
export interface ParticipantEventPoints {
|
||||
participant: ParticipantRef;
|
||||
basePoints: number;
|
||||
bonusPoints: number;
|
||||
penaltyPoints: number;
|
||||
totalPoints: number;
|
||||
}
|
||||
|
||||
function createDriverParticipant(driverId: string): ParticipantRef {
|
||||
return {
|
||||
type: 'driver' as ChampionshipType,
|
||||
id: driverId,
|
||||
};
|
||||
}
|
||||
|
||||
export class EventScoringService {
|
||||
scoreSession(params: {
|
||||
seasonId: string;
|
||||
championship: ChampionshipConfig;
|
||||
sessionType: SessionType;
|
||||
results: Result[];
|
||||
penalties: Penalty[];
|
||||
}): 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[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];
|
||||
|
||||
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) {
|
||||
const current = map.get(penalty.driverId) ?? 0;
|
||||
map.set(penalty.driverId, current + penalty.pointsDelta);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
8
packages/racing/domain/value-objects/BonusRule.ts
Normal file
8
packages/racing/domain/value-objects/BonusRule.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type BonusRuleType = 'fastestLap' | 'polePosition' | 'mostPositionsGained';
|
||||
|
||||
export interface BonusRule {
|
||||
id: string;
|
||||
type: BonusRuleType;
|
||||
points: number;
|
||||
requiresFinishInTopN?: number;
|
||||
}
|
||||
15
packages/racing/domain/value-objects/ChampionshipConfig.ts
Normal file
15
packages/racing/domain/value-objects/ChampionshipConfig.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ChampionshipType } from './ChampionshipType';
|
||||
import type { SessionType } from './SessionType';
|
||||
import { PointsTable } from './PointsTable';
|
||||
import type { BonusRule } from './BonusRule';
|
||||
import type { DropScorePolicy } from './DropScorePolicy';
|
||||
|
||||
export interface ChampionshipConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ChampionshipType;
|
||||
sessionTypes: SessionType[];
|
||||
pointsTableBySessionType: Record<SessionType, PointsTable>;
|
||||
bonusRulesBySessionType?: Record<SessionType, BonusRule[]>;
|
||||
dropScorePolicy: DropScorePolicy;
|
||||
}
|
||||
1
packages/racing/domain/value-objects/ChampionshipType.ts
Normal file
1
packages/racing/domain/value-objects/ChampionshipType.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type ChampionshipType = 'driver' | 'team' | 'nations' | 'trophy';
|
||||
13
packages/racing/domain/value-objects/DropScorePolicy.ts
Normal file
13
packages/racing/domain/value-objects/DropScorePolicy.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type DropScoreStrategy = 'none' | 'bestNResults' | 'dropWorstN';
|
||||
|
||||
export interface DropScorePolicy {
|
||||
strategy: DropScoreStrategy;
|
||||
/**
|
||||
* For 'bestNResults': number of best-scoring events to count.
|
||||
*/
|
||||
count?: number;
|
||||
/**
|
||||
* For 'dropWorstN': number of worst-scoring events to drop.
|
||||
*/
|
||||
dropCount?: number;
|
||||
}
|
||||
6
packages/racing/domain/value-objects/ParticipantRef.ts
Normal file
6
packages/racing/domain/value-objects/ParticipantRef.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ChampionshipType } from './ChampionshipType';
|
||||
|
||||
export interface ParticipantRef {
|
||||
type: ChampionshipType;
|
||||
id: string;
|
||||
}
|
||||
21
packages/racing/domain/value-objects/PointsTable.ts
Normal file
21
packages/racing/domain/value-objects/PointsTable.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export class PointsTable {
|
||||
private readonly pointsByPosition: Map<number, number>;
|
||||
|
||||
constructor(pointsByPosition: Record<number, number> | Map<number, number>) {
|
||||
if (pointsByPosition instanceof Map) {
|
||||
this.pointsByPosition = new Map(pointsByPosition);
|
||||
} else {
|
||||
this.pointsByPosition = new Map(
|
||||
Object.entries(pointsByPosition).map(([key, value]) => [Number(key), value]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getPointsForPosition(position: number): number {
|
||||
if (!Number.isInteger(position) || position < 1) {
|
||||
return 0;
|
||||
}
|
||||
const value = this.pointsByPosition.get(position);
|
||||
return typeof value === 'number' ? value : 0;
|
||||
}
|
||||
}
|
||||
9
packages/racing/domain/value-objects/SessionType.ts
Normal file
9
packages/racing/domain/value-objects/SessionType.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type SessionType =
|
||||
| 'practice'
|
||||
| 'qualifying'
|
||||
| 'q1'
|
||||
| 'q2'
|
||||
| 'q3'
|
||||
| 'sprint'
|
||||
| 'main'
|
||||
| 'timeTrial';
|
||||
Reference in New Issue
Block a user