mirror of
https://github.com/prosolis/gogobee.git
synced 2026-09-14 10:51:09 +00:00
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).
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/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
|
||||
+36
-23
@@ -1,38 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Aggregate h4_baseline.jsonl into a per-(class,level,zone) table.
|
||||
# Columns: class, level, zone, n, p50 yield, mean yield, mean rooms, %cleared, %tpk, %fled, %extracted.
|
||||
# Aggregate a sim corpus JSONL into a per-(class,level,zone) table.
|
||||
# Columns: class, level, zone, n, p50 yield (all), p50 yield among cleared,
|
||||
# mean yield, mean rooms, %cleared, %boss-reached, %tpk, %fled, %extracted.
|
||||
#
|
||||
# boss-reached counts runs whose StopCode is "boss" or "complete" — i.e.,
|
||||
# the autopilot entered the boss room, whether the run died there or
|
||||
# finished the zone.
|
||||
set -euo pipefail
|
||||
|
||||
infile=${1:-h4_baseline.jsonl}
|
||||
infile=${1:-baseline_j0.jsonl}
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
jq -rs '
|
||||
def p50(xs): if (xs|length)==0 then 0 else (xs|sort|.[(length/2|floor)]) end;
|
||||
|
||||
group_by([.Class, .Level, .Zone])
|
||||
| map({
|
||||
class: .[0].Class,
|
||||
level: .[0].Level,
|
||||
zone: .[0].Zone,
|
||||
n: length,
|
||||
yields: [.[] | .YieldCount],
|
||||
rooms: [.[] | .Rooms],
|
||||
outcomes: [.[] | .Outcome]
|
||||
class: .[0].Class,
|
||||
level: .[0].Level,
|
||||
zone: .[0].Zone,
|
||||
n: length,
|
||||
yields: [.[] | .YieldCount],
|
||||
yields_cleared: [.[] | select(.Outcome=="cleared") | .YieldCount],
|
||||
rooms: [.[] | .Rooms],
|
||||
outcomes: [.[] | .Outcome],
|
||||
stops: [.[] | .StopCode]
|
||||
})
|
||||
| map(. + {
|
||||
p50_yield: (.yields | sort | .[(length/2|floor)]),
|
||||
mean_yield: ((.yields | add) / length),
|
||||
mean_rooms: ((.rooms | add) / length),
|
||||
pct_cleared: (([.outcomes[] | select(. == "cleared")] | length) * 100 / .n),
|
||||
pct_tpk: (([.outcomes[] | select(. == "tpk")] | length) * 100 / .n),
|
||||
pct_fled: (([.outcomes[] | select(. == "fled")] | length) * 100 / .n),
|
||||
pct_extracted: (([.outcomes[] | select(. == "extracted")] | length) * 100 / .n)
|
||||
p50_yield: p50(.yields),
|
||||
p50_yield_cleared: p50(.yields_cleared),
|
||||
mean_yield: ((.yields | add) / .n),
|
||||
mean_rooms: ((.rooms | add) / .n),
|
||||
pct_cleared: (([.outcomes[] | select(. == "cleared")] | length) * 100 / .n),
|
||||
pct_boss_reached: (([.stops[] | select(. == "boss" or . == "complete")] | length) * 100 / .n),
|
||||
pct_tpk: (([.outcomes[] | select(. == "tpk")] | length) * 100 / .n),
|
||||
pct_fled: (([.outcomes[] | select(. == "fled")] | length) * 100 / .n),
|
||||
pct_extracted: (([.outcomes[] | select(. == "extracted")] | length) * 100 / .n)
|
||||
})
|
||||
| sort_by(.zone, .class, .level)
|
||||
| (["class","level","zone","n","p50_yld","mean_yld","mean_rms","%clr","%tpk","%fled","%ext"] | @tsv),
|
||||
(.[] | [.class, .level, .zone, .n, .p50_yield,
|
||||
| (["class","level","zone","n","p50_yld","p50_yld_clr","mean_yld","mean_rms","%clr","%boss","%tpk","%fled","%ext"] | @tsv),
|
||||
(.[] | [.class, .level, .zone, .n,
|
||||
.p50_yield, .p50_yield_cleared,
|
||||
(.mean_yield * 10 | round / 10),
|
||||
(.mean_rooms * 10 | round / 10),
|
||||
(.pct_cleared | round),
|
||||
(.pct_tpk | round),
|
||||
(.pct_fled | round),
|
||||
(.pct_extracted | round)] | @tsv)
|
||||
(.pct_cleared | round),
|
||||
(.pct_boss_reached | round),
|
||||
(.pct_tpk | round),
|
||||
(.pct_fled | round),
|
||||
(.pct_extracted | round)] | @tsv)
|
||||
' "$infile" | column -t -s $'\t'
|
||||
|
||||
Reference in New Issue
Block a user