457 lines
14 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |