forked from phenom/healer-man
Release Healer Man 0.1.9
This commit is contained in:
1 parent
c245a199b6
commit
d060296ade
74 files changed
+5901
-334
No files matched your search
@@ -0,0 +1,170 @@
|
||||
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",
|
||||
},
|
||||
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('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.endsWith("\n")).toBe(true);
|
||||
expect(markdown.endsWith("\n\n")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user