team rating
This commit is contained in:
185
core/racing/domain/value-objects/TeamRating.ts
Normal file
185
core/racing/domain/value-objects/TeamRating.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import type { IValueObject } from '@core/shared/domain';
|
||||
|
||||
/**
|
||||
* 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 IValueObject<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: IValueObject<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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user