Files
gogobee/sim_results/j2a_findings.md
prosolis f2c2d774d4 J2: sim picker casts+consumes; T5 raid-content warning
The post-J1 sweep had the casters clustered at 19–22% L12 clear, vs
martials at 70–80%. A per-round trace across 240 boss-room fights showed
why: autoResolveCombat dispatched !attack only — zero spell_casts, zero
mid-fight consumable uses across every caster class. The entire "caster
cliff" was the sim measuring a strawman where casters couldn't cast.

J2a teaches the sim's autoResolveCombat to mirror a competent prod
player: heal at low HP if an inventory consumable is available, otherwise
cast the highest-EV damage spell (slot or cantrip), otherwise swing.
BuildCharacter now seeds the known-spell list via ensureSpellsForCharacter
so the synthetic spellbook is populated. A -trace flag on the cmd
attaches the raw CombatEvent stream to the last combat of each run for
post-hoc diagnostics.

A first re-baseline (n=100, all 10 classes) showed Ranger regressed
-35.8pp — the picker was burning L3 slots on lightning_arrow when
Ranger's weapon chassis (Hunter's Mark + Extra Attack) was the better
play. Added simMartialFirstClass to gate the picker off for Ranger and
Paladin (whose default kit is also weapon-first / no damage spells).
J2c experimented with widening the picker to control + heal spells;
heal-spell preempt cost druid 10pp (slot heals are 10HP vs 40HP
consumables) and control-spell scoring at 22 cost warlock 6.6pp. Both
reverted. Corpora retained under baseline_j2c*.jsonl for the post-mortem
in sim_results/j2b_findings.md.

Post-J2 L12 leaderboard (baseline_j2a_v2_all10.jsonl, n=100):
  fighter 80.0, ranger 80.0, paladin 78.4, rogue 76.8,
  druid 61.6, mage 53.4, sorcerer 50.6, warlock 48.2,
  bard 40.4, cleric 39.0.

The caster cluster is dissolved; martials are within ±5pp of J1 (sweep
noise). Bard/cleric still trail, but it's no longer a sim artifact —
their defaultKnownSpells damage rosters cap at L2 and the picker can't
pick spells they don't have. That's a prod-level fix, deferred.

J3 trace (sim_results/j3_findings.md): T5 dragons_lair walls every solo
class at 0% (Infernax 546 HP vs solo player HP 110–175; ~25% boss HP
eaten before TPK across all classes). Per the J3 plan menu, this is
party-shaped content the engine doesn't yet have parties for. Surface
a TwinBee-voiced heads-up in handleDnDExpeditionCmd's start path and a
matching tag in !expedition list — players see "raid-shaped — solo
runs not yet survivable" before they spend outfitting coin. No combat
or class balance changes.

Files: cmd/expedition-sim/main.go +trace flag; expedition_sim.go picker
+ SimCombatSummary.Events + spellbook seed; dnd_expedition_cmd.go
raidContentWarning + list tag. All baselines + traces + findings
checked in under sim_results/.
2026-05-17 15:43:41 -07:00

6.3 KiB
Raw Blame History

J2a — autoResolveCombat cast+consume picker (verification sweep)

Date: 2026-05-17 Corpus: sim_results/j2a_traces.jsonl — 240 runs (6 classes × {manor_blackspire, underdark} × L12 × n=20) Pre-state corpus: sim_results/j2_traces.jsonl (same shape, picker disabled). See j2_findings.md for the strawman diagnosis that motivated this fix.

What J2a shipped

  • internal/plugin/expedition_sim.go:
    • New simPickCombatAction(uid, sess) decision tree:
      1. HP < 40% MaxHP + heal consumable in inventory → !consume <heal>
      2. Caster with a usable damage spell (slot or cantrip) → !cast <spell>
      3. Otherwise → !attack
    • New simPickSpell(c, uid) ranks prepared damage-effect spells by slot level (descending) then expected damage from the dice string. Reaction-cast spells excluded; non-cantrips require an available slot at their native level (no upcasting in v1).
    • BuildCharacter now calls ensureSpellsForCharacter so the synthetic spellbook is populated (prod players hit this on character creation; the sim path skipped it).
    • autoResolveCombat dispatches via the picker instead of always swinging.

Built on top of the per-round -trace plumbing landed earlier in J2 (SimCombatSummary.Events, SetSimIncludeTrace).

Verification (n=20 per cell, L12 only — same cells as j2_findings)

class zone n reachBoss won lost rnd pDmg eDmg slotRuns casts consumeRuns eHP@loss
mage manor_blackspire 20 15 12 3 7.2 133.0 74.3 15 / 15 93 12 83.3
mage underdark 20 8 3 5 7.0 143.8 78.6 8 / 8 54 2 34.2
cleric manor_blackspire 20 6 2 4 11.3 101.3 146.2 6 / 6 58 5 64.8
cleric underdark 20 4 0 4 7.0 74.5 89.5 4 / 4 28 0 87.5
sorcerer manor_blackspire 20 13 9 4 8.0 130.9 87.3 13 / 13 91 8 53.2
sorcerer underdark 20 7 1 6 5.9 89.6 67.4 7 / 7 40 1 86.2
warlock manor_blackspire 20 12 6 6 13.2 131.0 140.6 12 / 12 141 9 45.5
warlock underdark 20 15 0 15 6.8 74.3 82.5 15 / 15 101 1 87.7
bard manor_blackspire 20 12 0 12 10.6 68.1 128.2 12 / 12 117 6 75.9
bard underdark 20 12 0 12 11.2 74.3 108.8 12 / 12 132 2 87.7
druid manor_blackspire 20 15 14 1 6.9 142.6 84.5 15 / 15 87 11 79.0
druid underdark 20 18 3 15 5.1 123.3 82.3 18 / 18 85 5 47.5

slotRuns = boss fights where ≥1 spell_cast fired. casts = total spell_cast events. consumeRuns = fights with ≥1 use_consumable. eHP@loss = mean enemy HP remaining when player lost.

DoD check

  • J2a DoD: ≥80% of boss fights produce a cast or consumable use. MET — every cell shows slotRuns == reachBoss (100% cast rate).
  • Manor mean clear (5 trailer casters): mage 60%, sorcerer 45%, warlock 30%, bard 0%, cleric 10%. Mage and sorcerer clear the ≥40% J2 target on the strength of the sim fix alone; warlock/cleric are close, bard is the one outlier still at zero.
  • Druid jumped 0% → 70% manor / 0% → 15% underdark. Pre-J2a it was the lone mid-band class on weapon-attack alone; with cast on top, druid pulls into the top tier.

What's still broken and why

  • Bard 0% clear despite firing 117 casts/cell. Bard's prepared damage spell registry is thin: vicious_mockery cantrip (4d4 at L11) and shatter L2 (3d8) are the only damage-effect options in defaultKnownSpells. Every other bard spell is control/buff/heal, which the v1 picker skips. Bard's average pDmg 68 in 11-round fights = 6/round — that's accurate for the spell pool, not a picker bug. Fix candidates: (a) extend bard's default known-spell list with a damage L3+; (b) widen picker to score EffectControl (hold_person, hypnotic_pattern) which trivializes enemy turns and effectively converts to damage by buying free player rounds; (c) include healing spells in the consume-priority path. (b) has the highest expected ROI and lifts cleric and warlock too.
  • Cleric still bottom-of-table. 10% manor / 0% underdark. Cleric's damage-effect spells: sacred_flame cantrip (4d8 at L11) — that's it. Same shape problem as bard. spiritual_weapon, bless, guiding_bolt, cure_wounds aren't reachable by the v1 picker. Fix: same as bard — score control spells and recognise heal spells as alternatives to consumables (cleric has 40-HP healing word at low slot cost).
  • Warlock underdark 0%. Different shape: high cast rate (101 casts/15 fights = 7/round), high player damage (74), but loses anyway because enemy damage averaged 82.5 and warlock has no defensive lever. Underdark boss is dropping warlock before warlock can drop it. This is a class-balance problem the post-J2b corpus needs to confirm before we move on.

Implications for J2b (the big re-baseline)

A full n=100 all-classes baseline (sim_results/baseline_j2a_all10.jsonl) is kicked off; that replaces baseline_j1_all10.jsonl as the canonical post-fix corpus. Expected shape:

  • Mage / sorcerer / druid pull into mid-band (4070% L12 clear range).
  • Bard / cleric stay at bottom, but the reason is a thin damage-spell pool, not a sim artifact.
  • Warlock somewhere middle; partial-clear depending on zone.
  • Martials should be unchanged (picker is a no-op for non-casters with no low-HP heal trigger).

If that picture holds, the right J2 follow-up is picker widening (control + heal scoring) rather than per-class balance riders. Cleaner than five class buffs.

Files touched

  • internal/plugin/expedition_sim.go — picker, BuildCharacter spell-seed, dispatch in autoResolveCombat.
  • cmd/expedition-sim/main.go — already had -trace from earlier in J2.

No prod paths changed. The fix is sim-scoped.