alpha wip

This commit is contained in:
2025-12-03 00:46:08 +01:00
parent 3b55fd1a63
commit 97e29d3d80
51 changed files with 6321 additions and 237 deletions

View File

@@ -0,0 +1,99 @@
/**
* Domain Entity: Driver
*
* Represents a driver profile in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
export class Driver {
readonly id: string;
readonly iracingId: string;
readonly name: string;
readonly country: string;
readonly bio?: string;
readonly joinedAt: Date;
private constructor(props: {
id: string;
iracingId: string;
name: string;
country: string;
bio?: string;
joinedAt: Date;
}) {
this.id = props.id;
this.iracingId = props.iracingId;
this.name = props.name;
this.country = props.country;
this.bio = props.bio;
this.joinedAt = props.joinedAt;
}
/**
* Factory method to create a new Driver entity
*/
static create(props: {
id: string;
iracingId: string;
name: string;
country: string;
bio?: string;
joinedAt?: Date;
}): Driver {
this.validate(props);
return new Driver({
...props,
joinedAt: props.joinedAt ?? new Date(),
});
}
/**
* Domain validation logic
*/
private static validate(props: {
id: string;
iracingId: string;
name: string;
country: string;
}): void {
if (!props.id || props.id.trim().length === 0) {
throw new Error('Driver ID is required');
}
if (!props.iracingId || props.iracingId.trim().length === 0) {
throw new Error('iRacing ID is required');
}
if (!props.name || props.name.trim().length === 0) {
throw new Error('Driver name is required');
}
if (!props.country || props.country.trim().length === 0) {
throw new Error('Country code is required');
}
// Validate ISO country code format (2-3 letters)
if (!/^[A-Z]{2,3}$/i.test(props.country)) {
throw new Error('Country must be a valid ISO code (2-3 letters)');
}
}
/**
* Create a copy with updated properties
*/
update(props: Partial<{
name: string;
country: string;
bio: string;
}>): Driver {
return new Driver({
id: this.id,
iracingId: this.iracingId,
name: props.name ?? this.name,
country: props.country ?? this.country,
bio: props.bio ?? this.bio,
joinedAt: this.joinedAt,
});
}
}

View File

@@ -0,0 +1,115 @@
/**
* Domain Entity: League
*
* Represents a league in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
export interface LeagueSettings {
pointsSystem: 'f1-2024' | 'indycar' | 'custom';
sessionDuration?: number;
qualifyingFormat?: 'single-lap' | 'open';
customPoints?: Record<number, number>;
}
export class League {
readonly id: string;
readonly name: string;
readonly description: string;
readonly ownerId: string;
readonly settings: LeagueSettings;
readonly createdAt: Date;
private constructor(props: {
id: string;
name: string;
description: string;
ownerId: string;
settings: LeagueSettings;
createdAt: Date;
}) {
this.id = props.id;
this.name = props.name;
this.description = props.description;
this.ownerId = props.ownerId;
this.settings = props.settings;
this.createdAt = props.createdAt;
}
/**
* Factory method to create a new League entity
*/
static create(props: {
id: string;
name: string;
description: string;
ownerId: string;
settings?: Partial<LeagueSettings>;
createdAt?: Date;
}): League {
this.validate(props);
const defaultSettings: LeagueSettings = {
pointsSystem: 'f1-2024',
sessionDuration: 60,
qualifyingFormat: 'open',
};
return new League({
id: props.id,
name: props.name,
description: props.description,
ownerId: props.ownerId,
settings: { ...defaultSettings, ...props.settings },
createdAt: props.createdAt ?? new Date(),
});
}
/**
* Domain validation logic
*/
private static validate(props: {
id: string;
name: string;
description: string;
ownerId: string;
}): void {
if (!props.id || props.id.trim().length === 0) {
throw new Error('League ID is required');
}
if (!props.name || props.name.trim().length === 0) {
throw new Error('League name is required');
}
if (props.name.length > 100) {
throw new Error('League name must be 100 characters or less');
}
if (!props.description || props.description.trim().length === 0) {
throw new Error('League description is required');
}
if (!props.ownerId || props.ownerId.trim().length === 0) {
throw new Error('League owner ID is required');
}
}
/**
* Create a copy with updated properties
*/
update(props: Partial<{
name: string;
description: string;
settings: LeagueSettings;
}>): League {
return new League({
id: this.id,
name: props.name ?? this.name,
description: props.description ?? this.description,
ownerId: this.ownerId,
settings: props.settings ?? this.settings,
createdAt: this.createdAt,
});
}
}

View File

@@ -0,0 +1,143 @@
/**
* Domain Entity: Race
*
* Represents a race/session in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
export type SessionType = 'practice' | 'qualifying' | 'race';
export type RaceStatus = 'scheduled' | 'completed' | 'cancelled';
export class Race {
readonly id: string;
readonly leagueId: string;
readonly scheduledAt: Date;
readonly track: string;
readonly car: string;
readonly sessionType: SessionType;
readonly status: RaceStatus;
private constructor(props: {
id: string;
leagueId: string;
scheduledAt: Date;
track: string;
car: string;
sessionType: SessionType;
status: RaceStatus;
}) {
this.id = props.id;
this.leagueId = props.leagueId;
this.scheduledAt = props.scheduledAt;
this.track = props.track;
this.car = props.car;
this.sessionType = props.sessionType;
this.status = props.status;
}
/**
* Factory method to create a new Race entity
*/
static create(props: {
id: string;
leagueId: string;
scheduledAt: Date;
track: string;
car: string;
sessionType?: SessionType;
status?: RaceStatus;
}): Race {
this.validate(props);
return new Race({
id: props.id,
leagueId: props.leagueId,
scheduledAt: props.scheduledAt,
track: props.track,
car: props.car,
sessionType: props.sessionType ?? 'race',
status: props.status ?? 'scheduled',
});
}
/**
* Domain validation logic
*/
private static validate(props: {
id: string;
leagueId: string;
scheduledAt: Date;
track: string;
car: string;
}): void {
if (!props.id || props.id.trim().length === 0) {
throw new Error('Race ID is required');
}
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new Error('League ID is required');
}
if (!props.scheduledAt || !(props.scheduledAt instanceof Date)) {
throw new Error('Valid scheduled date is required');
}
if (!props.track || props.track.trim().length === 0) {
throw new Error('Track is required');
}
if (!props.car || props.car.trim().length === 0) {
throw new Error('Car is required');
}
}
/**
* Mark race as completed
*/
complete(): Race {
if (this.status === 'completed') {
throw new Error('Race is already completed');
}
if (this.status === 'cancelled') {
throw new Error('Cannot complete a cancelled race');
}
return new Race({
...this,
status: 'completed',
});
}
/**
* Cancel the race
*/
cancel(): Race {
if (this.status === 'completed') {
throw new Error('Cannot cancel a completed race');
}
if (this.status === 'cancelled') {
throw new Error('Race is already cancelled');
}
return new Race({
...this,
status: 'cancelled',
});
}
/**
* Check if race is in the past
*/
isPast(): boolean {
return this.scheduledAt < new Date();
}
/**
* Check if race is upcoming
*/
isUpcoming(): boolean {
return this.status === 'scheduled' && !this.isPast();
}
}

View File

@@ -0,0 +1,113 @@
/**
* Domain Entity: Result
*
* Represents a race result in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
export class Result {
readonly id: string;
readonly raceId: string;
readonly driverId: string;
readonly position: number;
readonly fastestLap: number;
readonly incidents: number;
readonly startPosition: number;
private constructor(props: {
id: string;
raceId: string;
driverId: string;
position: number;
fastestLap: number;
incidents: number;
startPosition: number;
}) {
this.id = props.id;
this.raceId = props.raceId;
this.driverId = props.driverId;
this.position = props.position;
this.fastestLap = props.fastestLap;
this.incidents = props.incidents;
this.startPosition = props.startPosition;
}
/**
* Factory method to create a new Result entity
*/
static create(props: {
id: string;
raceId: string;
driverId: string;
position: number;
fastestLap: number;
incidents: number;
startPosition: number;
}): Result {
this.validate(props);
return new Result(props);
}
/**
* Domain validation logic
*/
private static validate(props: {
id: string;
raceId: string;
driverId: string;
position: number;
fastestLap: number;
incidents: number;
startPosition: number;
}): void {
if (!props.id || props.id.trim().length === 0) {
throw new Error('Result ID is required');
}
if (!props.raceId || props.raceId.trim().length === 0) {
throw new Error('Race ID is required');
}
if (!props.driverId || props.driverId.trim().length === 0) {
throw new Error('Driver ID is required');
}
if (!Number.isInteger(props.position) || props.position < 1) {
throw new Error('Position must be a positive integer');
}
if (props.fastestLap < 0) {
throw new Error('Fastest lap cannot be negative');
}
if (!Number.isInteger(props.incidents) || props.incidents < 0) {
throw new Error('Incidents must be a non-negative integer');
}
if (!Number.isInteger(props.startPosition) || props.startPosition < 1) {
throw new Error('Start position must be a positive integer');
}
}
/**
* Calculate positions gained/lost
*/
getPositionChange(): number {
return this.startPosition - this.position;
}
/**
* Check if driver finished on podium
*/
isPodium(): boolean {
return this.position <= 3;
}
/**
* Check if driver had a clean race (0 incidents)
*/
isClean(): boolean {
return this.incidents === 0;
}
}

View File

@@ -0,0 +1,117 @@
/**
* Domain Entity: Standing
*
* Represents a championship standing in the GridPilot platform.
* Immutable entity with factory methods and domain validation.
*/
export class Standing {
readonly leagueId: string;
readonly driverId: string;
readonly points: number;
readonly wins: number;
readonly position: number;
readonly racesCompleted: number;
private constructor(props: {
leagueId: string;
driverId: string;
points: number;
wins: number;
position: number;
racesCompleted: number;
}) {
this.leagueId = props.leagueId;
this.driverId = props.driverId;
this.points = props.points;
this.wins = props.wins;
this.position = props.position;
this.racesCompleted = props.racesCompleted;
}
/**
* Factory method to create a new Standing entity
*/
static create(props: {
leagueId: string;
driverId: string;
points?: number;
wins?: number;
position?: number;
racesCompleted?: number;
}): Standing {
this.validate(props);
return new Standing({
leagueId: props.leagueId,
driverId: props.driverId,
points: props.points ?? 0,
wins: props.wins ?? 0,
position: props.position ?? 0,
racesCompleted: props.racesCompleted ?? 0,
});
}
/**
* Domain validation logic
*/
private static validate(props: {
leagueId: string;
driverId: string;
}): void {
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new Error('League ID is required');
}
if (!props.driverId || props.driverId.trim().length === 0) {
throw new Error('Driver ID is required');
}
}
/**
* Add points from a race result
*/
addRaceResult(position: number, pointsSystem: Record<number, number>): Standing {
const racePoints = pointsSystem[position] ?? 0;
const isWin = position === 1;
return new Standing({
leagueId: this.leagueId,
driverId: this.driverId,
points: this.points + racePoints,
wins: this.wins + (isWin ? 1 : 0),
position: this.position,
racesCompleted: this.racesCompleted + 1,
});
}
/**
* Update championship position
*/
updatePosition(position: number): Standing {
if (!Number.isInteger(position) || position < 1) {
throw new Error('Position must be a positive integer');
}
return new Standing({
...this,
position,
});
}
/**
* Calculate average points per race
*/
getAveragePoints(): number {
if (this.racesCompleted === 0) return 0;
return this.points / this.racesCompleted;
}
/**
* Calculate win percentage
*/
getWinPercentage(): number {
if (this.racesCompleted === 0) return 0;
return (this.wins / this.racesCompleted) * 100;
}
}