185 lines
4.7 KiB
TypeScript
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(),
|
|
});
|
|
}
|
|
} |