Skip to content
Merged
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
34 changes: 17 additions & 17 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Quickstart

Roll characters, form a party, build the smallest possible adventure, start a session, execute commands, read the events, and round-trip the game through a save. That's the whole loop, and you run it once here. [The complete program](#the-complete-program) is at the end of the page, and every snippet along the way comes from it.

Install [osrlib from PyPI](https://pypi.org/project/osrlib/) with [uv](https://docs.astral.sh/uv/) or pip. You'll need Python ≥ 3.14.
Install [osrlib from PyPI](https://pypi.org/project/osrlib/). Python ≥ 3.14 required.

```sh
uv add osrlib
Expand All @@ -16,7 +14,7 @@ pip install osrlib

## Roll the party

Character creation follows the SRD's procedure: roll ability scores, choose a class, roll hit points and starting gold. [`create_character`][osrlib.core.character.create_character] runs the whole procedure in one call. Every random draw in osrlib comes from a named stream forked from a master seed, so the same seed always produces the same characters:
[`create_character`][osrlib.core.character.create_character] runs the B/X procedure: ability scores, class, hit points, starting gold. Randomness flows through named streams forked from a master seed — same seed, same rolls.

```{.python .no-run}
# Roll two 1st-level characters; every random draw comes from a named, seeded stream.
Expand All @@ -27,11 +25,11 @@ cleric = create_character(name="Osric", class_id="cleric", alignment=Alignment.L
party = Party(members=[fighter.character, cleric.character])
```

`class_id` takes any id from [`load_classes`][osrlib.data.load_classes], listed in [the class id index][classes-index]. The result bundles the created [`Character`][osrlib.core.character.Character] with the raw rolls, which is why you build the party from `fighter.character`.
`class_id` takes any id from [`load_classes`][osrlib.data.load_classes]. The result wraps the character with its rolls; the party takes `fighter.character`.

## Build the smallest adventure
## Build the adventure

An [`Adventure`][osrlib.crawl.adventure.Adventure] is a town plus one or more dungeons. A dungeon level is a grid of 10-foot cells, and every edge between two cells is a wall unless you declare it open or a door. The dungeon you build here is a single corridor, two cells joined west-east:
An [`Adventure`][osrlib.crawl.adventure.Adventure] is a town plus dungeons. This one is a two-cell corridor.

```{.python .no-run}
# The smallest adventure: a town and a one-corridor dungeon, two cells joined west-east.
Expand All @@ -44,11 +42,11 @@ town = TownSpec(name="Threshold", travel_turns={"crypt": 1})
adventure = Adventure(name="A First Delve", town=town, dungeons=(crypt,))
```

The edge key `"1,0:west"` names the west side of cell `(1, 0)`, the boundary between the two cells. For the geometry and the content models in full, see [Building an adventure](building-an-adventure.md).
`"1,0:west"` keys the west side of cell `(1, 0)`. See [Building an adventure](building-an-adventure.md) for the full geometry.

## Start the session and move

A [`GameSession`][osrlib.crawl.session.GameSession] starts in town. [`EnterDungeon`][osrlib.crawl.commands.EnterDungeon] places the party at the entrance and switches the session to `exploring`, the only mode where movement and door commands are legal:
A [`GameSession`][osrlib.crawl.session.GameSession] starts in town. [`EnterDungeon`][osrlib.crawl.commands.EnterDungeon] places the party at the entrance and switches the session to `exploring`.

```{.python .no-run}
# A session starts in town; entering the dungeon switches it to exploring.
Expand All @@ -57,7 +55,7 @@ session.execute(EnterDungeon(dungeon_id="crypt"))
assert session.mode is SessionMode.EXPLORING
```

Game state changes only through commands, and every rules resolution comes back as typed events. A rejected command changes nothing. Rejection is a normal in-fiction outcome, not an exception (see [the rejection code reference](../reference/rejection-codes.md)):
Commands change the game state. Each returns typed events:

```{.python .no-run}
# Commands in, events out: every rules resolution is a typed event with a message code.
Expand All @@ -67,11 +65,11 @@ lines = [format_message(event) for event in result.events]
assert lines # every event formats to a default English line
```

Events have structured fields and a message code, never baked prose. [`format_message`][osrlib.messages.format_message] is the default English formatter, and your front end can supply its own (see [the message code reference](../reference/message-codes.md)).
A rejected command changes nothing. Events carry structured fields and a message code, never baked prose. [`format_message`][osrlib.messages.format_message] renders the default English line; your front end can supply its own.

## Save and load

The whole session serializes to a JSON-compatible dict. Loading restores the session from that state alone and re-executes nothing. Replay is the separate [`replay_game`][osrlib.persistence.replay_game] path, which rebuilds the same session by re-executing the command log from the same seed. Load and replay always land in the identical state, and that's the determinism guarantee (see [Determinism, saves, and replay](../guides/determinism-saves-replay.md)):
The session serializes to a JSON-compatible dict. Loading restores it from that state alone:

```{.python .no-run}
# The whole session round-trips through JSON: same seed, same commands, same game.
Expand All @@ -80,6 +78,8 @@ restored = load_game(document)
assert save_game(restored) == document
```

For replay — rebuilding the same session by re-executing the command log — see [Determinism, saves, and replay](../guides/determinism-saves-replay.md).

## The complete program

```python
Expand Down Expand Up @@ -130,8 +130,8 @@ assert save_game(restored) == document

## Where next

- [Building an adventure](building-an-adventure.md) - the dungeon itself: the grid and its edges, keyed areas, and the content that binds to those areas.
- [Gates, triggers, and quests](../guides/gates-triggers-quests.md) - the authored layer: a door that opens only with the right key, a lever that opens a portcullis, an errand that ends the adventure.
- [Sessions, commands, and events](../guides/sessions-commands-events.md) - the command loop in depth: modes, rejections, the event log.
- [Determinism, saves, and replay](../guides/determinism-saves-replay.md) - what the seed guarantees and how saves and replay reach the same state.
- [The TUI crawler](../front-ends/tui-crawler.md) - a complete example game built on everything above.
- [Building an adventure](building-an-adventure.md) - the dungeon grid, edges, keyed areas, and content.
- [Gates, triggers, and quests](../guides/gates-triggers-quests.md) - keyed doors, levers, quests.
- [Sessions, commands, and events](../guides/sessions-commands-events.md) - the command loop: modes, rejections, the event log.
- [Determinism, saves, and replay](../guides/determinism-saves-replay.md) - the seed guarantee and how saves and replay agree.
- [The TUI crawler](../front-ends/tui-crawler.md) - a complete game built on this.
Loading