This commit is contained in:
2025-12-05 12:24:38 +01:00
parent fb509607c1
commit 5a9cd28d5b
47 changed files with 5456 additions and 228 deletions

View File

@@ -0,0 +1,45 @@
import {
getLeagueScoringPresetById,
listLeagueScoringPresets,
} from './InMemoryScoringRepositories';
import type {
LeagueScoringPresetDTO,
LeagueScoringPresetProvider,
} from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
/**
* Infrastructure adapter exposing the in-memory scoring preset registry
* through the LeagueScoringPresetProvider application port.
*/
export class InMemoryLeagueScoringPresetProvider
implements LeagueScoringPresetProvider
{
listPresets(): LeagueScoringPresetDTO[] {
return listLeagueScoringPresets().map((preset) => ({
id: preset.id,
name: preset.name,
description: preset.description,
primaryChampionshipType: preset.primaryChampionshipType,
sessionSummary: preset.sessionSummary,
bonusSummary: preset.bonusSummary,
dropPolicySummary: preset.dropPolicySummary,
}));
}
getPresetById(id: string): LeagueScoringPresetDTO | undefined {
const preset = getLeagueScoringPresetById(id);
if (!preset) {
return undefined;
}
return {
id: preset.id,
name: preset.name,
description: preset.description,
primaryChampionshipType: preset.primaryChampionshipType,
sessionSummary: preset.sessionSummary,
bonusSummary: preset.bonusSummary,
dropPolicySummary: preset.dropPolicySummary,
};
}
}

View File

@@ -14,6 +14,238 @@ import { ChampionshipStanding } from '@gridpilot/racing/domain/entities/Champion
import type { ChampionshipType } from '@gridpilot/racing/domain/value-objects/ChampionshipType';
import type { ParticipantRef } from '@gridpilot/racing/domain/value-objects/ParticipantRef';
export type LeagueScoringPresetPrimaryChampionshipType =
| 'driver'
| 'team'
| 'nations'
| 'trophy';
export interface LeagueScoringPreset {
id: string;
name: string;
description: string;
primaryChampionshipType: LeagueScoringPresetPrimaryChampionshipType;
dropPolicySummary: string;
sessionSummary: string;
bonusSummary: string;
createConfig: (options: { seasonId: string }) => LeagueScoringConfig;
}
const mainPointsSprintMain = new PointsTable({
1: 25,
2: 18,
3: 15,
4: 12,
5: 10,
6: 8,
7: 6,
8: 4,
9: 2,
10: 1,
});
const sprintPointsSprintMain = new PointsTable({
1: 8,
2: 7,
3: 6,
4: 5,
5: 4,
6: 3,
7: 2,
8: 1,
});
const clubMainPoints = new PointsTable({
1: 20,
2: 15,
3: 12,
4: 10,
5: 8,
6: 6,
7: 4,
8: 2,
9: 1,
});
const enduranceMainPoints = new PointsTable({
1: 50,
2: 36,
3: 30,
4: 24,
5: 20,
6: 16,
7: 12,
8: 8,
9: 4,
10: 2,
});
const leagueScoringPresets: LeagueScoringPreset[] = [
{
id: 'sprint-main-driver',
name: 'Sprint + Main',
description:
'Short sprint race plus main race; sprint gives fewer points.',
primaryChampionshipType: 'driver',
dropPolicySummary: 'Best 6 results of 8 count towards the championship.',
sessionSummary: 'Sprint + Main',
bonusSummary: 'Fastest lap +1 point in main race if finishing P10 or better.',
createConfig: ({ seasonId }) => {
const fastestLapBonus: BonusRule = {
id: 'fastest-lap-main',
type: 'fastestLap',
points: 1,
requiresFinishInTopN: 10,
};
const sessionTypes: SessionType[] = ['sprint', 'main'];
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
sprint: sprintPointsSprintMain,
main: mainPointsSprintMain,
practice: new PointsTable({}),
qualifying: new PointsTable({}),
q1: new PointsTable({}),
q2: new PointsTable({}),
q3: new PointsTable({}),
timeTrial: new PointsTable({}),
};
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {
sprint: [],
main: [fastestLapBonus],
practice: [],
qualifying: [],
q1: [],
q2: [],
q3: [],
timeTrial: [],
};
const dropScorePolicy: DropScorePolicy = {
strategy: 'bestNResults',
count: 6,
};
const championship: ChampionshipConfig = {
id: 'driver-champ-sprint-main',
name: 'Driver Championship',
type: 'driver' as ChampionshipType,
sessionTypes,
pointsTableBySessionType,
bonusRulesBySessionType,
dropScorePolicy,
};
return {
id: `lsc-${seasonId}-sprint-main-driver`,
seasonId,
scoringPresetId: 'sprint-main-driver',
championships: [championship],
};
},
},
{
id: 'club-default',
name: 'Club ladder',
description:
'Simple club ladder with a single main race and no bonuses or drop scores.',
primaryChampionshipType: 'driver',
dropPolicySummary: 'All race results count, no drop scores.',
sessionSummary: 'Main race only',
bonusSummary: 'No bonus points.',
createConfig: ({ seasonId }) => {
const sessionTypes: SessionType[] = ['main'];
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
sprint: new PointsTable({}),
main: clubMainPoints,
practice: new PointsTable({}),
qualifying: new PointsTable({}),
q1: new PointsTable({}),
q2: new PointsTable({}),
q3: new PointsTable({}),
timeTrial: new PointsTable({}),
};
const dropScorePolicy: DropScorePolicy = {
strategy: 'none',
};
const championship: ChampionshipConfig = {
id: 'driver-champ-club-default',
name: 'Driver Championship',
type: 'driver' as ChampionshipType,
sessionTypes,
pointsTableBySessionType,
dropScorePolicy,
};
return {
id: `lsc-${seasonId}-club-default`,
seasonId,
scoringPresetId: 'club-default',
championships: [championship],
};
},
},
{
id: 'endurance-main-double',
name: 'Endurance weekend',
description:
'Single main endurance race with double points and a simple drop policy.',
primaryChampionshipType: 'driver',
dropPolicySummary: 'Best 4 results of 6 count towards the championship.',
sessionSummary: 'Main race only',
bonusSummary: 'No bonus points.',
createConfig: ({ seasonId }) => {
const sessionTypes: SessionType[] = ['main'];
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
sprint: new PointsTable({}),
main: enduranceMainPoints,
practice: new PointsTable({}),
qualifying: new PointsTable({}),
q1: new PointsTable({}),
q2: new PointsTable({}),
q3: new PointsTable({}),
timeTrial: new PointsTable({}),
};
const dropScorePolicy: DropScorePolicy = {
strategy: 'bestNResults',
count: 4,
};
const championship: ChampionshipConfig = {
id: 'driver-champ-endurance-main-double',
name: 'Driver Championship',
type: 'driver' as ChampionshipType,
sessionTypes,
pointsTableBySessionType,
dropScorePolicy,
};
return {
id: `lsc-${seasonId}-endurance-main-double`,
seasonId,
scoringPresetId: 'endurance-main-double',
championships: [championship],
};
},
},
];
export function listLeagueScoringPresets(): LeagueScoringPreset[] {
return [...leagueScoringPresets];
}
export function getLeagueScoringPresetById(
id: string,
): LeagueScoringPreset | undefined {
return leagueScoringPresets.find((preset) => preset.id === id);
}
export class InMemoryGameRepository implements IGameRepository {
private games: Game[];
@@ -49,6 +281,11 @@ export class InMemorySeasonRepository implements ISeasonRepository {
return this.seasons.filter((s) => s.leagueId === leagueId);
}
async create(season: Season): Promise<Season> {
this.seasons.push(season);
return season;
}
seed(season: Season): void {
this.seasons.push(season);
}
@@ -67,6 +304,18 @@ export class InMemoryLeagueScoringConfigRepository
return this.configs.find((c) => c.seasonId === seasonId) ?? null;
}
async save(config: LeagueScoringConfig): Promise<LeagueScoringConfig> {
const existingIndex = this.configs.findIndex(
(c) => c.id === config.id,
);
if (existingIndex >= 0) {
this.configs[existingIndex] = config;
} else {
this.configs.push(config);
}
return config;
}
seed(config: LeagueScoringConfig): void {
this.configs.push(config);
}
@@ -99,7 +348,7 @@ export class InMemoryChampionshipStandingRepository
}
}
export function createF1DemoScoringSetup(params: {
export function createSprintMainDemoScoringSetup(params: {
leagueId: string;
seasonId?: string;
}): {
@@ -111,7 +360,7 @@ export function createF1DemoScoringSetup(params: {
championshipId: string;
} {
const { leagueId } = params;
const seasonId = params.seasonId ?? 'season-f1-demo';
const seasonId = params.seasonId ?? 'season-sprint-main-demo';
const championshipId = 'driver-champ';
const game = Game.create({ id: 'iracing', name: 'iRacing' });
@@ -120,7 +369,7 @@ export function createF1DemoScoringSetup(params: {
id: seasonId,
leagueId,
gameId: game.id,
name: 'F1-Style Demo Season',
name: 'Sprint + Main Demo Season',
year: 2025,
order: 1,
status: 'active',
@@ -128,81 +377,14 @@ export function createF1DemoScoringSetup(params: {
endDate: new Date('2025-12-31'),
});
const mainPoints = new PointsTable({
1: 25,
2: 18,
3: 15,
4: 12,
5: 10,
6: 8,
7: 6,
8: 4,
9: 2,
10: 1,
});
const preset = getLeagueScoringPresetById('sprint-main-driver');
if (!preset) {
throw new Error('Missing sprint-main-driver scoring preset');
}
const sprintPoints = new PointsTable({
1: 8,
2: 7,
3: 6,
4: 5,
5: 4,
6: 3,
7: 2,
8: 1,
});
const fastestLapBonus: BonusRule = {
id: 'fastest-lap-main',
type: 'fastestLap',
points: 1,
requiresFinishInTopN: 10,
};
const sessionTypes: SessionType[] = ['sprint', 'main'];
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
sprint: sprintPoints,
main: mainPoints,
practice: new PointsTable({}),
qualifying: new PointsTable({}),
q1: new PointsTable({}),
q2: new PointsTable({}),
q3: new PointsTable({}),
timeTrial: new PointsTable({}),
};
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {
sprint: [],
main: [fastestLapBonus],
practice: [],
qualifying: [],
q1: [],
q2: [],
q3: [],
timeTrial: [],
};
const dropScorePolicy: DropScorePolicy = {
strategy: 'bestNResults',
count: 6,
};
const championship: ChampionshipConfig = {
id: championshipId,
name: 'Driver Championship',
type: 'driver' as ChampionshipType,
sessionTypes,
pointsTableBySessionType,
bonusRulesBySessionType,
dropScorePolicy,
};
const leagueScoringConfig: LeagueScoringConfig = {
id: 'lsc-f1-demo',
const leagueScoringConfig: LeagueScoringConfig = preset.createConfig({
seasonId: season.id,
championships: [championship],
};
});
const gameRepo = new InMemoryGameRepository([game]);
const seasonRepo = new InMemorySeasonRepository([season]);