The referee view is typed, one field per group the save keeps (#105) - #136
Merged
Merged
Conversation
RefereeView had one field, state, the raw save payload, so the view with the most information had the least structure and a front-end author indexed state["dungeon_state"]["..."] by hand. It now has one field per group session_state writes, minus master_seed and rng_streams, each field the session's own model: view.monsters[0].current_hp and view.flags["key"] read with the types the reference documents, and each field's docstring names the model to read next and, where a player is the reader, the player-view field to draw from instead. The nine counters a save writes under "exploration" are the new ExplorationCounters, named as the session names them. view.model_dump(mode="json") equals session_state(session) without the two withheld keys, which is what keeps the view and the save from drifting apart. Two mechanisms hold that up. The command and event logs are annotated SerializeAsAny, so each entry serializes with its own subclass fields rather than the base class's, and an event-log entry a save carried as a raw mapping passes through as the dict it is. The builder copies the session's mutable models rather than referencing them, so the view is a snapshot: the session playing on, or a caller editing a session model, leaves the view as it was. Commands, events, and journal entries are frozen records and go in as they are. Persistence is untouched: session_state stays the save's serializer, and no schema_version moves. Removing a public field is a public API removal, stated in the changelog beside the others in this Unreleased section; the version is the maintainer's call, so pyproject.toml is unchanged. The four existing tests that read RefereeView.state now read the typed fields, and the three documentation pages whose prose and runnable examples read it do too. One assertion in the acceptance test cannot pass and the test stays marked expected-to-fail: test_the_groups_are_the_sessions_own_models reads view.journal[-1].source, view.monsters[0].template_id, and compares view.encounter.groups[0].monster_ids against a tuple. JournalEntry has only text and rounds (source is a field of Command), MonsterInstance has template rather than template_id, and EncounterGroup.monster_ids is a list. Each would need a model outside this chunk's files changed, so the test is left exactly as committed. Claude-Session: https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs
The acceptance test named a journal entry source field, a monster template_id attribute, and a tuple for a group's monster ids, none of which the session's models have. The test now reads the template id through the template and compares the ids as a tuple, and the source assertion is gone, because a journal entry records text and a clock position only. Claude-Session: https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs
…re the frozen records (#105) Three corrections from the review of the typed referee view. The class docstring said the model being frozen means nothing updates the view in place. What frozen fixes is the set of fields, not their contents: rebinding view.flags raises, while view.flags["key"] = 1 and view.monsters[0].current_hp = 0 both succeed and edit the view's own copies. The docstring now states that rule, and says that editing what you find on a view changes nothing on the session, which is the fact a reader needs. The class and builder docstrings called the view an expensive object that copies the whole event log and told the reader to build one sparingly. The event log is shared by reference and the builder measures in fractions of a millisecond, so both claims are gone. In their place the docstrings say which groups are shared and why: a command, an event, and a journal entry are frozen records of something that has already happened, so copying them would protect nothing. The builder applied that rule unevenly, deep-copying four frozen models. The ruleset, the death records, and the defeated-monster records now go in as they are, their fields being scalars and enums with no container to edit. The adventure keeps its copy, because the authored tree contains dicts, the town's travel turns and each level's edges, that a caller can edit in place; the adventure field's docstring now says so. The event_log field docstring also states what the SerializeAsAny[Event] | dict union relies on: an Event instance validates as the event, a mapping falls to the dict arm under strict validation, and a raw entry therefore passes through unchanged. Claude-Session: https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs
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.
RefereeViewhad one field,state, the raw save payload. It now has one field per groupsession_statewrites, minusmaster_seedandrng_streams, each field the session's own model, so a front-end author readsview.monsters[0].current_hpandview.flags["key"]with the types the reference documents instead of indexing a dict by hand. The counters a save writes underexplorationare a newExplorationCountersmodel, named as the session names them.view.model_dump(mode="json")equalssession_state(session)without the two withheld keys. Two things hold that up: the command and event logs are annotatedSerializeAsAny, so each entry serializes with its own subclass fields and a raw-mapping event-log entry passes through as the dict it is, and the builder copies the session's mutable models, so the view is a snapshot that play going on afterwards leaves alone. Persistence is untouched and noschema_versionmoves. Removing a public field is a public API removal, stated in the changelog beside the others in theUnreleasedsection; the version is the maintainer's call, sopyproject.tomlis unchanged.The four existing tests that read
RefereeView.stateand the three documentation pages whose prose and runnable examples read it now read the typed fields.The gate passes in the worktree:
ruff format --check(157 files),ruff check,pyright(0 errors),pytest(2906 passed, 146 skipped, 7 xfailed, 3 xpassed), andmkdocs build --strict.One assertion cannot pass, and its test stays marked expected-to-fail.
tests/test_referee_view.py::TestTheRefereeViewIsTyped::test_the_groups_are_the_sessions_own_modelshas three assertions that contradict the session's own models:view.journal[-1].source—JournalEntrydeclarestextandroundsonly.sourceis a field ofCommand, stamped on the command and logged with it, which is what the spec says it is.view.monsters[0].template_id—MonsterInstancedeclarestemplate, aMonsterTemplate, and notemplate_idproperty. The id reads asview.monsters[0].template.id.view.encounter.groups[0].monster_ids == (view.monsters[0].id,)—EncounterGroup.monster_idsis alist[str], so it never equals a tuple.Each would need a model in a file outside this chunk's list changed, and the first and third would change the save payload as well, so the test file is exactly as committed. With those three lines adjusted the test passes: I ran a copy with
.sourceand.template_iddropped and the comparison wrapped intuple(...), and the file came back 6 passed. The other three marked tests pass now, as xpass.Closes #105
https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs