R2: !revisit <N> -- walk back one edge, for +1 threat

Retires forward-only navigation. `!revisit <N>` (alias `!zone revisit`)
walks one graph edge back to a room in VisitedNodes, chosen by the number
the player reads off !map's Path strip. Edges are directed, but a corridor
you walked down is a corridor you can walk back up, so adjacency checks
both directions. !map now prints the legal targets.

Cost is +1 threat, not the discount the plan specced. There was nothing to
discount: movement charges neither threat nor supplies, and a cleared room
fires no combat, so backtracking was free. +1 mirrors harvest noise -- less
than a fight (+5) or an elite (+8). Standalone runs have no threat clock and
pay nothing. A siege guard refuses the move at threat >= 99: siege is
one-way sticky, and a player must not be able to strand their own expedition
on a move they made to go pick up a herb.

The plan claimed terminal CombatSession rows already prevent re-triggering a
cleared room. They gate only Elite and Boss, at the doorway. An Exploration
room re-entered resolveCombatRoom unconditionally and a Trap room re-armed --
so revisit would have been an infinite farm: step back, advance, repeat.
resolveRoom now early-returns on an already-cleared room. No-op forward-only,
since advance clears a room only after resolving it.

Autopilot is ticker-driven with no on/off switch, so it would have walked the
player straight back out of the room they revisited. grantAutorunGrace stamps
last_autorun_at to buy one cooldown window. That is a stopgap; R5 still owes
the real policy.

Fork re-pick stays deferred to R3: revisit refuses while a fork is pending,
because advance and `!zone go` resolve node_choices without checking which
node the player is standing on.
This commit is contained in:
prosolis
2026-07-09 19:47:16 -07:00
parent 31e3d69d8d
commit ae5762fc91
5 changed files with 488 additions and 0 deletions

View File

@@ -113,6 +113,18 @@ func (r *DungeonRun) CurrentRoomType() RoomType {
return r.RoomSeq[r.CurrentRoom]
}
// RoomIsCleared reports whether the player has already resolved the room at
// the given path index. Sticky: once cleared, a room stays cleared for the
// life of the run, however many times the player walks back through it.
func (r *DungeonRun) RoomIsCleared(idx int) bool {
for _, c := range r.RoomsCleared {
if c == idx {
return true
}
}
return false
}
// generateRoomSequence builds the deterministic-but-seeded room layout
// for a run of the given zone. The boss room is always last; one Entry
// is always first; one Trap and one Elite room sit between explorations.