Files
gridpilot.gg/core/identity/domain/services/DrivingRatingCalculator.test.ts
2025-12-29 22:27:33 +01:00

457 lines
14 KiB
TypeScript

import { DrivingRatingCalculator, DrivingRaceFactsDto } from './DrivingRatingCalculator';
import { RatingEvent } from '../entities/RatingEvent';
import { RatingEventId } from '../value-objects/RatingEventId';
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
import { RatingDelta } from '../value-objects/RatingDelta';
describe('DrivingRatingCalculator', () => {
describe('calculateFromRaceFacts', () => {
it('should calculate delta for finished race with good performance', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
sof: 2500,
},
{
userId: 'user-456',
startPos: 3,
finishPos: 8,
incidents: 0,
status: 'finished',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
expect(results.has('user-123')).toBe(true);
const result = results.get('user-123')!;
expect(result.userId).toBe('user-123');
expect(result.delta).toBeGreaterThan(0); // Positive for good performance
expect(result.events.length).toBeGreaterThan(0);
// Should have performance event
const performanceEvent = result.events.find(e => e.reasonCode === 'DRIVING_FINISH_STRENGTH_GAIN');
expect(performanceEvent).toBeDefined();
expect(performanceEvent?.delta).toBeGreaterThan(0);
});
it('should calculate delta for finished race with poor performance', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 2,
finishPos: 8,
incidents: 0,
status: 'finished',
sof: 2500,
},
{
userId: 'user-456',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
const result = results.get('user-123')!;
expect(result.delta).toBeLessThan(0); // Negative for poor performance
});
it('should add incident penalties', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 5,
incidents: 3,
status: 'finished',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
const result = results.get('user-123')!;
const incidentEvent = result.events.find(e => e.reasonCode === 'DRIVING_INCIDENTS_PENALTY');
expect(incidentEvent).toBeDefined();
expect(incidentEvent?.delta).toBeLessThan(0);
expect(result.delta).toBeLessThan(0);
});
it('should apply DNS penalty', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 10,
incidents: 0,
status: 'dns',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
const result = results.get('user-123')!;
const dnsEvent = result.events.find(e => e.reasonCode === 'DRIVING_DNS_PENALTY');
expect(dnsEvent).toBeDefined();
expect(dnsEvent?.delta).toBe(-15);
expect(result.delta).toBeLessThan(0);
});
it('should apply DNF penalty', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 10,
incidents: 0,
status: 'dnf',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
const result = results.get('user-123')!;
const dnfEvent = result.events.find(e => e.reasonCode === 'DRIVING_DNF_PENALTY');
expect(dnfEvent).toBeDefined();
expect(dnfEvent?.delta).toBe(-10);
});
it('should apply DSQ penalty', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 10,
incidents: 0,
status: 'dsq',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
const result = results.get('user-123')!;
const dsqEvent = result.events.find(e => e.reasonCode === 'DRIVING_DSQ_PENALTY');
expect(dsqEvent).toBeDefined();
expect(dsqEvent?.delta).toBe(-25);
});
it('should apply AFK penalty', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 10,
incidents: 0,
status: 'afk',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
const result = results.get('user-123')!;
const afkEvent = result.events.find(e => e.reasonCode === 'DRIVING_AFK_PENALTY');
expect(afkEvent).toBeDefined();
expect(afkEvent?.delta).toBe(-20);
});
it('should calculate positions gained bonus', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 10,
finishPos: 3,
incidents: 0,
status: 'finished',
sof: 2500,
},
{
userId: 'user-456',
startPos: 5,
finishPos: 8,
incidents: 0,
status: 'finished',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
const result = results.get('user-123')!;
const gainEvent = result.events.find(e => e.reasonCode === 'DRIVING_POSITIONS_GAINED_BONUS');
expect(gainEvent).toBeDefined();
expect(gainEvent?.delta).toBeGreaterThan(0);
});
it('should handle multiple drivers in race', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
sof: 2500,
},
{
userId: 'user-456',
startPos: 3,
finishPos: 8,
incidents: 2,
status: 'finished',
sof: 2500,
},
{
userId: 'user-789',
startPos: 5,
finishPos: 5,
incidents: 0,
status: 'dns',
sof: 2500,
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
expect(results.has('user-123')).toBe(true);
expect(results.has('user-456')).toBe(true);
expect(results.has('user-789')).toBe(true);
// user-123 should have positive delta
expect(results.get('user-123')!.delta).toBeGreaterThan(0);
// user-456 should have negative delta (poor position + incidents)
expect(results.get('user-456')!.delta).toBeLessThan(0);
// user-789 should have negative delta (DNS)
expect(results.get('user-789')!.delta).toBeLessThan(0);
});
it('should calculate SoF if not provided', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
// No sof provided
},
{
userId: 'user-456',
startPos: 3,
finishPos: 8,
incidents: 0,
status: 'finished',
// No sof provided
},
],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
// Should still calculate without errors
expect(results.size).toBe(2);
expect(results.get('user-123')!.events.length).toBeGreaterThan(0);
});
it('should handle empty results array', () => {
const facts: DrivingRaceFactsDto = {
raceId: 'race-123',
results: [],
};
const results = DrivingRatingCalculator.calculateFromRaceFacts(facts);
expect(results.size).toBe(0);
});
});
describe('calculate', () => {
it('should sum events with component weights', () => {
const events: RatingEvent[] = [
RatingEvent.create({
id: RatingEventId.generate(),
userId: 'user-123',
dimension: RatingDimensionKey.create('driving'),
delta: RatingDelta.create(10),
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'race', id: 'race-123' },
reason: {
code: 'DRIVING_FINISH_STRENGTH_GAIN',
summary: 'Good finish',
details: {},
},
visibility: { public: true, redactedFields: [] },
version: 1,
}),
RatingEvent.create({
id: RatingEventId.generate(),
userId: 'user-123',
dimension: RatingDimensionKey.create('driving'),
delta: RatingDelta.create(-5),
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'race', id: 'race-123' },
reason: {
code: 'DRIVING_INCIDENTS_PENALTY',
summary: '1 incident',
details: {},
},
visibility: { public: true, redactedFields: [] },
version: 1,
}),
];
const result = DrivingRatingCalculator.calculate(events);
// Should apply weights: 10 * 0.5 + (-5) * 0.3 = 5 - 1.5 = 3.5
// Then normalized by total weight (1 + 1 = 2)
expect(result).toBeGreaterThan(0);
expect(result).toBeLessThan(5);
});
it('should handle empty events array', () => {
const result = DrivingRatingCalculator.calculate([]);
expect(result).toBe(0);
});
it('should apply reliability weight to penalty events', () => {
const events: RatingEvent[] = [
RatingEvent.create({
id: RatingEventId.generate(),
userId: 'user-123',
dimension: RatingDimensionKey.create('driving'),
delta: RatingDelta.create(-15),
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'race', id: 'race-123' },
reason: {
code: 'DRIVING_DNS_PENALTY',
summary: 'Did not start',
details: {},
},
visibility: { public: true, redactedFields: [] },
version: 1,
}),
];
const result = DrivingRatingCalculator.calculate(events);
// Should apply reliability weight (0.2)
expect(result).toBe(-15 * 0.2);
});
it('should normalize by total weight', () => {
const events: RatingEvent[] = [
RatingEvent.create({
id: RatingEventId.generate(),
userId: 'user-123',
dimension: RatingDimensionKey.create('driving'),
delta: RatingDelta.create(20),
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'race', id: 'race-123' },
reason: {
code: 'DRIVING_FINISH_STRENGTH_GAIN',
summary: 'Test',
details: {},
},
visibility: { public: true, redactedFields: [] },
version: 1,
}),
RatingEvent.create({
id: RatingEventId.generate(),
userId: 'user-123',
dimension: RatingDimensionKey.create('driving'),
delta: RatingDelta.create(-10),
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'race', id: 'race-123' },
reason: {
code: 'DRIVING_DNS_PENALTY',
summary: 'Test',
details: {},
},
visibility: { public: true, redactedFields: [] },
version: 1,
}),
];
const result = DrivingRatingCalculator.calculate(events);
// 20 * 0.5 + (-10) * 0.2 = 10 - 2 = 8
// Normalized by (1 + 1) = 2
// Result = 8 / 2 = 4
expect(result).toBe(4);
});
it('should handle events with custom weights', () => {
const events: RatingEvent[] = [
RatingEvent.create({
id: RatingEventId.generate(),
userId: 'user-123',
dimension: RatingDimensionKey.create('driving'),
delta: RatingDelta.create(10),
weight: 2,
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'race', id: 'race-123' },
reason: {
code: 'DRIVING_FINISH_STRENGTH_GAIN',
summary: 'Test',
details: {},
},
visibility: { public: true, redactedFields: [] },
version: 1,
}),
];
const result = DrivingRatingCalculator.calculate(events);
// Should consider event weight
expect(result).toBeGreaterThan(0);
});
});
});