Repository navigation
Add RollDice authorial adjudication command + release 1.1.0 (#22) - #23
Merged
Merged
Conversation
Add a RollDice command so a referee front end can resolve a freeform chance outcome by rolling through the seeded session — accepted, logged, replayable, and grounded in a typed event — instead of inventing a number or rolling out-of-band. - RollDice(expression) in crawl/commands.py, registered in ALL_COMMAND_CLASSES and AnyCommand. A malformed expression is rejected at construction by a field validator (matching SpawnMonsters.count_dice), so validation stays a pure pre-phase and a bad roll consumes no draw and no log entry. - A dedicated ADJUDICATION_STREAM keeps ad-hoc referee rolls off the mechanical streams, so they never perturb a keyed mechanic's draw sequence. - DiceRolledEvent (referee visibility) carries the expression, total, and the individual die results, with an adjudication.dice_rolled message template. - Tests cover accept/reject, referee visibility, determinism, stream isolation, replay, and legality in every mode; docs and CHANGELOG updated.
Bump the version for the RollDice authorial adjudication command (#22). Minor release: the change is purely additive to the public command/event surface. Rename the CHANGELOG [Unreleased] section to [1.1.0] and refresh the compare links; regenerate uv.lock. The local dry run passed: uv build, check_dist, and a fresh-venv install_smoke all clean for 1.1.0. Versioned docs (mike) adoption — the AGENTS.md trigger for this first post-1.0 minor with API changes — is consciously deferred to keep the release focused; tracked in #24.
Owner
Author
|
Folded the 1.1.0 release into this PR (following the v1.0.0 precedent of bumping within the impl PR and tagging its merge commit):
Versioned-docs (mike) adoption — the AGENTS.md trigger for this first post-1.0 minor with API changes — is consciously deferred to keep the release focused; tracked in #24. On merge, an annotated |
mmacy
added a commit
that referenced
this pull request
Jul 30, 2026
The light reveal (1.2.1) was deliberately ephemeral: the player view recomputed what the party sees by its own light on every build, so the moment the party walked on — or the torch guttered out — every cell it had glimpsed but never stepped on fell dark again. A front end's automap could therefore only remember the physically walked footprint, and a mapped room vanished behind the party (osr-web issue #23). The client cannot fix this itself; the projection is its only source of geometry. Persist sight as its own state, distinct from exploration: - `DungeonState.seen` holds the remembered cells beside `explored`, same `"{dungeon}:{level}"` key shape. `mark_seen` is an idempotent bulk append, new cells in sorted (x, y) order so serialization is deterministic across runs. - `GameSession.execute` folds the current light reveal into `seen` after every accepted command (`_persist_sight`) — the one hook that covers every reveal-changing action uniformly: entering, moving, stairs, doors, lighting, placement, discovery, battle-flight relocation. Rejected commands change no state, so they never reach it. - The projection unions walked, remembered-seen, and currently lit cells; the live reveal stays so lighting a torch reveals immediately, even when a light effect lands on the ledger without a command. - `_light_reveal` and its radius helpers move from views.py to exploration.py, beside `_sight_passes` — they are game logic now, not projection. Seen is memory of sight, never exploration: movement cost still reads the walked footprint (a lit-but-unwalked cell costs the full 30), drop-pile visibility stays on explored cells, an undiscovered secret door's sealed alcove is never recorded, and the referee view is unchanged. The field is additive with a default — no schema bump, no migration; an older save loads with empty memory and starts remembering. Adds TestSeenPersistence (walk-away persistence, save/load round-trip, pre-`seen` save loads clean, sealed-alcove no-leak) and mark_seen idempotence/ordering coverage; the leak property test's invariant becomes explored-or-seen, and the cross-level test now expects level 1's memory to survive the descent while the live reveal still never bleeds across levels. Claude-Session: https://claude.ai/code/session_01Gu9KVvucLb7NoquyNjmxxv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #22.
Adds an authorial
RollDicecommand so a referee front end (the osrlib-referee MCP server) can resolve a freeform chance outcome by rolling through the seeded session — accepted, logged, replayable, and grounded in a typed event — instead of inventing a number or rolling out-of-band. This completes the authorial surface for freeform adjudication described indocs/front-ends/llm-referees.md.What's in scope
RollDiceonly. Per the plan's Correction 1,CheckandSaveare deferred to their own follow-up issues (Saveis not a thin wrapper —combat.saving_throwis entity-coupled).RollDicealone unblocks the front end.Design
RollDice(expression: str)incrawl/commands.py, appended toALL_COMMAND_CLASSES(stable, additive wire order) and reachable viaAnyCommand/parse_command. Legal in every mode (inherits_ALL_MODES).expressionis rejected at construction by a@field_validatorcallingdice.parse, exactly asSpawnMonsters.count_diceis. This keepsRejectionreserved for state/legality and matches the existing dice-string precedent. This deliberately supersedes the issue's literal acceptance-criterion wording ("returnsaccepted=Falsewith aRejection"): a bad expression surfaces as a parse-timeContentValidationErrorthe front end catches, not aCommandResult. The RNG draw happens strictly after validation, so a malformed roll consumes no draw and no log entry — the same rejection purity, achieved one layer earlier.ADJUDICATION_STREAM = "adjudication"keeps ad-hoc referee rolls off the mechanical streams, so an adjudication roll never perturbs the draw sequence of a keyed mechanic (encounter/combat/exploration) and keyed outcomes stay fixed under replay.DiceRolledEvent(referee visibility) carriesexpression,total, and the individualrolls, with a stableadjudication.dice_rolledmessage code and a defaultformat_messagetemplate.Deliberate deviation from the plan's touch-point table
The plan's table listed defining
DiceRolledEventincore/events.pybut registering it incrawl/events.py'sCRAWL_EVENT_CLASSES. I instead define and register it entirely incrawl/events.py. Every otherCRAWL_EVENT_CLASSESmember emitted by a referee handler (FlagSetEvent,MonstersSpawnedEvent,TimeAdvancedEvent,GameOverEvent) is defined incrawl/events.py;DiceRolledEventis emitted only by the crawl session handler_handle_roll_dice, so a split home (define in core, register in the crawl list) would make it the lone crawl-registered event living in core — a "one home per symbol" violation (AGENTS.md greenfield discipline). Noosrlib.corefunction needs to emit it, so core placement buys nothing and the layering rule is upheld either way.Tests
New coverage maps to each acceptance criterion: accept + logged (
TestRollDice), malformed-expression rejection with no draw / no log, referee visibility, byte-identical determinism under a fixed seed, stream isolation (both a "touches only the adjudication stream" diff test and an interleaved-roll-leaves-SpawnMonsters-unshifted test), save/load + command-log replay (test_persistence.py), and legality in every mode. Fixture wiring added totest_commands.py,test_events_kernel.py, and the property-based command fuzzer (test_crawl_properties.py). Docs (rng-streams.md) andCHANGELOG.mdupdated.ruff format --check,ruff check,pytest(1512 passed, 58 skipped), and a strictmkdocs buildare all clean.Review provenance
Followed the AGENTS.md plan → rubber-duck → PR loop; the issue body + owner comment are the ducked plan. A fresh skeptical reviewer audited the implementation against AGENTS.md invariants (determinism, layering, command purity, events/schema discipline, greenfield anti-patterns) and the acceptance criteria. Verdict: SOLID, no blocking findings. It endorsed the event-home deviation as the more consistent choice, confirmed the stream-isolation tests are non-tautological, and verified the message template renders honestly when
total != sum(rolls). Its two non-blocking notes (name the AC-wording supersession explicitly; record the event-home deviation) are folded into this description.