Skip to content

The referee view is typed, one field per group the save keeps (#105) - #136

Merged
mmacy merged 5 commits into
mainfrom
chunk/typed-referee-view
Sep 15, 2026
Merged

mmacy merged 5 commits into
mainfrom
chunk/typed-referee-view

Conversation

@mmacy

@mmacy mmacy commented Sep 15, 2026

Copy link
Copy Markdown
Owner

RefereeView had one field, state, the raw save payload. It now has one field per group session_state writes, minus master_seed and rng_streams, each field the session's own model, so a front-end author reads view.monsters[0].current_hp and view.flags["key"] with the types the reference documents instead of indexing a dict by hand. The counters a save writes under exploration are a new ExplorationCounters model, named as the session names them.

view.model_dump(mode="json") equals session_state(session) without the two withheld keys. Two things hold that up: the command and event logs are annotated SerializeAsAny, 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 no schema_version moves. Removing a public field is a public API removal, stated in the changelog beside the others in the Unreleased section; the version is the maintainer's call, so pyproject.toml is unchanged.

The four existing tests that read RefereeView.state and 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), and mkdocs 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_models has three assertions that contradict the session's own models:

  • view.journal[-1].sourceJournalEntry declares text and rounds only. source is a field of Command, stamped on the command and logged with it, which is what the spec says it is.
  • view.monsters[0].template_idMonsterInstance declares template, a MonsterTemplate, and no template_id property. The id reads as view.monsters[0].template.id.
  • view.encounter.groups[0].monster_ids == (view.monsters[0].id,)EncounterGroup.monster_ids is a list[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 .source and .template_id dropped and the comparison wrapped in tuple(...), and the file came back 6 passed. The other three marked tests pass now, as xpass.

Closes #105

https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs

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
@mmacy
mmacy merged commit d7cd9fa into main Sep 15, 2026
4 checks passed
@mmacy
mmacy deleted the chunk/typed-referee-view branch September 15, 2026 04:33
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.

Give the referee view a typed model instead of a raw dict

1 participant