Files
gridpilot.gg/core/racing/domain/services/TeamRatingSnapshotCalculator.ts
2026-01-16 19:46:49 +01:00

162 lines
4.8 KiB
TypeScript

import { TeamRatingEvent } from '../entities/TeamRatingEvent';
import { TeamRatingDimensionKey } from '../value-objects/TeamRatingDimensionKey';
import { TeamRatingValue } from '../value-objects/TeamRatingValue';
export interface TeamRatingSnapshot {
teamId: string;
driving: TeamRatingValue;
adminTrust: TeamRatingValue;
overall: number; // Calculated overall rating
lastUpdated: Date;
eventCount: number;
}
/**
* Domain Service: TeamRatingSnapshotCalculator
*
* Calculates team rating snapshots from event ledgers.
* Mirrors the user RatingSnapshotCalculator pattern.
*
* Pure domain logic - no persistence concerns.
*/
export class TeamRatingSnapshotCalculator {
/**
* Calculate current team rating snapshot from all events.
*
* @param teamId - The team ID to calculate for
* @param events - All rating events for the team
* @returns TeamRatingSnapshot with current ratings
*/
static calculate(teamId: string, events: TeamRatingEvent[]): TeamRatingSnapshot {
// Start with default ratings (50 for each dimension)
const defaultRating = 50;
if (events.length === 0) {
return {
teamId,
driving: TeamRatingValue.create(defaultRating),
adminTrust: TeamRatingValue.create(defaultRating),
overall: defaultRating,
lastUpdated: new Date(),
eventCount: 0,
};
}
// Group events by dimension
const eventsByDimension = events.reduce((acc, event) => {
const key = event.dimension.value;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(event);
return acc;
}, {} as Record<string, TeamRatingEvent[]>);
// Calculate each dimension
const dimensionRatings: Record<string, number> = {};
for (const [dimensionKey, dimensionEvents] of Object.entries(eventsByDimension)) {
const totalWeight = dimensionEvents.reduce((sum, event) => {
return sum + (event.weight || 1);
}, 0);
const weightedSum = dimensionEvents.reduce((sum, event) => {
return sum + (event.delta.value * (event.weight || 1));
}, 0);
// Normalize and add to base rating
const normalizedDelta = weightedSum / totalWeight;
dimensionRatings[dimensionKey] = Math.max(0, Math.min(100, defaultRating + normalizedDelta));
}
const drivingRating = dimensionRatings['driving'] ?? defaultRating;
const adminTrustRating = dimensionRatings['adminTrust'] ?? defaultRating;
// Calculate overall as weighted average
const overall = (drivingRating * 0.7 + adminTrustRating * 0.3);
// Find latest event date
const lastUpdated = events.reduce((latest, event) => {
return event.occurredAt > latest ? event.occurredAt : latest;
}, new Date(0));
return {
teamId,
driving: TeamRatingValue.create(drivingRating),
adminTrust: TeamRatingValue.create(adminTrustRating),
overall: Math.round(overall * 10) / 10, // Round to 1 decimal
lastUpdated,
eventCount: events.length,
};
}
/**
* Calculate rating change for a specific dimension from events.
*
* @param dimension - The dimension to calculate for
* @param events - Events to calculate from
* @returns Net change value
*/
static calculateDimensionChange(
dimension: TeamRatingDimensionKey,
events: TeamRatingEvent[]
): number {
const filtered = events.filter(e => e.dimension.equals(dimension));
if (filtered.length === 0) return 0;
const totalWeight = filtered.reduce((sum, event) => {
return sum + (event.weight || 1);
}, 0);
const weightedSum = filtered.reduce((sum, event) => {
return sum + (event.delta.value * (event.weight || 1));
}, 0);
return weightedSum / totalWeight;
}
/**
* Calculate rating change over a time window.
*
* @param teamId - The team ID
* @param events - All events
* @param from - Start date
* @param to - End date
* @returns Snapshot of ratings at the end of the window
*/
static calculateOverWindow(
teamId: string,
events: TeamRatingEvent[],
from: Date,
to: Date
): TeamRatingSnapshot {
const windowEvents = events.filter(e =>
e.occurredAt >= from && e.occurredAt <= to
);
return this.calculate(teamId, windowEvents);
}
/**
* Calculate rating change between two snapshots.
*
* @param before - Snapshot before changes
* @param after - Snapshot after changes
* @returns Object with change values
*/
static calculateDelta(
before: TeamRatingSnapshot,
after: TeamRatingSnapshot
): {
driving: number;
adminTrust: number;
overall: number;
} {
return {
driving: after.driving.value - before.driving.value,
adminTrust: after.adminTrust.value - before.adminTrust.value,
overall: after.overall - before.overall,
};
}
}