R1: split "where am I" from "how far have I walked"

Backtracking (revisit R2) breaks the assumption every zone-run caller
quietly relied on: that CurrentRoom == len(VisitedNodes)-1. Position and
progress have been the same number only because navigation was
forward-only.

Split them. CurrentRoom is now the first-entry index of CurrentNode in
VisitedNodes -- the room number the player was shown on the way in, and
the salt that enemy/trap/harvest/encounter keys hash. A revisited room
therefore resolves to the same room. RoomsTraversed is a new monotonic
step counter, persisted, backfilled from visited_nodes for in-flight rows.

The audit found only two progress-shaped reads. narrationCadence moves to
RoomsTraversed so a backtracking player draws fresh flavor rather than
replaying the entry-room lines. The other was a latent bug: zoneCmdGo
labelled the room it moved into as CurrentRoom+1, which is only correct at
the frontier; advanceZoneRunNode now returns the true path index.

VisitedNodes becomes an ordered set and RoomsCleared becomes idempotent --
both no-ops while navigation is forward-only, both load-bearing after R2.

No player-visible behavior change. Nothing to route off RoomsTraversed for
threat/SU: verified at HEAD that movement charges neither. Threat comes
from combat, supplies burn per day. The revisit plan's cost model claimed
otherwise and has been corrected -- R2's "discount" is really a net-new
cost decision.
This commit is contained in:
prosolis
2026-07-09 19:36:24 -07:00
parent adcc62112b
commit 31e3d69d8d
9 changed files with 418 additions and 36 deletions

View File

@@ -330,16 +330,26 @@ func eliteRoomEntryLine(zoneID ZoneID, runID string, roomIdx int) string {
return "🎭 **TwinBee:** " + line
}
// narrationCadence returns the salt index used to seed narration
// pickers — the count of nodes visited so far (0-based, matching the
// pre-G6 CurrentRoom semantics). Both linear-mode and graph-mode
// advance bump len(VisitedNodes) in lockstep with CurrentRoom, so this
// preserves existing pick determinism while letting G9 drop the legacy
// current_room column without re-keying every flavor pool seed.
// narrationCadence returns the salt index used to seed narration pickers —
// the number of rooms walked so far, 0-based. This is a *progress* read, not
// a position read (revisit R1's audit split): flavor should keep moving
// forward as the player does, so a backtrack draws fresh lines rather than
// replaying the ones they already read on the way in.
//
// Forward-only runs have RoomsTraversed == len(VisitedNodes), so this stays
// numerically identical to the pre-R1 derivation and every existing pick
// keeps landing on the same line. It is stable while the player stands
// still, which is what lets a re-read of `!status` print the same prose.
//
// The len(VisitedNodes) fallback covers rows that predate the R1 column and
// have yet to be swept by bootstrapRoomsTraversed.
func narrationCadence(run *DungeonRun) int {
if run == nil {
return 0
}
if run.RoomsTraversed > 0 {
return run.RoomsTraversed - 1
}
if n := len(run.VisitedNodes); n > 0 {
return n - 1
}