Files
gridpilot.gg/core/racing/domain/value-objects/TeamRating.ts
2026-01-16 16:46:57 +01:00

185 lines
4.7 KiB
TypeScript

import type { ValueObject } from '@core/shared/domain/ValueObject';
/**
* Value Object: TeamRating
*
* Multi-dimensional rating system for teams covering:
* - Driving: racing ability, performance, consistency
* - AdminTrust: reliability, leadership, community contribution
*/
export interface TeamRatingDimension {
value: number; // Current rating value (0-100 scale)
confidence: number; // Confidence level based on sample size (0-1)
sampleSize: number; // Number of events contributing to this rating
trend: 'rising' | 'stable' | 'falling';
lastUpdated: Date;
}
export interface TeamRatingProps {
teamId: string;
driving: TeamRatingDimension;
adminTrust: TeamRatingDimension;
overall: number;
calculatorVersion?: string;
createdAt: Date;
updatedAt: Date;
}
const DEFAULT_DIMENSION: TeamRatingDimension = {
value: 50,
confidence: 0,
sampleSize: 0,
trend: 'stable',
lastUpdated: new Date(),
};
export class TeamRating implements ValueObject<TeamRatingProps> {
readonly props: TeamRatingProps;
private constructor(props: TeamRatingProps) {
this.props = props;
}
get teamId(): string {
return this.props.teamId;
}
get driving(): TeamRatingDimension {
return this.props.driving;
}
get adminTrust(): TeamRatingDimension {
return this.props.adminTrust;
}
get overall(): number {
return this.props.overall;
}
get createdAt(): Date {
return this.props.createdAt;
}
get updatedAt(): Date {
return this.props.updatedAt;
}
get calculatorVersion(): string | undefined {
return this.props.calculatorVersion;
}
static create(teamId: string): TeamRating {
if (!teamId || teamId.trim().length === 0) {
throw new Error('TeamRating teamId is required');
}
const now = new Date();
return new TeamRating({
teamId,
driving: { ...DEFAULT_DIMENSION, lastUpdated: now },
adminTrust: { ...DEFAULT_DIMENSION, lastUpdated: now },
overall: 50,
calculatorVersion: '1.0',
createdAt: now,
updatedAt: now,
});
}
static restore(props: TeamRatingProps): TeamRating {
return new TeamRating(props);
}
equals(other: ValueObject<TeamRatingProps>): boolean {
return this.props.teamId === other.props.teamId;
}
/**
* Update driving rating based on race performance
*/
updateDrivingRating(
newValue: number,
weight: number = 1
): TeamRating {
const updated = this.updateDimension(this.driving, newValue, weight);
return this.withUpdates({ driving: updated });
}
/**
* Update admin trust rating based on league management feedback
*/
updateAdminTrustRating(
newValue: number,
weight: number = 1
): TeamRating {
const updated = this.updateDimension(this.adminTrust, newValue, weight);
return this.withUpdates({ adminTrust: updated });
}
/**
* Calculate weighted overall rating
*/
calculateOverall(): number {
// Weight dimensions by confidence
const weights = {
driving: 0.7 * this.driving.confidence,
adminTrust: 0.3 * this.adminTrust.confidence,
};
const totalWeight = Object.values(weights).reduce((sum, w) => sum + w, 0);
if (totalWeight === 0) {
return 50; // Default when no ratings yet
}
const weightedSum =
this.driving.value * weights.driving +
this.adminTrust.value * weights.adminTrust;
return Math.round(weightedSum / totalWeight);
}
private updateDimension(
dimension: TeamRatingDimension,
newValue: number,
weight: number
): TeamRatingDimension {
const clampedValue = Math.max(0, Math.min(100, newValue));
const newSampleSize = dimension.sampleSize + weight;
// Exponential moving average with decay based on sample size
const alpha = Math.min(0.3, 1 / (dimension.sampleSize + 1));
const updatedValue = dimension.value * (1 - alpha) + clampedValue * alpha;
// Calculate confidence (asymptotic to 1)
const confidence = 1 - Math.exp(-newSampleSize / 20);
// Determine trend
const valueDiff = updatedValue - dimension.value;
let trend: 'rising' | 'stable' | 'falling' = 'stable';
if (valueDiff > 2) trend = 'rising';
if (valueDiff < -2) trend = 'falling';
return {
value: Math.round(updatedValue * 10) / 10,
confidence: Math.round(confidence * 100) / 100,
sampleSize: newSampleSize,
trend,
lastUpdated: new Date(),
};
}
private withUpdates(updates: Partial<TeamRatingProps>): TeamRating {
const newRating = new TeamRating({
...this.props,
...updates,
updatedAt: new Date(),
});
// Recalculate overall
return new TeamRating({
...newRating.props,
overall: newRating.calculateOverall(),
});
}
}