rating
This commit is contained in:
457
core/identity/domain/services/DrivingRatingCalculator.test.ts
Normal file
457
core/identity/domain/services/DrivingRatingCalculator.test.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user