Repository navigation
Tell explorer states apart by the turns left of each timer - #236
Conversation
The fingerprint ignored State.timers, so a state after a few turns of waiting was the same as the start, and an ending that a timer triggers was never reached. With the turns left in the key, the two-robot demo gets its water clock back: the timer water floods the station after eight turns, and river_wins names closes: [drained]. The demo and the Lock-Keeper slice now have more states than their budgets; the tests raise them to 20,000. Closes #210
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🟢 Approved
The change is narrowly focused on preventing incorrect state merging by including running timers in the exploration fingerprint and is backed by corresponding regression tests plus aligned budget/documentation updates, with no remaining issues indicated in the provided review evidence.
No open issues 🎉
What changed in this PR
This pull request updates the state explorer to distinguish states by the remaining turns on running timers, fixing cases where “waiting” previously produced no observable state change and could prevent timer-triggered endings from ever being reached. It also restores the intended timer-driven loss condition (“water clock”) in the examples/two-robots demo and adjusts tests/budgets and docs to match the larger explored state space.
Changes:
- Include running timers’ turns-remaining in the explorer’s state fingerprint to prevent incorrect state merging across time.
- Reintroduce the
two-robotswater-clock timer and a timer-driven ending (river_wins→flooded) with appropriate metadata. - Add a regression test for timer-triggered endings and raise exploration budgets in impacted runtime tests; update docs/changelog accordingly.
| File | Summary |
|---|---|
examples/two-robots/timers/water.yaml |
Adds/restores an always-running 8-turn “water clock” timer with a warning at 3 turns remaining. |
examples/two-robots/rules/water.yaml |
Adds river_wins timer rule to end the game as flooded when the timer runs out, including closes and a reason. |
examples/two-robots/README.md |
Documents the restored time limit and notes the resulting exploration/state-count impact. |
examples/lock-keeper-slice/PENDING.md |
Updates notes to reflect increased exploration needs after timer-aware fingerprinting. |
crates/loremaster-runtime/tests/two_robots.rs |
Raises exploration budget and validates the timer-driven ending behavior (including point-of-no-return acceptance via closes). |
crates/loremaster-runtime/tests/slice.rs |
Raises exploration budget to accommodate additional state splitting from timers. |
crates/loremaster-runtime/tests/ending_checks.rs |
Adds regression coverage proving a running timer can reach and trigger an ending during exploration. |
crates/loremaster-runtime/src/explore.rs |
Updates explorer fingerprinting to include running timers (turns left), ensuring time progression affects state identity. |
CHANGELOG.md |
Records the explorer fix and the restored timer-driven demo behavior. |
Review effort: Balanced
| game.agenda_waits(), | ||
| &s.active, | ||
| &s.orders, | ||
| &s.timers, |
There was a problem hiding this comment.
[SUGGESTION]: State space now scales with each timer's remaining-turn count
Adding the exact turns left of every running timer to the fingerprint multiplies the explored state space by roughly the timer's length (and multiplicatively across timers). The budget updates here cover examples/two-robots (8 turns) and the Lock-Keeper slice (3 turns), but examples/zork1/timers/lamp_battery.yaml runs for 385 turns while lamp.lit, and any user story with a long timer has the same shape. Every loremaster explore of such a story now truncates against the default 2000-state budget much earlier, and the cost grows without bound with the timer's turns value. The exact count is needed for correctness (the timer's future depends on when it reaches its at thresholds and 0), so consider documenting that cost for long timers, or coarsening the key to the timer's meaningful thresholds (at keys and 0) when no turn-granularity interaction needs the intermediate values.
There was a problem hiding this comment.
Documented in 1c05364 (docs/writing-stories.md, ending checks: a timer of n turns can make the walk up to n times larger, so give it a bigger budget or a shorter timer). I kept the exact count. Bucketing to the at keys and 0 is not exact: two states that differ in turns left reach a threshold on different turns, and timer.<id> is a number that conditions can read. That is the same trade-off as time bands, and #239 is open for that decision. Measured on zork1 with the default budget: the loremaster explore output is byte for byte the same on main and on this branch (2000 states, budget reached in both, the same 91 unreached rooms and 52 never-fired rules). The lamp timer only runs while the lamp is lit, and the walk is already cut by the budget.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Verification Notes
Files Reviewed (2 files)
Previous Review Summary (commit b29f162)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit b29f162)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Verification Notes
Files Reviewed (9 files)
Reviewed by deepseek-v4.1-flash-1017 · Input: 35.3K · Output: 5.9K · Cached: 278.3K |
# Conflicts: # CHANGELOG.md
Closes #210
What and why
The explorer's state fingerprint left out
State.timers. Waiting changed nothing the fingerprint saw, so the walk merged "three turns later" with "now", and an ending that a timer triggers was never reached. That is why the two-robot demo (#199) had no time limit.explore.rs: the fingerprint has the turns left of each running timer.examples/two-robots: the water clock is back.timers/water.yamlruns for eight turns (the best play needs four or five) with a warning at three left.rules/water.yaml(river_wins) ends the game asfloodedwhen it runs out, withcloses: [drained]and a reason. The README says so.The cost is more states. Two-robots goes from 1294 to 3758 states, above the default budget of 2000, so its test raises the budget to 20,000.
loremaster explore examples/two-robotswith the default budget now stops early and says so. The Lock-Keeper slice has thelock_draintimer and went over its test budget of 5000, so that test now walks up to 20,000 too. The Lock-Keeper demo stays within its 70,000.How verified
a_timer_reaches_its_ending(ending_checks): a cell with a running three-turn timer whose rule ends the game. It fails onmain(onlyescapedis found) and passes now.explorer_terminates_and_every_ending_is_reachable(two_robots) also checks thatriver_winsfires and that itsclosesaccepts at least one point of no return. The checks are proven, with no diagnostics.loremaster checkandloremaster test examples/two-robots: all 3 walkthroughs pass.cargo fmt --all -- --check,cargo clippy --workspace --all-targets --no-default-features -- -D warnings,cargo test --workspace --no-default-features.