Improve training statistics, achievements and stable pace audio

This commit is contained in:
larssand
2026-09-03 06:36:17 +02:00
parent 82d52ed6cd
commit 4f847610dc
16 changed files with 315 additions and 50 deletions

View File

@@ -60,6 +60,18 @@ function createHarness(options = {}) {
return { state, session, passing, overlayEvents, audioEvents, getSaveCount: () => saveCount };
}
test("five controlled practice laps trigger one stable-pace voice cue", () => {
const harness = createHarness({ sessionType: "open_practice" });
for (let lap = 1; lap <= 5; lap += 1) harness.passing(1_000 + lap * 20_000, "main");
assert.equal(harness.audioEvents.filter((event) => event === "training_stable_pace").length, 1);
});
test("stable-pace training cue is never emitted during a race", () => {
const harness = createHarness({ sessionType: "team_race" });
for (let lap = 1; lap <= 5; lap += 1) harness.passing(1_000 + lap * 20_000, "main");
assert.equal(harness.audioEvents.includes("training_stable_pace"), false);
});
test("split decoder records intermediate time without changing main lap state", () => {
const harness = createHarness();
assert.equal(harness.passing(17_000, "main"), true);

View File

@@ -105,6 +105,7 @@ test("public profile contains only the selected driver's sanitized aggregates",
splitCount: 2,
bestSplitMs: 7000,
averageSplitMs: 7100,
latestSplitGainMs: 200,
pitStopCount: 1,
totalPitMs: 12500,
bestPitStopMs: 12500,

View File

@@ -88,7 +88,7 @@ test("event catalog covers every supported race-audio event", () => {
const keys = new Set(RACE_AUDIO_EVENT_DEFINITIONS.map((item) => item.key));
[
"grid_open", "grid_closed", "race_1_minute", "race_30_seconds", "race_10_seconds", "drivers_ready", "start_sequence", "start_tone", "false_start", "restart_race",
"transponder_pass", "lap_completed", "personal_best", "fastest_lap", "track_record", "new_leader", "position_gain", "position_loss", "final_lap", "one_minute_remaining", "thirty_seconds_remaining", "ten_seconds_remaining", "time_expired", "race_finished",
"transponder_pass", "lap_completed", "personal_best", "fastest_lap", "track_record", "training_stable_pace", "new_leader", "position_gain", "position_loss", "final_lap", "one_minute_remaining", "thirty_seconds_remaining", "ten_seconds_remaining", "time_expired", "race_finished",
"track_call", "track_clear", "yellow_flag", "red_flag", "race_paused", "race_resumed", "marshal_warning", "marshal_required", "penalty", "stop_and_go", "drive_through", "disqualified",
"pit_lane_open", "pit_lane_closed", "entering_pits", "leaving_pits", "pit_speed_warning",
"qualifying_start", "qualifying_remaining", "qualifying_finished", "provisional_pole", "pole_position",

View File

@@ -134,7 +134,8 @@ test("driver statistics remain isolated and aggregate days and sessions", () =>
assert.equal(stats.trainingDays, 2);
assert.equal(stats.bestLapMs, 16500);
assert.equal(stats.averageLapMs, 17167);
assert.equal(stats.controlledLapStreak, 2);
// A flow streak never carries over into another session or stint.
assert.equal(stats.controlledLapStreak, 1);
assert.equal(stats.dailySummaries[0].laps, 1);
assert.equal(stats.dailySummaries[1].laps, 2);
});
@@ -193,6 +194,7 @@ test("split and pit statistics aggregate by personal profile and selected class"
assert.equal(stats.splitCount, 2);
assert.equal(stats.bestSplitMs, 7900);
assert.equal(stats.averageSplitMs, 8000);
assert.equal(stats.latestSplitGainMs, 200);
assert.equal(stats.pitStopCount, 2);
assert.equal(stats.totalPitMs, 22000);
assert.equal(stats.bestPitStopMs, 9500);
@@ -292,6 +294,9 @@ test("active practice stats include only the selected driver's current valid lap
bestLapMs: 16_000,
averageLapMs: 16_500,
lastLapMs: 17_000,
controlZoneMinMs: 16_000,
controlZoneMaxMs: 17_280,
controlledLapStreak: 2,
});
});
@@ -391,6 +396,31 @@ test("a stint shorter than the target duration has no five-minute result", () =>
assert.equal(getBestTrainingDurationResult(stints, 300_000), null);
});
test("training progress recognizes a clean five-minute stint and strong finish", () => {
const ledger = Array.from({ length: 16 }, (_, index) => ({
driverId: "driver_1",
sourceId: "transponder_1",
sessionId: "practice_1",
timestamp: (index + 1) * 20_000,
lapMs: 20_000,
}));
const state = { training: { ledger } };
const stats = buildDriverTrainingStats(state, "driver_1");
const analysis = buildTrainingStintAnalysis(state, "driver_1");
assert.equal(stats.controlZoneMinMs, 20_000);
assert.equal(stats.controlZoneMaxMs, 21_600);
assert.equal(stats.controlledLapStreak, 16);
assert.equal(stats.outlierLaps, 0);
assert.equal(stats.strongFinishStints, 1);
assert.equal(stats.cleanFiveMinuteStints, 1);
assert.equal(stats.achievements.find((item) => item.id === "strong_finish").unlocked, true);
assert.equal(stats.achievements.find((item) => item.id === "clean_five_minute").unlocked, true);
assert.equal(analysis.rows[0].strongFinish, true);
assert.equal(analysis.rows[0].fiveMinuteResult.lapCount, 15);
assert.equal(analysis.rows[0].fiveMinuteResult.totalMs, 300_000);
});
test("recommendations select baseline, consistency, and progression", () => {
assert.equal(buildTrainingRecommendation({ totalLaps: 9 }).level, "baseline");
assert.deepEqual(