diff --git a/docs/front-ends/llm-referees.md b/docs/front-ends/llm-referees.md index 1f0739e..d06c863 100644 --- a/docs/front-ends/llm-referees.md +++ b/docs/front-ends/llm-referees.md @@ -1,12 +1,12 @@ # LLM referees -An LLM-driven referee — a model that reads the game and decides what happens next — is a first-class consumer of osrlib, not an afterthought. The engine's shape is already the agent loop's shape: typed commands in, typed events out, a full-knowledge view to observe, and a deterministic core that makes every run reproducible. Everything such an agent would consume ships today: the schemas, the referee surface, the authored-content story, and the determinism guarantee. This page assembles the pieces: [the complete program](#the-complete-program) at the end runs as written, and every fragment along the way is an excerpt of it. +An LLM-driven referee (a model that reads the game and decides what happens next) is one of the consumers osrlib is built for. The engine's shape is already the agent loop's shape: typed commands in, typed events out, a full-knowledge view to observe, and a deterministic core that makes every run reproducible. Everything such an agent consumes ships with the library: the schemas, the referee surface, the authored-content story, and the determinism guarantee. [The complete program](#the-complete-program) at the end runs as written, and every snippet along the way comes from it. ## The schemas are the tool definitions -The reference section ships two raw artifacts alongside its pages: [commands.json](../reference/commands/commands.json) and [events.json](../reference/events/events.json) — the complete command and event surfaces as discriminated-union JSON Schemas, keyed on `command_type` and `event_type`. They are generated from the same registries the engine executes, so they cannot drift from what a session will actually accept and emit; [the command schema reference](../reference/commands/index.md) and [the event schema reference](../reference/events/index.md) render the same schemas page by page for human readers. +The reference section ships two raw artifacts alongside its pages: [commands.json](../reference/commands/commands.json) and [events.json](../reference/events/events.json). The command surface is a discriminated-union JSON Schema keyed on `command_type`, and the event surface is one keyed on `event_type`. They are generated from the same registries the engine executes, so they cannot drift from what a session will actually accept and emit. [The command schema reference](../reference/commands/index.md) and [the event schema reference](../reference/events/index.md) render the same schemas page by page for human readers. -The same unions are importable — [`AnyCommand`][osrlib.crawl.commands.AnyCommand] and [`AnyEvent`][osrlib.crawl.events.AnyEvent] — so a Python agent can build its tool definitions in-process instead of shipping files around: +The same unions are importable as [`AnyCommand`][osrlib.crawl.commands.AnyCommand] and [`AnyEvent`][osrlib.crawl.events.AnyEvent], so a Python agent can build its tool definitions in-process instead of shipping files around: ```{.python .no-run} # The whole command surface as one discriminated union: a ready-made tool definition. @@ -21,7 +21,7 @@ assert len(observations["oneOf"]) == len(ALL_EVENT_CLASSES) assert json.loads(json.dumps(tools)) == tools # plain JSON Schema, ready for a tool registry ``` -The whole command surface and the whole event surface, one discriminator field each — an agent framework that accepts JSON Schema tool definitions can load the command union as-is and let the model emit any command in the game, with validation for free. The loop such an agent runs is short (this is a sketch, not a framework): +The whole command surface and the whole event surface have one discriminator field each. An agent framework that accepts JSON Schema tool definitions can load the command union as-is and let the model emit any command in the game, with validation for free. The loop such an agent runs is short (this is a sketch, not a framework): ```{.python .no-run} # Sketch: the agent loop, framework left to the reader. @@ -34,7 +34,7 @@ while not session.mode.terminal: # the party fell, or the adventure is won ## The referee sees everything -The observation side is [`GameSession.view`][osrlib.crawl.session.GameSession.view] with [`Visibility.REFEREE`][osrlib.core.events.Visibility], which returns a [`RefereeView`][osrlib.crawl.views.RefereeView]: the full session state — party internals, monster hit points, session flags, door states, the complete event log — with exactly two things withheld, the RNG internals and the master seed (those live only in the save document). Each group is a field of its own, typed as the session's own model, so an agent reads `view.monsters[0].current_hp` and `view.flags["key"]` off it and serializes the whole observation with `view.model_dump(mode="json")`. The player view is the opposite discipline, an enumerated whitelist; [Views and visibility](../guides/views-and-visibility.md) draws the line precisely. +The observation side is [`GameSession.view`][osrlib.crawl.session.GameSession.view] with [`Visibility.REFEREE`][osrlib.core.events.Visibility], which returns a [`RefereeView`][osrlib.crawl.views.RefereeView]: the full session state (party internals, monster hit points, session flags, door states, the complete event log) with exactly two things withheld, the RNG internals and the master seed, which live only in the save document. Each group is a field of its own, typed as the session's own model, so an agent reads `view.monsters[0].current_hp` and `view.flags["key"]` off it and serializes the whole observation with `view.model_dump(mode="json")`. The player view is the opposite discipline, an enumerated whitelist. [Views and visibility](../guides/views-and-visibility.md) draws the line precisely. ```{.python .no-run} # The referee view is full state — flags, monster internals — minus RNG state and the seed. @@ -45,7 +45,7 @@ dumped = view.model_dump() assert "master_seed" not in dumped and "rng_streams" not in dumped ``` -The event stream carries the same privilege. [`GameSession.execute`][osrlib.crawl.session.GameSession.execute] returns its events unfiltered, and each event is stamped with a visibility: referee-visibility events carry the hidden rolls — surprise, reaction, secret-door detection — that a player-facing front end must strip at its wire (as [the FastAPI pattern](fastapi-pattern.md) does). An in-process referee agent reads them all; they are its perception of what the dice just did. +The event stream is unfiltered for the same reason. [`GameSession.execute`][osrlib.crawl.session.GameSession.execute] returns its events unfiltered, and each event is stamped with a visibility: referee-visibility events include the hidden rolls (surprise, reaction, secret-door detection) that a player-facing front end must strip at its wire, as [the FastAPI pattern](fastapi-pattern.md) does. An in-process referee agent reads them all. They are its perception of what the dice just did. ```{.python .no-run} # The unfiltered event stream is the observation: referee events carry the hidden rolls. @@ -56,15 +56,15 @@ assert any(event.visibility is Visibility.REFEREE for event in result.events) ## The authorial surface -Player commands let the model drive the party's turn; referee commands let it *run the table*. They ride the same envelope and the same rejection discipline as everything else — no separate API, just more entries in the union: +Player commands let the model drive the party's turn. Referee commands let it *run the table*. They use the same envelope and the same rejection discipline as everything else: no separate API, only more entries in the union: -- [`SetFlag`][osrlib.crawl.commands.SetFlag] — record a durable fact (the lever was pulled, the alarm was raised) that listeners and later narration can react to; see [Listeners and flags](../guides/listeners-and-flags.md) -- [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] and [`SpawnNpcParty`][osrlib.crawl.commands.SpawnNpcParty] — open an encounter at a chosen distance, by fixed count or dice -- [`GrantItem`][osrlib.crawl.commands.GrantItem], [`GrantCoins`][osrlib.crawl.commands.GrantCoins], [`AwardXP`][osrlib.crawl.commands.AwardXP] — place rewards directly -- [`SetDoorState`][osrlib.crawl.commands.SetDoorState] — rewrite any door's state anywhere: lock it, wedge it, reveal it -- [`PlaceParty`][osrlib.crawl.commands.PlaceParty] and [`AdvanceTime`][osrlib.crawl.commands.AdvanceTime] — teleport the party, advance the clock -- [`AddJournalEntry`][osrlib.crawl.commands.AddJournalEntry] and [`RecordNote`][osrlib.crawl.commands.RecordNote] — the agent's durable in-world memory: a journal entry speaks to the players and ships in their view, a note is the referee's own margin and stays behind the screen -- [`ActivateQuest`][osrlib.crawl.commands.ActivateQuest], [`RevealObjective`][osrlib.crawl.commands.RevealObjective], [`CompleteObjective`][osrlib.crawl.commands.CompleteObjective], [`CompleteQuest`][osrlib.crawl.commands.CompleteQuest] — advance authored quest state by hand, with one sharp edge: `CompleteQuest` pays nothing. Whoever completes a quest issues its rewards afterwards — the interpreter does exactly that — so a hand-issued completion that expects the payout to follow on its own will strand the party unpaid +- [`SetFlag`][osrlib.crawl.commands.SetFlag] - record a durable fact (the lever was pulled, the alarm was raised) that listeners and later narration can react to. See [Listeners and flags](../guides/listeners-and-flags.md) +- [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] and [`SpawnNpcParty`][osrlib.crawl.commands.SpawnNpcParty] - open an encounter at a chosen distance, by fixed count or dice +- [`GrantItem`][osrlib.crawl.commands.GrantItem], [`GrantCoins`][osrlib.crawl.commands.GrantCoins], [`AwardXP`][osrlib.crawl.commands.AwardXP] - place rewards directly +- [`SetDoorState`][osrlib.crawl.commands.SetDoorState] - rewrite any door's state anywhere: lock it, wedge it, reveal it +- [`PlaceParty`][osrlib.crawl.commands.PlaceParty] and [`AdvanceTime`][osrlib.crawl.commands.AdvanceTime] - teleport the party, advance the clock +- [`AddJournalEntry`][osrlib.crawl.commands.AddJournalEntry] and [`RecordNote`][osrlib.crawl.commands.RecordNote] - the agent's durable in-world memory: a journal entry is written for the players and ships in their view, a note is the referee's own margin and stays behind the screen +- [`ActivateQuest`][osrlib.crawl.commands.ActivateQuest], [`RevealObjective`][osrlib.crawl.commands.RevealObjective], [`CompleteObjective`][osrlib.crawl.commands.CompleteObjective], [`CompleteQuest`][osrlib.crawl.commands.CompleteQuest] - advance authored quest state by hand. `CompleteQuest` pays nothing: whoever completes a quest issues its rewards afterwards, as the interpreter does, so a hand-issued completion leaves the party unpaid unless the agent issues the rewards too ```{.python .no-run} # Referee commands are the authorial surface: record a fact, then spring an ambush. @@ -77,7 +77,7 @@ The rejection contract matters as much here as it does for players: a rejected c ## Narrate from codes, not prose -Events never carry *engine-baked* prose. Each carries a stable message `code` — a compact fact like `session.monsters.spawned` or `encounter.surprise.rolled` — plus typed fields; [the message code reference](../reference/message-codes.md) lists every shipped code with its emitting event class and default template, and each event's fields are on [its schema page](../reference/events/index.md). That is exactly what a narrator model wants: ground truth it can render freely without parsing English back into facts. The one kind of English an event does carry is *authored* narrative — a beat the adventure's author wrote, riding a structured field, which the next section teaches the narrator to treat differently from its own words. When a plain default line is enough, [`format_message`][osrlib.messages.format_message] renders one for any event, appending any authored beat verbatim: +Events never include *engine-baked* prose. Each has a stable message `code` (a compact fact like `session.monsters.spawned` or `encounter.surprise.rolled`) plus typed fields. [The message code reference](../reference/message-codes.md) lists every shipped code with its emitting event class and default template, and each event's fields are on [its schema page](../reference/events/index.md). That is exactly what a narrator model wants: ground truth it can render freely without parsing English back into facts. The one kind of English an event does include is *authored* narrative, a beat the adventure's author wrote in a structured field, which [Narrating authored content](#narrating-authored-content) below treats separately from the model's own words. When a plain default line is enough, [`format_message`][osrlib.messages.format_message] renders one for any event, appending any authored beat verbatim: ```{.python .no-run} # Every event also renders to a default English line the model can lean on. @@ -85,29 +85,29 @@ lines = [format_message(event) for event in result.events] assert all(lines) ``` -A practical narrator prompt sends the structured events (or their codes and fields) as the facts to narrate, and keeps the model's creativity in the telling — the dice already decided what happened. +A practical narrator prompt sends the structured events (or their codes and fields) as the facts to narrate, and keeps the model's creativity in the telling. The dice already decided what happened. ## Narrating authored content An adventure written for the authored layer ([Gates, triggers, and quests](../guides/gates-triggers-quests.md)) arrives with material aimed squarely at a narrating referee, and an agent serving one should use all of it. -**Register the library's [`Interpreter`][osrlib.crawl.interpreter.Interpreter] and do no bookkeeping.** One `session.register_listener(Interpreter(session))` after the session is built (and again after a load), and the triggers, the quests, the fired-marks, and the rewards all play themselves as ordinary logged commands. The agent referees; the adventure runs its own wiring. +**Register the library's [`Interpreter`][osrlib.crawl.interpreter.Interpreter] and do no bookkeeping.** One `session.register_listener(Interpreter(session))` after the session is built (and again after a load), and the interpreter plays the triggers, the quests, the fired-marks, and the rewards as ordinary logged commands. The agent referees, and the interpreter runs the adventure's wiring. -**`guidance` is steering, never script.** [`NarrativeBlock`][osrlib.crawl.narrative.NarrativeBlock] carries a `guidance` field on any authored object, and [`LevelSpec.guidance`][osrlib.crawl.dungeon.LevelSpec] holds whole-level ambience that hangs on no object at all — the TUI barrow's first level reads: +**`guidance` is steering, never script.** [`NarrativeBlock`][osrlib.crawl.narrative.NarrativeBlock] has a `guidance` field on any authored object, and [`LevelSpec.guidance`][osrlib.crawl.dungeon.LevelSpec] holds whole-level ambience that hangs on no object at all. The TUI barrow's first level reads: ```{.python .no-run} --8<-- "examples/tui_crawler/content.py:level-guidance" ``` -A referee-side narrator reads these straight off the adventure document it is refereeing and never prints them verbatim — the same trust posture as an area's description prose, which already flows into narration. No event carries guidance and no view ships it; it is the author talking to the narrator. +A referee-side narrator reads these straight off the adventure document it is refereeing and never prints them verbatim, the same trust posture as an area's description prose, which already flows into narration. No event includes guidance and no view ships it. It is the author talking to the narrator. **Authored beats are text to weave, not paraphrase.** A quest's offer and completion, an objective's progress, a gate's success and refusal arrive as structured fields on player-visible events and rejections, with `speaker` attribution when the author wrote one ("the temple almoner"). Those are the table's words: deliver them as written, in the speaker's voice, and put the model's creativity around them rather than over them. -**[`Command.source`][osrlib.crawl.commands.Command] keeps the agent's hands visible.** Every command the interpreter issues is stamped `trigger:{id}` or `quest:{id}`, so an agent that stamps its own referee commands — or simply leaves them unstamped — leaves a log where its choices and the adventure's consequences never blur. That attribution is what completes the eval story below: replay a trajectory and the log itself says which grants were the model's ideas and which were the adventure playing out. +**[`Command.source`][osrlib.crawl.commands.Command] keeps the agent's hands visible.** Every command the interpreter issues is stamped `trigger:{id}` or `quest:{id}`, so an agent that stamps its own referee commands, or leaves them unstamped, leaves a log where its choices and the adventure's consequences never blur. That attribution is what completes the eval story below: replay a trajectory and the log itself says which grants were the model's ideas and which were the adventure playing out. ## Determinism is the eval story -Every random draw in osrlib comes from a named stream forked from the master seed, so the same seed plus the same command sequence produces the same game, bit for bit. For agent work this is the property that makes everything else tractable: a trajectory — the seed and the list of commands the model chose — is a complete, reproducible record of a run. Re-execute it offline and you get the same events to score; change a prompt and replay the same seeds to regression-test the change; diff two models on identical dungeons. [Determinism, saves, and replay](../guides/determinism-saves-replay.md) covers the exact guarantee and its boundary (identical replays are promised only under an identical engine version). +Every random draw in osrlib comes from a named stream forked from the master seed, so the same seed plus the same command sequence produces the same game, bit for bit. For agent work this is the property that makes everything else tractable: a trajectory (the seed and the list of commands the model chose) is a complete, reproducible record of a run. Re-execute it offline and you get the same events to score. Change a prompt and replay the same seeds to regression-test the change. Diff two models on identical dungeons. [Determinism, saves, and replay](../guides/determinism-saves-replay.md) covers the exact guarantee and its boundary (identical replays are promised only under an identical engine version). ```{.python .no-run} # Determinism is the eval story: same seed, same commands, same trajectory. @@ -214,8 +214,8 @@ assert [e.model_dump(mode="json") for e in rerun.events] == [e.model_dump(mode=" ## Where next -- [Gates, triggers, and quests](../guides/gates-triggers-quests.md) — the authoring side of the guidance and beats this page narrates. -- [Views and visibility](../guides/views-and-visibility.md) — the referee/player projection line this page builds on. -- [Determinism, saves, and replay](../guides/determinism-saves-replay.md) — the reproducibility guarantee behind the eval story. -- [The FastAPI pattern](fastapi-pattern.md) — the other side of the doctrine: serving players who must *not* see what the referee sees. -- [The message code reference](../reference/message-codes.md) — every code an event can carry, with its default English template. +- [Gates, triggers, and quests](../guides/gates-triggers-quests.md) - the authoring side of the guidance and beats this page narrates. +- [Views and visibility](../guides/views-and-visibility.md) - the referee/player projection line this page builds on. +- [Determinism, saves, and replay](../guides/determinism-saves-replay.md) - the reproducibility guarantee behind the eval story. +- [The FastAPI pattern](fastapi-pattern.md) - the other side of the doctrine: serving players who must *not* see what the referee sees. +- [The message code reference](../reference/message-codes.md) - every code an event can have, with its default English template. diff --git a/docs/guides/listeners-and-flags.md b/docs/guides/listeners-and-flags.md index 5504dad..21adafa 100644 --- a/docs/guides/listeners-and-flags.md +++ b/docs/guides/listeners-and-flags.md @@ -1,6 +1,6 @@ # Listeners and flags -Command handlers implement the SRD's rules: movement, combat, searching, spellcasting, and everything else a [`GameSession`][osrlib.crawl.session.GameSession] resolves on its own. They don't know what a fetch quest is, what a lever in a guard room does, or what your game's win condition looks like. That logic belongs to your game, not the engine, and two mechanisms let you add it without forking the library: **listeners**, which watch every command's events and react by executing more commands, and **flags**, a small piece of session state your game reads and writes directly. +Command handlers implement the SRD's rules: movement, combat, searching, spellcasting, and everything else a [`GameSession`][osrlib.crawl.session.GameSession] resolves on its own. They have no rule for a fetch quest, for a lever in a guard room, or for your game's win condition. That logic belongs to your game, not the engine, and two mechanisms let you add it without forking the library: **listeners**, which watch every command's events and react by executing more commands, and **flags**, a small piece of session state your game reads and writes directly. If you'd like to jump right to the code, [the complete program](#the-complete-program) at the end is self-contained and runnable, and every snippet along the way comes from it. @@ -28,7 +28,7 @@ session.register_listener(MoveCounter()) That returned-events list is for events a listener **authors** directly. A listener that reacts by executing its own commands must return an empty list. A nested `session.execute(...)` call already appends that command's events to the session's event log. Returning them again from `handle` would log the same event twice. -Returning an empty list hides nothing from the caller. `execute` notes where the event log ends before it calls each listener, then folds everything logged while that listener ran into the result it hands back. That's the nested commands' events, however deeply they nest, each exactly once and in log order, followed by whatever the listener authored. So the `CommandResult` from a player's `MoveParty` includes the events for the portcullis opening and the journal entry that recorded it, and your front end renders the whole chain from one envelope. +Returning an empty list hides nothing from the caller. `execute` notes where the event log ends before it calls each listener, then folds everything logged while that listener ran into the result it hands back. That's the nested commands' events, however deeply they nest, each exactly once and in log order, followed by whatever the listener authored. So the `CommandResult` from a player's `MoveParty` includes the events for the portcullis opening and the journal entry that recorded the opening, and your front end renders the whole chain from one envelope. The nested-`execute` call matters for a second reason: it re-enters the entire dispatch pipeline, listener loop included. If a listener issues a command from inside `handle`, every registered listener, itself included, runs again against *that* command's events, with whatever `state` happens to be stored in `session.listener_state` at that moment. The outer `handle` call's own state update hasn't landed yet: `execute` only writes `listener_state[key] = state` after `handle` returns, and the outer call is still running. A listener whose trigger condition could look "not yet handled" from that stale perspective needs a re-entrancy guard, or it fires its own reaction over and over. The fetch quest below includes exactly that guard. @@ -68,13 +68,13 @@ session.register_listener(Interpreter(session)) From then on it watches every command's events, matches them against the adventure's authored [triggers](gates-triggers-quests.md#wiring-the-dungeon-with-triggers) and its [`QuestSpec`][osrlib.crawl.quests.QuestSpec]s, and reacts the only way a listener may: by executing referee commands, each stamped `source="trigger:{id}"` or `source="quest:{id}"`. Three properties are worth copying into your own listeners: -- **It returns no events.** Each event it causes comes from a command it executed, and the result envelope picks those up from the log. `handle` returns `[], {}` unconditionally. +- **It returns no events.** Each event it causes comes from a command it executed, and `execute` folds those into the result from the log. `handle` returns `[], {}` unconditionally. - **It keeps no state.** Its `listener_state` slot exists, because `register_listener` creates one, and stays the empty dict for the life of the session. Fired-marks live in `session.fired_triggers`, beats in `session.journal`, and everything else in the world state the commands changed. That's what makes a triggered game replay exactly: a replay runs with no listeners at all, and re-executing the log rebuilds all of that state. - **It has no re-entrancy guard, on purpose.** The fetch quest below needs one because its trigger condition can look unsatisfied from inside its own reaction. The interpreter instead records the fired-mark *before* running a trigger's consequences, so the trigger is already marked when one of its own consequences re-matches it. Re-entrant self-invocation is how one trigger's consequences fire the next, and a depth bound rather than a latch is what stops a cascade. For more information, see [When something doesn't land](gates-triggers-quests.md#when-something-doesnt-land). ## A fetch quest, worked -Most fetch quests belong in the adventure document, where [`QuestSpec`][osrlib.crawl.quests.QuestSpec] defines what to fetch and the interpreter above runs it. [Gates, triggers, and quests](gates-triggers-quests.md#authoring-a-quest) covers that surface, and the TUI crawler's Jade Idol is authored exactly that way (see [the complete front end](../front-ends/tui-crawler.md)). The same errand also works as an example of the game-owned pattern, because everything a quest needs is on this page's surface: a listener that watches events, keeps its own objective state, and acts through commands. The [complete program](#the-complete-program) below includes this listener whole and runs it. +Most fetch quests belong in the adventure document, where [`QuestSpec`][osrlib.crawl.quests.QuestSpec] defines what to fetch and the interpreter above runs it. [Gates, triggers, and quests](gates-triggers-quests.md#authoring-a-quest) covers that surface, and the TUI crawler's Jade Idol is authored exactly that way (see [the complete front end](../front-ends/tui-crawler.md)). The same errand also works as an example of the game-owned pattern, because this page's surface is all it takes: a listener that watches events, keeps its own objective state, and acts through commands. The [complete program](#the-complete-program) below includes this listener whole and runs it. ```{.python .no-run} class FetchQuestListener: diff --git a/docs/guides/views-and-visibility.md b/docs/guides/views-and-visibility.md index 82ccd82..ef42a06 100644 --- a/docs/guides/views-and-visibility.md +++ b/docs/guides/views-and-visibility.md @@ -23,7 +23,7 @@ referee_view = session.view(Visibility.REFEREE) [`PlayerView`][osrlib.crawl.views.PlayerView] is an enumerated whitelist, built straight from session state and never from the event log, so it can't leak a referee-visibility event that happened to mention a hidden number. -You get the adventure's and town's public names and descriptions, the town's list of services, the party's location and facing, the elapsed clock, and the session mode. Each party member has a public sheet ([`MemberView`][osrlib.crawl.views.MemberView]) with an id, name, class, level, current and max hit points, conditions, inventory, and memorized spells, because a player always sees their own characters in full. Unidentified magic items are masked to a category-level description rather than their true name (see [`MagicItemCategory`][osrlib.core.items.MagicItemCategory]), so even a character's own inventory names an item only after the party identifies it. An unidentified enchanted arm is the one exception to "name only": when its display string is built from the base weapon ("a sword with a faint aura"), the view also carries that weapon's `qualities` and `missile_ranges` (enough for a front end to tell a melee declaration from a missile one), while the bonus, any curse, and the template id stay hidden until identification. A weapon-backed item whose display instead comes from its category, such as a staff of striking still reading "a staff", carries neither field, because the display never named the weapon and the fields would single that one item out among its category-mates. +You get the adventure's and town's public names and descriptions, the town's list of services, the party's location and facing, the elapsed clock, and the session mode. Each party member has a public sheet ([`MemberView`][osrlib.crawl.views.MemberView]) with an id, name, class, level, current and max hit points, conditions, inventory, and memorized spells, because a player always sees their own characters in full. Unidentified magic items are masked to a category-level description rather than their true name (see [`MagicItemCategory`][osrlib.core.items.MagicItemCategory]), so even a character's own inventory names an item only after the party identifies it. An unidentified enchanted arm is the one exception to "name only": when its display string is built from the base weapon ("a sword with a faint aura"), the view also includes that weapon's `qualities` and `missile_ranges` (enough for a front end to tell a melee declaration from a missile one), while the bonus, any curse, and the template id stay hidden until identification. A weapon-backed item whose display instead comes from its category, such as a staff of striking still reading "a staff", includes neither field, because the display never named the weapon and the fields would single that one item out among its category-mates. For the map, you get the mapped cells with their edges ([`ExploredLevelView`][osrlib.crawl.views.ExploredLevelView] and [`EdgeView`][osrlib.crawl.views.EdgeView]): every cell the party has walked, every cell the party's own light has shown it, and whatever that light reveals from where the party stands right now. The cells the light has shown persist as map memory in [`DungeonState.seen`][osrlib.crawl.dungeon.DungeonState.seen], so the automap you draw can still show a torchlit room after the party walks on. An undiscovered secret door renders as a plain wall throughout. Known dropped piles and emptied treasure caches in that explored space are in the view too. @@ -56,7 +56,7 @@ assert "lever-east" not in journal_view.model_dump_json() assert referee_after.fired_triggers == ("lever-east",) ``` -Quests draw the same line, one level finer. `PlayerView.quests` contains the **active** quests only, in document order. A quest nobody has been given yet is absent, because an activation clause is wiring like any other, and a finished quest leaves the list, because its record is the journal. Under each quest, only the **revealed** objectives appear. A hidden objective's id is not in the projection at all until its `reveal_when` clause fires or the objective completes, which is why `ObjectiveView.state` needs only `"incomplete"` and `"complete"`. Nothing else about a quest reaches the player view: no clause, no pattern, no condition, no reward, and no `guidance` from any narrative block or level. +Quests draw the same line, one level finer. `PlayerView.quests` contains the **active** quests only, in document order. A quest nobody has been given yet is absent, because an activation clause is wiring like any other, and a finished quest is no longer in `PlayerView.quests`, because its record is the journal. Under each quest, only the **revealed** objectives appear. A hidden objective's id is not in the projection at all until its `reveal_when` clause fires or the objective completes, which is why `ObjectiveView.state` needs only `"incomplete"` and `"complete"`. Nothing else about a quest reaches the player view: no clause, no pattern, no condition, no reward, and no `guidance` from any narrative block or level. ```{.python .no-run} # Active quests only, revealed objectives only, and none of the wiring behind them.