WOS Sim
← Back to Runs

Compare Runs

A (baseline)
d2c123f8-60a29120cde17/06/2026, 20:16:06
B (current)
0ee66158-e7038c4f1b018/06/2026, 02:41:35
Avg Error A0.20%
Avg Error B0.37%
Δ Avg Error+0.17%
Improved0
Regressed0
Added0
Retired98

Testcase Delta

178 / 178
FileCase#AdjA%B%Δ%Status
testcases/emulator_verified/greg_mia_combo.jsongreg_mia_combo0-1.58%-1.97%+0.39%unchanged
testcases/emulator_verified/mia_solo.jsonmia_solo2-0.16%-0.32%+0.16%unchanged
testcases/emulator_verified/greg_mia_defender_current.jsongreg_mia_defender_current00.00%-0.09%+0.09%unchanged
testcases/emulator_verified/mia_solo.jsonmia_solo0-0.18%-0.27%+0.09%unchanged
testcases/emulator_verified/mia_only_defender_current.jsonmia_only_defender_current1-0.13%-0.21%+0.08%unchanged
testcases/emulator_verified/greg_mia_combo.jsongreg_mia_combo1-0.05%1.12%1.06%-0.06%unchanged
testcases/emulator_verified/mia_only_defender_current.jsonmia_only_defender_current00.01%-0.07%+0.06%unchanged
testcases/emulator_verified/mia_solo.jsonmia_solo10.27%0.22%-0.05%unchanged
testcases/emulator_verified/mia_solo.jsonmia_solo30.09%0.04%-0.05%unchanged
testcases/emulator_verified/mia_solo.jsonmia_solo40.26%0.21%-0.05%unchanged
testcases/emulator_verified/greg_mia_nohero_control_current.jsongreg_mia_nohero_control_current00.00%0.00%0.00%unchanged
testcases/emulator_verified/greg_mia_nohero_control_current.jsongreg_mia_nohero_control_current1+0.05%0.00%0.00%0.00%unchanged
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1010.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1120.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1230.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1340.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1450.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1560.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1670.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1780.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_1890.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_19100.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_20110.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_21120.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_22130.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_23140.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_24150.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_25160.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_26170.00%retired
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_900.00%retired
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_10-0.32%retired
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_21-0.06%retired
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_32-0.38%retired
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_43-0.45%retired
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_540.00%retired
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_65-0.07%retired
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_100.19%retired
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_210.00%retired
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_32-0.42%retired
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_430.02%retired
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_540.64%retired
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_750.16%retired
testcases/4-testcases_no-heroes_infantry_fc5.jsonNitro_Beast_30_10-0.22%retired
testcases/emulator_verified/ahmose_solo_nc.jsonahmose_solo00.06%retired
testcases/emulator_verified/alonso_attacker_600_all.jsonalonso_attacker_600_all0-0.14%retired
testcases/emulator_verified/alonso_attacker_900_inf_lancer.jsonalonso_attacker_900_inf_lancer00.04%retired
testcases/emulator_verified/alonso_attacker_900_inf_lancer.jsonalonso_attacker_900_inf_lancer10.02%retired
testcases/emulator_verified/alonso_minxxx_defender.jsonalonso_minxxx_defender00.14%retired
testcases/emulator_verified/alonso_norah_combo.jsonalonso_norah_combo0-0.02%retired
testcases/emulator_verified/alonso_norah_combo.jsonalonso_norah_combo1-0.26%retired
testcases/emulator_verified/alonso_norah_combo.jsonalonso_norah_combo2-0.18%retired
testcases/emulator_verified/alonso_solo.jsonalonso_solo02.97%retired
testcases/emulator_verified/alonso_solo_v2.jsonalonso_solo_v201.58%retired
testcases/emulator_verified/alonso_solo_v2.jsonalonso_solo_v21-1.59%retired
testcases/emulator_verified/alonso_vs_alonso.jsonalonso_vs_alonso0-0.17%retired
testcases/emulator_verified/alonso_vs_alonso_inf_lancer.jsonalonso_vs_alonso_inf_lancer0-0.04%retired
testcases/emulator_verified/alonso_wip_attacker.jsonalonso_wip_attacker0-0.03%retired
testcases/emulator_verified/ambusher_no_marksmen.jsonambusher_no_marksmen00.64%retired
testcases/emulator_verified/attdef_jasser_patrick_minxxx_attacks_nc.jsonattdef_jasser_patrick_minxxx_attacks00.00%retired
testcases/emulator_verified/attdef_jasser_patrick_nc.jsonattdef_jasser_patrick00.00%retired
testcases/emulator_verified/attdef_jasser_patrick_swap_nc.jsonattdef_jasser_patrick_swap00.00%retired
testcases/emulator_verified/attdef_jasser_patrick_wip_attacks_nc.jsonattdef_jasser_patrick_wip_attacks00.00%retired
testcases/emulator_verified/attdef_jasser_patrick_wip_attacks_nc.jsonattdef_jasser_patrick_wip_attacks10.00%retired
testcases/emulator_verified/attdef_test_minxxx_attacks_nc.jsonattdef_test_minxxx_attacks_nc00.08%retired
testcases/emulator_verified/attdef_test_wip_attacks_nc.jsonattdef_test_wip_attacks_nc00.00%retired
testcases/emulator_verified/bahiti_norah_combo_nc.jsonbahiti_norah_combo_nc0-2.99%retired
testcases/emulator_verified/bahiti_solo.jsonbahiti_solo00.32%retired
testcases/emulator_verified/bahiti_solo_nc.jsonbahiti_solo_nc00.04%retired
testcases/emulator_verified/determinism_test_normal_nc.jsondeterminism_test_normal00.00%retired
testcases/emulator_verified/determinism_test_swapped_nc.jsondeterminism_test_swapped00.00%retired
testcases/emulator_verified/flint_reina_zinman_combo.jsonflint_reina_zinman_combo0-0.10%retired
testcases/emulator_verified/flint_solo_nc.jsonflint_solo00.00%retired
testcases/emulator_verified/flint_zinman_combo_nc.jsonflint_zinman_combo_nc00.00%retired
testcases/emulator_verified/greg_only_defender_current.jsongreg_only_defender_current00.00%retired
testcases/emulator_verified/greg_only_defender_current.jsongreg_only_defender_current10.01%retired
testcases/emulator_verified/greg_only_defender_current.jsongreg_only_defender_current2-0.02%retired
testcases/emulator_verified/greg_solo.jsongreg_solo00.16%retired
testcases/emulator_verified/gwen_norah_combo_nc.jsongwen_norah_combo_nc00.65%retired
testcases/emulator_verified/gwen_solo_nc.jsongwen_solo00.24%retired
testcases/emulator_verified/hector_patrick_combo.jsonhector_patrick_combo0-0.05%retired
testcases/emulator_verified/hector_renee_wayne.jsondeterminism_test_normal00.54%retired
testcases/emulator_verified/hector_solo.jsonhector_solo00.12%retired
testcases/emulator_verified/jasser_solo_nc.jsonjasser_solo00.00%retired
testcases/emulator_verified/jessie_solo_nc.jsonjessie_solo00.00%retired
testcases/emulator_verified/ling_xue_solo_nc.jsonling_xue_solo00.00%retired
testcases/emulator_verified/ling_xue_solo_nc.jsonling_xue_solo1-0.02%retired
testcases/emulator_verified/logan_bahiti_combo_nc.jsonlogan_bahiti_combo_nc00.55%retired
testcases/emulator_verified/logan_gwen_combo_nc.jsonlogan_gwen_combo_nc00.13%retired
testcases/emulator_verified/logan_lynn_combo.jsonlogan_lynn_combo00.08%retired
testcases/emulator_verified/logan_lynn_combo.jsonlogan_lynn_combo10.03%retired
testcases/emulator_verified/logan_lynn_combo.jsonlogan_lynn_combo20.00%retired
testcases/emulator_verified/logan_reina_bahiti_combo_nc.jsonlogan_reina_bahiti_combo_nc0-0.50%retired
testcases/emulator_verified/logan_solo_minxxx_nc.jsonlogan_solo_minxxx00.00%retired
testcases/emulator_verified/logan_solo_v2_nc.jsonlogan_solo_v200.00%retired
testcases/emulator_verified/lumak_bokan_solo_nc.jsonlumak_bokan_solo00.00%retired
testcases/emulator_verified/lumak_bokan_solo_nc.jsonlumak_bokan_solo10.00%retired
testcases/emulator_verified/lynn_solo.jsonlynn_solo0-0.08%retired
testcases/emulator_verified/molly_lynn_combo.jsonmolly_lynn_combo00.00%retired
testcases/emulator_verified/molly_solo.jsonmolly_solo0-0.07%retired
testcases/emulator_verified/natalia_solo.jsonnatalia_solo0-1.10%retired
testcases/emulator_verified/norah_greg_combo.jsonnorah_greg_combo0-0.04%retired
testcases/emulator_verified/norah_hector_zinman_combo.jsonnorah_hector_zinman_combo00.08%retired
testcases/emulator_verified/norah_s2_inf_only_A.jsonnorah_s2_inf_only_A0-0.35%retired
testcases/emulator_verified/norah_s2_inf_only_B_nc.jsonnorah_s2_inf_only_B00.00%retired
testcases/emulator_verified/norah_s2_splash_A.jsonnorah_s2_splash_A00.23%retired
testcases/emulator_verified/norah_s2_splash_B_nc.jsonnorah_s2_splash_B00.00%retired
testcases/emulator_verified/norah_s2_splash_C_nc.jsonnorah_s2_splash_C00.00%retired
testcases/emulator_verified/norah_s2_splash_D.jsonnorah_s2_splash_D00.12%retired
testcases/emulator_verified/norah_solo.jsonnorah_solo0-0.28%retired
testcases/emulator_verified/norah_zinman_combo_nc.jsonnorah_zinman_combo_nc00.39%retired
testcases/emulator_verified/patrick_solo_nc.jsonpatrick_solo0-0.02%retired
testcases/emulator_verified/philly_bahiti_combo.jsonphilly_bahiti_combo0-0.13%retired
testcases/emulator_verified/philly_bahiti_combo.jsonphilly_bahiti_combo10.24%retired
testcases/emulator_verified/philly_solo.jsonphilly_solo00.24%retired
testcases/emulator_verified/philly_solo.jsonphilly_solo10.03%retired
testcases/emulator_verified/reina_attacker_wip.jsonreina_attacker_wip00.01%retired
testcases/emulator_verified/reina_bahiti_combo_nc.jsonreina_bahiti_combo_nc01.12%retired
testcases/emulator_verified/reina_logan_combo_simulator.jsonreina_logan_combo_simulator00.14%retired
testcases/emulator_verified/reina_solo_wip.jsonreina_solo_wip0-0.02%retired
testcases/emulator_verified/renee_solo_nc.jsonrenee_solo_nc0-0.48%retired
testcases/emulator_verified/seo_yoon_solo_nc.jsonseo_yoon_solo00.10%retired
testcases/emulator_verified/sergey_norah_zinman_combo_nc.jsonsergey_norah_zinman_combo_nc0-1.44%retired
testcases/emulator_verified/sergey_reina_combo_nc.jsonsergey_reina_combo_nc0-0.35%retired
testcases/emulator_verified/sergey_solo_nc.jsonsergey_solo00.00%retired
testcases/emulator_verified/simple_001_nc.jsonsimple_00100.00%retired
testcases/emulator_verified/troop_check_nc.jsontroop_check00.00%retired
testcases/emulator_verified/type_triangle_A_nc.jsontype_triangle_A_nc00.00%retired
testcases/emulator_verified/type_triangle_B_nc.jsontype_triangle_B_nc00.00%retired
testcases/emulator_verified/wayne_mixed_solo.jsonwayne_mixed_solo0-0.23%retired
testcases/emulator_verified/wayne_mixed_solo.jsonwayne_mixed_solo1-2.01%retired
testcases/emulator_verified/wayne_reina_combo.jsonwayne_reina_combo0-0.37%retired
testcases/emulator_verified/wayne_reina_wuming_combo.jsonwayne_reina_wuming_combo00.18%retired
testcases/emulator_verified/wayne_s1_solo.jsonwayne_s1_solo00.36%retired
testcases/emulator_verified/wayne_s2_solo.jsonwayne_s2_solo00.43%retired
testcases/emulator_verified/wos425_wip_nohero_control_nc.jsonwos425_wip_nohero_control_nc00.00%retired
testcases/emulator_verified/wos425_wip_patrick_health_nc.jsonwos425_wip_patrick_health_nc00.00%retired
testcases/emulator_verified/wos425_wip_philly_defense.jsonwos425_wip_philly_defense0-0.02%retired
testcases/emulator_verified/wos425_wip_sergey_damage_taken_nc.jsonwos425_wip_sergey_damage_taken_nc00.00%retired
testcases/emulator_verified/wos425_wip_sergey_patrick_combo_nc.jsonwos425_wip_sergey_patrick_combo_nc00.00%retired
testcases/emulator_verified/wos425_wip_sergey_philly_combo.jsonwos425_wip_sergey_philly_combo0-0.01%retired
testcases/emulator_verified/wos427_wip_sergey_philly_840.jsonwos427_wip_sergey_philly_8400-0.28%retired
testcases/emulator_verified/wu_ming_solo_nc.jsonwu_ming_solo00.00%retired
testcases/emulator_verified/zinman_solo_nc.jsonzinman_solo00.00%retired
testcases/heroes_unittests/Flint_tc.jsondaut_viper_10-0.01%retired
testcases/heroes_unittests/Flint_tc.jsondaut_viper_210.56%retired
testcases/heroes_unittests/Hector_tc.jsondaut_viper_100.00%retired
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_10-0.06%retired
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_21-0.07%retired
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_320.00%retired
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_430.00%retired
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_540.00%retired
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_65-0.06%retired
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_76-0.05%retired
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_870.00%retired
testcases/heroes_unittests/Mia_tc.jsondaut_viper_100.21%retired
testcases/heroes_unittests/Mia_tc.jsondaut_viper_21-0.50%retired
testcases/heroes_unittests/Mia_tc.jsondaut_viper_32-0.21%retired
testcases/heroes_unittests/Mia_tc.jsondaut_viper_43-0.29%retired
testcases/heroes_unittests/Mia_tc.jsondaut_viper_54-0.10%retired
testcases/heroes_unittests/Molly_tc.jsondaut_viper_10-0.06%retired
testcases/heroes_unittests/Molly_tc.jsondaut_viper_21-0.07%retired
testcases/heroes_unittests/Molly_tc.jsondaut_viper_320.00%retired
testcases/heroes_unittests/Molly_tc.jsondaut_viper_430.33%retired
testcases/heroes_unittests/Molly_tc.jsondaut_viper_54-0.02%retired
testcases/heroes_unittests/Reina_tc.jsondaut_viper_10-0.18%retired
testcases/heroes_unittests/Reina_tc.jsondaut_viper_210.00%retired
testcases/heroes_unittests/Reina_tc.jsondaut_viper_42-0.46%retired
testcases/heroes_unittests/Reina_tc.jsondaut_viper_530.01%retired
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_10-0.06%retired
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_320.00%retired
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_43-0.04%retired
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_54-0.06%retired
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_61-0.07%retired
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_650.04%retired
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_760.04%retired
testcases/heroes_unittests/Zinman_tc.jsondaut_viper_100.00%retired
testcases/heroes_unittests/Zinman_tc.jsondaut_viper_210.00%retired
testcases/heroes_unittests/Zinman_tc.jsondaut_viper_320.00%retired

Code / Config Changes

Commits (29120cde38c4f1b0)

38c4f1b0F: lone-candidate fast path in applyBucketCandidatespiddlyminx17/06/2026, 20:48:07

Code Changes (Run A → Run B)

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 per-run patches
Run A dirty state patch
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/config/hero_definitions/Mia.json b/simulator/config/hero_definitions/Mia.json
--- a/simulator/config/hero_definitions/Mia.json
+++ b/simulator/config/hero_definitions/Mia.json
@@ -30,7 +30,8 @@
},
"duration": {
"type": "turn",
- "value": 1
+ "value": 1,
+ "delay": 1
},
"same_effect_stacking": "max"
}
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;
Run B dirty state patch
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;