Files
gogobee/sim_results/run_matrix.sh
prosolis 519964fb01 J1: Extra Attack now fires in turn-based combat
The class-identity audit (98ba416) wired Extra Attack via the new
resolvePlayerSwings helper, but only SimulateCombat (auto-resolve)
called it. The turn-based engine — every !fight/!attack and every
elite/boss gate the sim drives via autoResolveCombat — still called
single-swing resolvePlayerAttack, so Fighter L11+ got 1 swing/turn at
the gates instead of 3. The audit close-out was correct in spirit but
half-applied.

J1 baseline matrix surfaced it: Fighter L12 cleared 100% of T2 forest
but 2% of T3 manor and 7% of T4 underdark, with %boss_reached at 100%
across the board. The wall was the boss-room damage exchange, not
mid-zone attrition. Trace dump on a sample fight: Fighter dealt 79
dmg in 14 rounds (7 hits / 9 swings) — exactly one swing per round —
versus 167 enemy dmg. With multi-swing wired in, the same fight ends
in 7 rounds with the boss dead, Fighter at 87/168 HP, 16 hits in 19
swings.

n=100 matrix after the fix:
  Fighter L12 manor:     2% → 100% clr
  Fighter L12 underdark: 7% → 98%  clr
  Fighter L12 forest:    94% → 100% (no leader regression)
Mage cells unchanged (J2 territory). Rogue cells within noise.

Sim infra changes that landed alongside (needed to read the J1
signal):

* expedition_sim auto-arms class-default defensive abilities
  (Second Wind / Healing Word) via the new simAutoArmEnabled toggle
  + trySimAutoArm helper, hooked before applyArmedAbility in both
  combat builders. Production code paths untouched (toggle stays
  off). Without this the sim simulated a player who never types
  !arm, which under-counts class survival.
* SimResult.Combats captures per-fight turn-log summaries (rounds,
  hits/misses, damage by side, AC values inferred from RollAgainst)
  so future J-phase questions can dig into the engine without
  re-running the matrix.
* sim_results/run_matrix.sh fans the matrix across (class,level,zone)
  cells via xargs -P (one process per cell — each owns its global
  sqlite handle). ~6× wall-clock speedup on a 14-core box; n=100
  matrix runs in ~3min.
* sim_results/summarize.sh gains p50_yld_clr + %boss_reached columns
  so future sweeps don't conflate "reaches boss" with "clears zone".

Baselines:
  sim_results/baseline_j0_n100.jsonl       — pre-fix (1350 rows)
  sim_results/baseline_j1_extra_attack.jsonl — post-fix (4500 rows)

Phase J state: J0 baseline locked, J1 done. T5 dragons_lair still
0% clear universally (J3). Mage T2+ wall still real (J2).
2026-05-17 14:11:39 -07:00

81 lines
2.4 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# Parallel driver for cmd/expedition-sim matrix sweeps.
#
# Spawns one expedition-sim process per (class, level, zone) cell, so each
# worker owns its own global sqlite handle (NewSimRunner closes+reinits the
# package-level db.Get() — workers in the same process would clobber each
# other). All worker stdouts are concatenated into a single JSONL.
#
# Usage:
# run_matrix.sh OUTFILE RUNS CLASSES LEVELS ZONES [PARALLEL]
#
# Example (1350 rows, 14-way parallel):
# run_matrix.sh baseline_j0.jsonl 30 \
# fighter,mage,rogue 3,7,12 \
# goblin_warrens,forest_shadows,manor_blackspire,underdark,dragons_lair
set -euo pipefail
outfile=${1:?outfile required}
runs=${2:?runs required}
classes=${3:?classes required}
levels=${4:?levels required}
zones=${5:?zones required}
parallel=${6:-$(nproc)}
repo=$(cd "$(dirname "$0")/.." && pwd)
bin=$repo/expedition-sim
[[ -x "$bin" ]] || { echo "missing $bin — run 'go build ./cmd/expedition-sim' first" >&2; exit 1; }
cd "$repo/sim_results"
tmpdir=$(mktemp -d -t simrun-XXXXXX)
trap 'rm -rf "$tmpdir"' EXIT
errfile=${outfile%.jsonl}.err
: > "$outfile"
: > "$errfile"
# Enumerate cells one per line: "class level zone".
cells=$tmpdir/cells.txt
IFS=, read -ra cls <<< "$classes"
IFS=, read -ra lvs <<< "$levels"
IFS=, read -ra zns <<< "$zones"
for c in "${cls[@]}"; do
for l in "${lvs[@]}"; do
for z in "${zns[@]}"; do
printf "%s\t%s\t%s\n" "$c" "$l" "$z"
done
done
done > "$cells"
ncells=$(wc -l < "$cells")
echo "matrix: $ncells cells × $runs runs = $((ncells * runs)) rows, $parallel workers" >&2
# Fan out: one process per cell. Per-cell stdout goes to its own shard,
# stderr is collected to the shared errfile.
worker() {
local class=$1 level=$2 zone=$3
local shard=$tmpdir/$class-$level-$zone.jsonl
"$bin" -matrix \
-classes "$class" -levels "$level" -zones "$zone" \
-runs "$runs" \
> "$shard" 2>> "$errfile"
}
export -f worker
export bin tmpdir runs errfile
xargs -P "$parallel" -L 1 -a "$cells" bash -c 'worker "$@"' _
# Stitch shards in deterministic order (zone, class, level — matches
# summarize.sh's sort_by) so diff-friendliness survives parallel arrival.
for c in "${cls[@]}"; do
for l in "${lvs[@]}"; do
for z in "${zns[@]}"; do
shard=$tmpdir/$c-$l-$z.jsonl
[[ -f "$shard" ]] && cat "$shard" >> "$outfile"
done
done
done
rows=$(wc -l < "$outfile")
echo "wrote $rows rows → $outfile (errors in $errfile)" >&2