Files
healer-man/src/game/benchmark/benchmarkReport.test.ts
T
2026-09-17 09:13:15 -04:00

174 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import { EMPTY_ITEM_STATS } from "../itemStats";
import { benchmarkScenarioById } from "./benchmarkScenarios";
import {
aggregateBenchmarkMatrix,
benchmarkQuantile,
renderBenchmarkMatrixMarkdown,
serializeBenchmarkMatrixCsv,
} from "./benchmarkReport";
import type {
BenchmarkBuildDefinition,
BenchmarkMatrixReport,
BenchmarkRunMetrics,
BenchmarkRunReport,
} from "./benchmarkTypes";
const scenario = benchmarkScenarioById("damage-single-burst")!;
const build: BenchmarkBuildDefinition = {
id: "mage-burst",
source: "wow",
classId: "mage",
secondaryClassId: null,
role: "damage",
level: 60,
orientation: 'spell, "burst"',
talentRanks: {},
inventory: [],
equipment: {},
gearPresetId: "level-60-baseline",
};
const defaultMetrics: BenchmarkRunMetrics = {
rawDamage: 0,
effectiveDamage: 0,
priorityTargetDamage: 0,
effectiveHealing: 0,
rawHealing: 0,
overheal: 0,
absorbsApplied: 0,
absorbsConsumed: 0,
rawDamageTaken: 0,
effectiveDamageTaken: 0,
incomingAttacks: 0,
avoidedAttacks: 0,
deaths: 0,
minimumPlayerHealthPercent: 100,
minimumPartyHealthPercent: 100,
castsStarted: 0,
castsCompleted: 0,
movementInterrupts: 0,
invalidCasts: 0,
distanceMoved: 0,
movementTimeMs: 0,
stackCompliancePercent: null,
threatControlPercent: null,
timeToResourceFailureMs: null,
timeToFirstDeathMs: null,
dps: 0,
hps: 0,
dtps: 0,
overhealPercent: 0,
mitigationPercent: 0,
avoidancePercent: 0,
};
function run(
seed: number,
metrics: Partial<BenchmarkRunMetrics>,
valid = true,
): BenchmarkRunReport {
return {
schemaVersion: 1,
runId: `mage-burst:${seed}`,
policyId: "baseline-v1",
seed,
build,
scenario,
simulatedDurationMs: scenario.durationMs,
completed: seed !== 2,
metrics: { ...defaultMetrics, ...metrics },
resources: [],
abilities: [],
eventCounts: {},
validation: { valid, reasons: valid ? [] : ["fixture invalid"] },
gearStats: EMPTY_ITEM_STATS,
};
}
function matrix(): BenchmarkMatrixReport {
return {
schemaVersion: 1,
generatedAt: "2026-08-28T12:00:00.000Z",
appVersion: "test",
cohort: {
level: 60,
gearPresetId: "level-60-baseline",
seeds: [1, 2, 3, 4],
durationScale: 1,
policyId: "baseline-v1",
rotationVersion: "playerbots-inspired-v1",
},
scenarios: [scenario],
builds: [build],
runs: [
run(1, { dps: 10, priorityTargetDamage: 100 }),
run(2, { dps: 20, priorityTargetDamage: 200 }),
run(3, { dps: 30, priorityTargetDamage: 300 }),
run(4, { dps: 999, priorityTargetDamage: 9_999 }, false),
],
};
}
describe("benchmark reporting", () => {
it("uses deterministic interpolated quantiles", () => {
expect(benchmarkQuantile([30, null, 10, 20], 0.1)).toBe(12);
expect(benchmarkQuantile([30, null, 10, 20], 0.5)).toBe(20);
expect(benchmarkQuantile([30, null, 10, 20], 0.9)).toBe(28);
expect(benchmarkQuantile([null], 0.5)).toBeNull();
expect(() => benchmarkQuantile([1], 1.1)).toThrow(RangeError);
});
it("aggregates validated runs and retains null-event counts", () => {
const summary = aggregateBenchmarkMatrix(matrix());
const [row] = summary.rows;
expect(summary).toMatchObject({ runCount: 4, validRunCount: 3, invalidRunCount: 1 });
expect(row).toMatchObject({
runCount: 4,
validRunCount: 3,
invalidRunCount: 1,
completedRunCount: 3,
});
expect(row.metrics.dps).toEqual({
count: 3,
nullCount: 0,
p10: 12,
median: 20,
p90: 28,
});
expect(row.metrics.timeToFirstDeathMs).toEqual({
count: 0,
nullCount: 3,
p10: null,
median: null,
p90: null,
});
});
it("serializes complete metric rows as newline-clean CSV", () => {
const csv = serializeBenchmarkMatrixCsv(matrix());
expect(csv).toContain("scenario_id,scenario_version,scenario_name");
expect(csv).toContain("policy_id,rotation_version,scenario_id");
expect(csv).toContain('mage-burst,wow,mage,,damage,60,"spell, ""burst"""');
expect(csv).toContain(",dps,3,0,12,20,28\n");
expect(csv.endsWith("\n")).toBe(true);
expect(csv.endsWith("\n\n")).toBe(false);
expect(csv).not.toContain("\r");
});
it("renders separate role metrics instead of a composite ranking", () => {
const markdown = renderBenchmarkMatrixMarkdown(matrix());
expect(markdown).toContain("## Damage");
expect(markdown).toContain("DPS: 20 [12–28]");
expect(markdown).toContain("1 invalid run(s) are excluded");
expect(markdown).toContain("does not calculate a composite score");
expect(markdown).toContain("rotation: `playerbots-inspired-v1`");
expect(markdown.endsWith("\n")).toBe(true);
expect(markdown.endsWith("\n\n")).toBe(false);
});
});