WOS Sim
← Back to Runs

Run Detail

0ee66158-e704-49ec-8f77-9a3bb2758390

Started18/06/2026, 02:41:35
Git SHA38c4f1b0
Statedirty
Avg Error %0.37%
BH Sig Count1
Total Cases12
Passing11
Run Report

Testcase Set Diff vs Previous Run

- Removed (166)

Code Changes Since Previous Run

Index: simulator/config/hero_definitions/Mia.json
===================================================================
--- simulator/config/hero_definitions/Mia.json prev run
+++ simulator/config/hero_definitions/Mia.json this run
@@ -29,10 +29,9 @@
"applies_to": "target"
},
"duration": {
"type": "turn",
- "value": 1,
- "delay": 1
+ "value": 1
},
"same_effect_stacking": "max"
}
}
Index: simulator/src/damage.ts
===================================================================
--- simulator/src/damage.ts prev run
+++ simulator/src/damage.ts this run
@@ -221,26 +221,25 @@
} else {
maxGroups.set(key, { selected: candidate, candidates: [candidate] });
}
} else {
- applyBucketCandidateGroup(candidate, [candidate], buckets, detail, appliedEffects, rejectedEffects, consumedEffectIds);
+ applyBucketCandidate(candidate, buckets, detail, appliedEffects, rejectedEffects, consumedEffectIds);
}
}
if (!maxGroups) return;
for (const group of maxGroups.values()) {
applyBucketCandidateGroup(group.selected, group.candidates, buckets, detail, appliedEffects, rejectedEffects, consumedEffectIds);
}
}
-function applyBucketCandidateGroup(
+// Apply the selected candidate's value to its bucket and (in trace mode) record it; returns the
+// applied percentage. Shared by the single-candidate and max-group paths.
+function applySelectedBucket(
selected: BucketCandidate,
- candidates: BucketCandidate[],
buckets: NumericDamageBuckets,
detail: DamageDetail,
- appliedEffects: DamageEquationTrace["appliedEffects"],
- rejectedEffects: DamageEquationTrace["rejectedEffects"],
- consumedEffectIds: Set<string>
-): void {
+ appliedEffects: DamageEquationTrace["appliedEffects"]
+): number {
const appliedValuePct = applyBucketValue(
buckets,
selected.bucket,
selected.valuePct,
@@ -250,21 +249,52 @@
selected.bucket,
selected.effect.stackingKey,
selected.effect.sameEffectStacking
);
+ if (appliedValuePct !== 0 && detail === "full") {
+ const appliedEffect: DamageEquationTrace["appliedEffects"][number] = {
+ effectId: selected.effect.source.effectId ?? selected.effect.id,
+ bucket: selected.bucket,
+ valuePct: appliedValuePct,
+ source: sourceLabel(selected.effect),
+ sourceSide: selected.effect.ownerSide,
+ sameEffectStacking: selected.effect.sameEffectStacking
+ };
+ if (selected.effect.stackingKey !== undefined) appliedEffect.stackingKey = selected.effect.stackingKey;
+ appliedEffects.push(appliedEffect);
+ }
+ return appliedValuePct;
+}
+
+// Lone-candidate fast path (the common case): no max-stacking group, so no temporary [candidate]
+// array and no suppressed-sibling bookkeeping.
+function applyBucketCandidate(
+ candidate: BucketCandidate,
+ buckets: NumericDamageBuckets,
+ detail: DamageDetail,
+ appliedEffects: DamageEquationTrace["appliedEffects"],
+ rejectedEffects: DamageEquationTrace["rejectedEffects"],
+ consumedEffectIds: Set<string>
+): void {
+ const appliedValuePct = applySelectedBucket(candidate, buckets, detail, appliedEffects);
if (appliedValuePct !== 0) {
- if (detail === "full") {
- const appliedEffect: DamageEquationTrace["appliedEffects"][number] = {
- effectId: selected.effect.source.effectId ?? selected.effect.id,
- bucket: selected.bucket,
- valuePct: appliedValuePct,
- source: sourceLabel(selected.effect),
- sourceSide: selected.effect.ownerSide,
- sameEffectStacking: selected.effect.sameEffectStacking
- };
- if (selected.effect.stackingKey !== undefined) appliedEffect.stackingKey = selected.effect.stackingKey;
- appliedEffects.push(appliedEffect);
- }
+ if (candidate.effect.duration.type === "attack") consumedEffectIds.add(candidate.effect.id);
+ } else if (detail === "full") {
+ rejectedEffects.push({ effectId: candidate.effect.source.effectId ?? candidate.effect.id, reason: "same_effect_max_superseded" });
+ }
+}
+
+function applyBucketCandidateGroup(
+ selected: BucketCandidate,
+ candidates: BucketCandidate[],
+ buckets: NumericDamageBuckets,
+ detail: DamageDetail,
+ appliedEffects: DamageEquationTrace["appliedEffects"],
+ rejectedEffects: DamageEquationTrace["rejectedEffects"],
+ consumedEffectIds: Set<string>
+): void {
+ const appliedValuePct = applySelectedBucket(selected, buckets, detail, appliedEffects);
+ if (appliedValuePct !== 0) {
for (const candidate of candidates) {
if (candidate.effect.duration.type === "attack") consumedEffectIds.add(candidate.effect.id);
if (detail === "full" && candidate !== selected) {
rejectedEffects.push({ effectId: candidate.effect.source.effectId ?? candidate.effect.id, reason: "same_effect_max_suppressed" });
Show Raw Dirty State Patch (vs Clean Baseline)
diff --git a/shared/fighters_data/fighters_stats.json b/shared/fighters_data/fighters_stats.json
--- a/shared/fighters_data/fighters_stats.json
+++ b/shared/fighters_data/fighters_stats.json
@@ -4,6 +4,11 @@
"lanc": [1400.98, 1432.28, 1756.8, 1722.5],
"mark": [1400.98, 1431.28, 1806.4, 1785.4]
},
+ "custom": {
+ "inf": [1370.6, 1380.5, 1706.7, 1864.7],
+ "lanc": [1370.6, 1380.5, 1820.4, 1669.3],
+ "mark": [1370.6, 1380.5, 1910.9, 1732.9]
+ },
"daut": {
"inf": [
9.94,
diff --git a/simulator/src/testcases.test.ts b/simulator/src/testcases.test.ts
--- a/simulator/src/testcases.test.ts
+++ b/simulator/src/testcases.test.ts
@@ -237,13 +237,13 @@
assert.equal(summary?.gameStatAdjustment?.unadjusted.bias_raw, -2);
});
-test("runTestcases skips stat rounding correction for stochastic misses", () => {
+test("runTestcases applies stat rounding correction for stochastic misses", () => {
const config = loadSimulatorConfig();
const report = runTestcases({ matching: "greg_mia_combo", repeat: 5, calibrationReportPath: "/tmp/does-not-exist.json" }, config);
const summaries = Object.values(report.testcases);
assert.ok(summaries.some((summary) => summary.deterministic === false && summary.game?.passes === false));
- assert.deepEqual(summaries.map((summary) => summary.gameStatAdjustment), summaries.map(() => undefined));
+ assert.ok(summaries.some((summary) => summary.deterministic === false && summary.gameStatAdjustment));
});
test("runTestcases default round cap lets long no-hero baselines reach battle end", () => {
diff --git a/simulator/src/testcases.ts b/simulator/src/testcases.ts
--- a/simulator/src/testcases.ts
+++ b/simulator/src/testcases.ts
@@ -435,8 +435,11 @@
deterministic: boolean;
thresholds?: Record<string, number>;
}): InternalStatAdjustment | undefined {
- if (!options.deterministic || options.game.bias_raw === 0) return undefined;
+ const shouldCorrect = options.deterministic ? options.game.bias_raw !== 0 : !options.game.passes;
+ if (!shouldCorrect || options.game.bias_raw === 0) return undefined;
const direction = -Math.sign(options.game.bias_raw);
+ if (!options.deterministic) return findStochasticGameStatAdjustment(options, direction);
+
const maxCandidate = evaluateStatAdjustment(options, direction * STAT_ROUNDING_MAX_ADJUSTMENT);
let best = maxCandidate;
if (maxCandidate.mode === "deterministic_exact") return maxCandidate;
@@ -465,6 +468,33 @@
return best;
}
+function findStochasticGameStatAdjustment(options: {
+ game: ParityComparisonMetrics;
+ input: BattleInput;
+ config: SimulatorConfig;
+ job: TestcaseExecutionJob;
+ reference: { n: number; mu: number; sigma: number };
+ initialTroops: number;
+ deterministic: boolean;
+ thresholds?: Record<string, number>;
+}, direction: number): InternalStatAdjustment | undefined {
+ let best: InternalStatAdjustment | undefined;
+ for (const value of statAdjustmentCandidates(direction)) {
+ const candidate = evaluateStatAdjustment(options, value);
+ if (!best || correctionScore(candidate.adjusted, options.deterministic) < correctionScore(best.adjusted, options.deterministic)) best = candidate;
+ }
+ if (best?.adjusted.passes) return { ...best, mode: "stochastic_tolerance" };
+ return best;
+}
+
+function statAdjustmentCandidates(direction: number): number[] {
+ const values: number[] = [];
+ for (let step = STAT_ROUNDING_SCAN_STEPS; step >= 1; step -= 1) {
+ values.push(roundStatAdjustment(direction * (STAT_ROUNDING_MAX_ADJUSTMENT * step) / STAT_ROUNDING_SCAN_STEPS));
+ }
+ return values;
+}
+
function evaluateStatAdjustment(options: {
game: ParityComparisonMetrics;
input: BattleInput;

Accuracy Results

12 / 12
FileCase#AdjS nG nS μG μBias%tqPW
testcases/emulator_verified/greg_mia_combo.jsongreg_mia_combo010042416.22508.3-1.97%-1.610.36P
testcases/emulator_verified/greg_mia_combo.jsongreg_mia_combo1-0.05%10053482.43432.81.06%5.570.0F
testcases/emulator_verified/greg_mia_defender_current.jsongreg_mia_defender_current0100183347.63351.6-0.09%-0.700.62P
testcases/emulator_verified/greg_mia_nohero_control_current.jsongreg_mia_nohero_control_current0133752.03752.00.00%P
testcases/emulator_verified/greg_mia_nohero_control_current.jsongreg_mia_nohero_control_current1+0.05%133652.03652.00.00%P
testcases/emulator_verified/mia_only_defender_current.jsonmia_only_defender_current0100103540.83543.9-0.07%-0.630.62P
testcases/emulator_verified/mia_only_defender_current.jsonmia_only_defender_current1100203439.73449.2-0.21%-2.460.069P
testcases/emulator_verified/mia_solo.jsonmia_solo01001-269.3-260.0-0.27%-0.970.62P
testcases/emulator_verified/mia_solo.jsonmia_solo110011297.91293.00.22%0.590.62P
testcases/emulator_verified/mia_solo.jsonmia_solo21001-185.0-180.0-0.32%-1.010.62P
testcases/emulator_verified/mia_solo.jsonmia_solo31001879.6879.00.04%0.140.88P
testcases/emulator_verified/mia_solo.jsonmia_solo41001969.9967.00.21%0.650.62P