Skip to content

Tell explorer states apart by the turns left of each timer - #236

Merged
sunsided merged 4 commits into
mainfrom
feature/explore-timers
Nov 9, 2026
Merged

sunsided merged 4 commits into
mainfrom
feature/explore-timers

Conversation

@sunsided

@sunsided sunsided commented Nov 9, 2026

Copy link
Copy Markdown
Owner

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.yaml runs for eight turns (the best play needs four or five) with a warning at three left. rules/water.yaml (river_wins) ends the game as flooded when it runs out, with closes: [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-robots with the default budget now stops early and says so. The Lock-Keeper slice has the lock_drain timer 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

  • New test a_timer_reaches_its_ending (ending_checks): a cell with a running three-turn timer whose rule ends the game. It fails on main (only escaped is found) and passes now.
  • explorer_terminates_and_every_ending_is_reachable (two_robots) also checks that river_wins fires and that its closes accepts at least one point of no return. The checks are proven, with no diagnostics.
  • loremaster check and loremaster 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.

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
Copilot AI balanced review requested due to automatic review settings November 9, 2026 01:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-robots water-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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kilo-code-bot

kilo-code-bot Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Verification Notes

  • Incremental review from b29f162 to HEAD 1c05364: the only new PR change is commit 1c05364, which adds one line to docs/writing-stories.md (line 689).
  • The previous SUGGESTION (exact per-timer remaining turns blow up the state space, crates/loremaster-runtime/src/explore.rs:134) is resolved: the doc now states that a timer of n turns can make the walk up to n times larger and tells authors to raise the budget or shorten the timer. The author's reply also documents a measured zork1 run with identical output on main and this branch under the default budget.
  • Re-checked the current fingerprint: State.timers is BTreeMap<Id, u32> (state.rs:72) and is serialized in the nested tuple at explore.rs:129-135, so the new doc bullet matches the implementation. Growth is bounded by the largest timer for timers that always run; timers with different runs_while conditions can compound, which the guidance still covers by advising a larger budget.
  • loremaster explore's default of 2000 states / 40 commands is documented at writing-stories.md:1119, so the new bullet's "bigger budget" advice is actionable.
  • Remaining .github, Cargo.lock, macro, combat, and job-registry changes in b29f162..HEAD come from merging origin/main (Combat: special rules per enemy and a target choice #190, world! and builder syntax for await, combat and stats #191, Weave/thread-based testing #185, Combat: target choice, special damage per enemy, host rolls #233); they are not part of this PR's diff and were not reviewed.
  • Tests were not executed (read-only review).
Files Reviewed (2 files)
  • docs/writing-stories.md - no issues
  • crates/loremaster-runtime/src/explore.rs - previous suggestion re-verified as addressed
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

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
crates/loremaster-runtime/src/explore.rs 134 Adding each running timer's exact remaining turns to the fingerprint multiplies the state space by the timer length (and across timers). Budgets were raised only for two-robots (8 turns) and the Lock-Keeper slice (3 turns); examples/zork1/timers/lamp_battery.yaml runs 385 turns, so loremaster explore truncates much earlier there. Document the cost for long timers or coarsen the key to meaningful thresholds if turn-granularity is not required.

Verification Notes

  • Reviewed all 9 changed files against the current HEAD (b29f162).
  • The fingerprint change is otherwise sound: exact timer counts are required for correctness, every at effect's persistent result lands in fields already in the fingerprint (facts, locations, timers, room, ended), and the tuple nesting stays within serde's 16-element limit.
  • examples/two-robots/rules/water.yaml (on: { timer: water }, end: flooded, closes: [drained], reason) matches the Rule schema, and flooded/drained exist in endings/.
  • timers/water.yaml (turns: 8, running: true, at: 3) is valid; 3 < turns, so the validator's "never reaches start value" warning does not apply.
  • The new test a_timer_reaches_its_ending is well-formed: only escape exists as a rule, so 3 turns is the true shortest path to caught.
  • Tests were not executed (read-only review), so the raised budgets and state counts rely on the PR author's reported runs.
Files Reviewed (9 files)
  • crates/loremaster-runtime/src/explore.rs - 1 suggestion
  • crates/loremaster-runtime/tests/ending_checks.rs - no issues
  • crates/loremaster-runtime/tests/slice.rs - no issues
  • crates/loremaster-runtime/tests/two_robots.rs - no issues
  • examples/two-robots/rules/water.yaml - no issues
  • examples/two-robots/timers/water.yaml - no issues
  • examples/two-robots/README.md - no issues
  • examples/lock-keeper-slice/PENDING.md - no issues
  • CHANGELOG.md - no issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash-1017 · Input: 35.3K · Output: 5.9K · Cached: 278.3K

@sunsided
sunsided enabled auto-merge (squash) November 9, 2026 02:30
@sunsided
sunsided merged commit d4600a1 into main Nov 9, 2026
7 checks passed
@sunsided
sunsided deleted the feature/explore-timers branch November 9, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Explorer: timers in the state fingerprint

2 participants