Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- The API reference has a front page for `osrlib.core` and for `osrlib.crawl`, at the top of each layer's section, rendering that package's docstring, so the crawl package's end-to-end program is on the published site (#101).
- Four event fields say where something came from, so a front end reads the fact off the event instead of rebuilding it from where the party is standing (#67, #68, #70, #71). [`LocationEnteredEvent`][osrlib.crawl.events.LocationEnteredEvent]`.via` gives, on a level or dungeon entry, the transition's `kind` for [`UseStairs`][osrlib.crawl.commands.UseStairs], `"trap"` for a trap that dropped the party through the floor, `"entrance"` for [`EnterDungeon`][osrlib.crawl.commands.EnterDungeon], and `"placed"` for a referee's [`PlaceParty`][osrlib.crawl.commands.PlaceParty], and `.transition_ref` names the cell the transition stands on, as [`cell_ref`][osrlib.crawl.dungeon.cell_ref] writes it, so a stairs crossing maps back to its [`TransitionSpec`][osrlib.crawl.dungeon.TransitionSpec] and the gate on it, which is what a narrator needs to read the gate's `guidance` beside the success beat. [`ItemAcquiredEvent`][osrlib.crawl.events.ItemAcquiredEvent]`.origin` says where goods came from, `"treasure"` for a share of a haul, `"purchase"` for gear bought in town, and `"grant"` for a referee's [`GrantItem`][osrlib.crawl.commands.GrantItem] or [`GrantCoins`][osrlib.crawl.commands.GrantCoins], so a reward counted out per member never reads as a pot split. A room trap found through a door names that door on both surfaces the find has: [`TrapEvent`][osrlib.crawl.events.TrapEvent]`.direction` carries the bearing, and the search token takes a third segment, `room_trap:<area id>:<direction>`, while a trap found inside its own area keeps the bare token and no bearing. The engine always fills the four fields, and each defaults `None` so a log written before schema 4 still parses. The default formatter reads them, so a stairs crossing reads as the climb or the descent it was and a door-edge trap find names the door.
- An unidentified enchanted arm's public combat facts reach the player view, matching an identified one. [`MemberView`][osrlib.crawl.views.MemberView]'s masked inventory gives an unidentified magic item `qualities` and `missile_ranges` when its masked display string was built from that base weapon ("a dagger with a faint aura"). An item whose display comes from its category instead, such as a `staff_of_striking` reading "a staff", shows neither field, and neither does a shield or a ring. On the base-weapon path the two fields reveal nothing the display string does not already: they are rulebook facts about the mundane weapon underneath, not about the enchantment, and the bonus, any curse, the template id, and the true name all stay hidden. Without them a front end cannot tell a melee declaration from a missile one for an item it cannot yet name.
- A passage is a transition the party walks through instead of taking with [`UseStairs`][osrlib.crawl.commands.UseStairs]. Use one where a map shows two cells side by side that the grid can't put side by side, like the floors of two levels that meet, or two halves of one map that you lay out apart. A [`TransitionSpec`][osrlib.crawl.dungeon.TransitionSpec] whose `kind` is `"passage"` names the `side` of its cell that it stands on, and that side is a wall in the level's edges. A [`MoveParty`][osrlib.crawl.commands.MoveParty] from the cell across that side lands the party on the passage's destination. The move makes the usual can't-move checks, then evaluates the passage's gate as `UseStairs` does, and it costs what a step onto the landing cell costs. It reports what `UseStairs` reports for the same destination, with `via="passage"`, and then a [`PartyMovedEvent`][osrlib.crawl.events.PartyMovedEvent] for the landing cell. [`LevelSpec.passage_at`][osrlib.crawl.dungeon.LevelSpec.passage_at] finds a passage by cell and side, and [`LevelSpec.transition_at`][osrlib.crawl.dungeon.LevelSpec.transition_at] and `UseStairs` ignore passages. The player view reports a passage's side as `"open"`, and sight, light, and fighting space treat it as the wall it is. [`validate_adventure`][osrlib.crawl.adventure.validate_adventure] refuses a passage whose side isn't a wall. It also refuses a passage whose far cell, the cell one step across its side, is off the grid, in an area, or not walled on its other three sides, and a passage that lands on its own cell or on any passage's far cell. It refuses two passages on the same cell and side, and a trap whose transition is a passage. A new kind value and an optional field are additive within a schema version, so [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. No draw sequence or golden file changed.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion docs/adaptations.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ The detection-chance precedence is thief row, then class tag, then the universal

### The time-cost census and the thirds odometer

Zero time: `TurnParty`, `ReorderParty`, `OpenDoor`, `CloseDoor`, `WedgeDoor`, `ForceDoor` (a moment of violence, and the noise is the cost), `ListenAtDoor` (the once-per-door cap is the limiter), equip/unequip, `DropItems`, `ExtinguishSource`, `PurchaseEquipment` (town time is abstract), and all referee commands. One round: `CastSpell` and `TurnUndead` outside battle, `LightSource` (per tinder attempt too, RAW). One turn: `Search`, `InspectTreasure`, `RemoveTreasureTrap` (pinned symmetric, where RAW is silent), `PickLock` (pinned, where RAW is silent and the skill is delicate work), `TakeTreasure` (RAW's packing-bags turn), `Rest(turn)`. On the odometer: `MoveParty` per cell and `UseStairs` at one unexplored-cell cost (pinned). Stated durations: `Rest(night)` 48 turns, `Rest(day)` 144, `PrepareSpells` 6, travel per content. Movement accrues in thirds-of-feet, an unexplored cell 30 units and an explored cell 10, implementing the SRD's three-times-through-familiar-areas rule exactly, and turn-costing actions snap the clock to the next boundary, absorbing the partial move. Locked by `test_exploration.py::TestTimeCostCensus` and `TestOdometer`.
Zero time: `TurnParty`, `ReorderParty`, `OpenDoor`, `CloseDoor`, `WedgeDoor`, `ForceDoor` (a moment of violence, and the noise is the cost), `ListenAtDoor` (the once-per-door cap is the limiter), equip/unequip, `DropItems`, `ExtinguishSource`, `PurchaseEquipment` (town time is abstract), and all referee commands. One round: `CastSpell` and `TurnUndead` outside battle, `LightSource` (per tinder attempt too, RAW). One turn: `Search`, `InspectTreasure`, `RemoveTreasureTrap` (pinned symmetric, where RAW is silent), `PickLock` (pinned, where RAW is silent and the skill is delicate work), `TakeTreasure` (RAW's packing-bags turn), `Rest(turn)`. On the odometer: `MoveParty` per cell, a passage at the cost of a step onto its landing cell, and `UseStairs` at one unexplored-cell cost (pinned). Stated durations: `Rest(night)` 48 turns, `Rest(day)` 144, `PrepareSpells` 6, travel per content. Movement accrues in thirds-of-feet, an unexplored cell 30 units and an explored cell 10, implementing the SRD's three-times-through-familiar-areas rule exactly, and turn-costing actions snap the clock to the next boundary, absorbing the partial move. Locked by `test_exploration.py::TestTimeCostCensus` and `TestOdometer`.

### Rest, fatigue, sleep, and provisions

Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started/building-an-adventure.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The shape of the tree:

A level is a `width × height` grid. Cells are addressed `(x, y)` with `x` increasing east and `y` increasing south from `(0, 0)` at the northwest corner.

Walls are the default. You declare the exceptions (passages and doors) in the `edges` map. Everything absent from the map is solid wall, including the level boundary. The key `"1,0:west"` is the west side of cell `(1, 0)`; the [`edge_key`][osrlib.crawl.dungeon.edge_key] helper computes the canonical key for any cell and direction:
Walls are the default. You declare the exceptions (open edges and doors) in the `edges` map. Everything absent from the map is solid wall, including the level boundary. The key `"1,0:west"` is the west side of cell `(1, 0)`. The [`edge_key`][osrlib.crawl.dungeon.edge_key] helper computes the canonical key for any cell and direction:

```{.python .no-run}
# The level: a 4x1 corridor, entered at the west end, with a door at the far end.
Expand Down Expand Up @@ -55,7 +55,7 @@ Beyond encounters, an area (or the level itself) can contain:
- [`FeatureSpec`][osrlib.crawl.dungeon.FeatureSpec] - a treasure cache, a construction trick, or custom content for your front end. A cache contains hand-placed items, magic items (any id from [the magic item id index][magic-items-index], like a `sword_plus_1` in a chest), coins, and named valuables ([`ValuableSpec`][osrlib.crawl.dungeon.ValuableSpec]).
- [`TrapSpec`][osrlib.crawl.dungeon.TrapSpec] - room traps on areas, treasure traps on caches. A room trap springs when the party steps in or, with `trigger="open"`, when the party opens one of the area's doors.
- [`AreaTreasureSpec`][osrlib.crawl.dungeon.AreaTreasureSpec] - generated treasure: explicit treasure type letters (see [the treasure type index][treasure-types-index]) or the level's unguarded-treasure band.
- [`TransitionSpec`][osrlib.crawl.dungeon.TransitionSpec] - stairs, trapdoors, and chutes between levels. Transitions live on the level, not the area.
- [`TransitionSpec`][osrlib.crawl.dungeon.TransitionSpec] - stairs, trapdoors, and chutes, which the party takes with [`UseStairs`][osrlib.crawl.commands.UseStairs], and passages, which the party walks through with [`MoveParty`][osrlib.crawl.commands.MoveParty]. A passage joins two cells that a map shows side by side but the grid can't, like the floors of two levels that meet. Transitions live on the level, not the area.
- [`WanderingSpec`][osrlib.crawl.dungeon.WanderingSpec] - the level's wandering-monster check: 1-in-6 every two turns by default, with an optional custom table.

## The dungeon, the town, and the root
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/gates-triggers-quests.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Locks and gates are separate layers: on a door with both, the party must open th

`consumes=True` turns a `has_item` condition into a toll: each time the gated command succeeds, one instance leaves the first holder in marching order, reported by [`ItemConsumedEvent`][osrlib.crawl.events.ItemConsumedEvent] just before the door or arrival event. Every success charges again, so a consumed key-door that swings shut takes another key. Coins are not items and can't be tolled. To charge one, mint a token as a bundled item and gate on that.

A [`NarrativeBlock`][osrlib.crawl.narrative.NarrativeBlock] contains the authored text for the mechanical object it hangs on. Gates read two of its beats: `refusal`, returned in the rejection, and `success`, which goes on the successful command's event, the [`DoorEvent`][osrlib.crawl.events.DoorEvent] for a door and the [`LocationEnteredEvent`][osrlib.crawl.events.LocationEnteredEvent] for a transition that crosses into a new level or dungeon. [`format_message`][osrlib.messages.format_message] appends the beat verbatim, so it shows up in a bare transcript. A transition whose destination is its own level crosses no boundary and emits no arrival event, so a success beat there has nowhere to display. A narrator that wants the gate's `guidance` alongside its success text maps the event back to the object the gate hangs on: a door event names the cell and the side, and a transition's arrival names the cell the transition stands on in `transition_ref`, so neither mapping has to guess from where the party is standing now. The block's other fields are `journal`, `guidance` for an LLM narrator, and `speaker`, each read by the surface that consumes it. A gate's `journal` beat has no consumer at all: journaling a door is [a trigger's](#wiring-the-dungeon-with-triggers) job. No part of a gate's narrative block reaches the player view, which contains no gate setup at all.
A [`NarrativeBlock`][osrlib.crawl.narrative.NarrativeBlock] contains the authored text for the mechanical object it hangs on. Gates read two of its beats: `refusal`, returned in the rejection, and `success`, which goes on the successful command's event, the [`DoorEvent`][osrlib.crawl.events.DoorEvent] for a door and the [`LocationEnteredEvent`][osrlib.crawl.events.LocationEnteredEvent] for a transition that crosses into a new level or dungeon. [`format_message`][osrlib.messages.format_message] appends the beat verbatim, so it shows up in a bare transcript. A transition whose destination is its own level crosses no boundary and emits no arrival event, so a success beat there has nowhere to display. A narrator that wants the gate's `guidance` alongside its success text maps the event back to the object the gate hangs on: a door event names the cell and the side, and a transition's arrival names the cell the transition stands on in `transition_ref`, so neither mapping has to guess from where the party is standing now. A passage stands on one side of its cell, so for a passage crossing the narrator also needs the direction of the [`MoveParty`][osrlib.crawl.commands.MoveParty], and [`LevelSpec.passage_at`][osrlib.crawl.dungeon.LevelSpec.passage_at] finds the passage from the cell and that direction. The block's other fields are `journal`, `guidance` for an LLM narrator, and `speaker`, each read by the surface that consumes it. A gate's `journal` beat has no consumer at all: journaling a door is [a trigger's](#wiring-the-dungeon-with-triggers) job. No part of a gate's narrative block reaches the player view, which contains no gate setup at all.

## Wiring the dungeon with triggers

Expand Down
29 changes: 29 additions & 0 deletions docs/phase-17-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Phase 17 plan: passages

Phase 17 of [the osrlib spec](spec.md) adds the passage, a transition the party crosses by walking across one side of its cell with `MoveParty`. osr-forge's map tracing writes passages where two levels' floors meet on a printed map, and re-pod writes each one as a FRUA transfer. The cross-project design is the "Passages" section of re-pod's `docs/plans/map-tracing.md`, and passages ship in osrlib 2.0.

## Chunks

The phase was one chunk, `passage`, built under the suite's working method.

- **Contract**, pull requests 153 and 154: the `"passage"` kind and the `side` field on `TransitionSpec`, the `LevelSpec.passage_at` stub, the spec's passage bullet and roadmap entry, and `tests/test_passages.py`. Pull request 154 added a test that a passage may land on another level's cell with its own coordinates, and on a cell whose coordinates match a far cell on a different level.
- **Implementation**: the model's side rule, `passage_at`, `transition_at` and `UseStairs` ignoring passages, the passage branch of `MoveParty`, the player view's open side, and the `validate_adventure` refusals.

## What the contract pins

- **Event order.** A crossing to another level emits the level or dungeon `LocationEnteredEvent` first, with `via="passage"` and the passage's cell as `transition_ref`. The swung-shut events of the doors left behind come next, then a `PartyMovedEvent` for the landing cell, then the landing cell's own events. A crossing within one level emits the `PartyMovedEvent` first, and doors the party opened swing shut as after any step.
- **Cost.** A crossing costs what a step onto the landing cell costs: 10 odometer units if the party has explored it, else 30.
- **Refusal lines.** Each `validate_adventure` refusal has an exact line, because osr-editor's diagnostics match osrlib's messages by pattern. The far-cell check covers the far cell's three sides other than the passage's own, so an open passage side is reported once.
- **Schema.** A new kind value and an optional field are additive within schema 4, so `SCHEMA_VERSION` doesn't move.

## Verification

The acceptance tests pass with the marker removed, and the standing gate passes. A fresh review also ran these cases, which no test covers, and each behaved as the plan says:

- A keyed encounter on the landing cell starts after the `PartyMovedEvent`.
- A gated passage within one level takes its toll and shows no success narration.
- An overloaded party can't cross.
- A refused gate leaves the whole dungeon state unchanged.
- Arriving on a passage cell by stairs, by a trap slide, or at the entrance fires nothing.
- The far cell stays out of the player view under a lit torch.
- A replay through a passage pair matches the session.
Loading
Loading