mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
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).
81 lines
2.4 KiB
Bash
Executable File
81 lines
2.4 KiB
Bash
Executable File
#!/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
|