osrlib 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. osrlib-1.0.0/LICENSE +21 -0
  2. osrlib-1.0.0/LICENSE-OGL.md +97 -0
  3. osrlib-1.0.0/PKG-INFO +150 -0
  4. osrlib-1.0.0/README.md +125 -0
  5. osrlib-1.0.0/pyproject.toml +109 -0
  6. osrlib-1.0.0/src/osrlib/__init__.py +65 -0
  7. osrlib-1.0.0/src/osrlib/core/__init__.py +5 -0
  8. osrlib-1.0.0/src/osrlib/core/abilities.py +447 -0
  9. osrlib-1.0.0/src/osrlib/core/alignment.py +25 -0
  10. osrlib-1.0.0/src/osrlib/core/character.py +768 -0
  11. osrlib-1.0.0/src/osrlib/core/classes.py +827 -0
  12. osrlib-1.0.0/src/osrlib/core/clock.py +129 -0
  13. osrlib-1.0.0/src/osrlib/core/combat.py +2166 -0
  14. osrlib-1.0.0/src/osrlib/core/dice.py +167 -0
  15. osrlib-1.0.0/src/osrlib/core/effects.py +918 -0
  16. osrlib-1.0.0/src/osrlib/core/events.py +781 -0
  17. osrlib-1.0.0/src/osrlib/core/items.py +1724 -0
  18. osrlib-1.0.0/src/osrlib/core/monsters.py +620 -0
  19. osrlib-1.0.0/src/osrlib/core/npc.py +326 -0
  20. osrlib-1.0.0/src/osrlib/core/rng.py +287 -0
  21. osrlib-1.0.0/src/osrlib/core/ruleset.py +135 -0
  22. osrlib-1.0.0/src/osrlib/core/spells.py +2292 -0
  23. osrlib-1.0.0/src/osrlib/core/tables.py +733 -0
  24. osrlib-1.0.0/src/osrlib/core/treasure.py +969 -0
  25. osrlib-1.0.0/src/osrlib/core/validation.py +46 -0
  26. osrlib-1.0.0/src/osrlib/crawl/__init__.py +9 -0
  27. osrlib-1.0.0/src/osrlib/crawl/adventure.py +172 -0
  28. osrlib-1.0.0/src/osrlib/crawl/battle.py +2053 -0
  29. osrlib-1.0.0/src/osrlib/crawl/commands.py +1699 -0
  30. osrlib-1.0.0/src/osrlib/crawl/dungeon.py +725 -0
  31. osrlib-1.0.0/src/osrlib/crawl/encounter.py +774 -0
  32. osrlib-1.0.0/src/osrlib/crawl/events.py +752 -0
  33. osrlib-1.0.0/src/osrlib/crawl/exploration.py +2903 -0
  34. osrlib-1.0.0/src/osrlib/crawl/party.py +96 -0
  35. osrlib-1.0.0/src/osrlib/crawl/session.py +982 -0
  36. osrlib-1.0.0/src/osrlib/crawl/views.py +418 -0
  37. osrlib-1.0.0/src/osrlib/data/LICENSE-OGL.md +99 -0
  38. osrlib-1.0.0/src/osrlib/data/__init__.py +283 -0
  39. osrlib-1.0.0/src/osrlib/data/abilities.json +307 -0
  40. osrlib-1.0.0/src/osrlib/data/classes.json +2827 -0
  41. osrlib-1.0.0/src/osrlib/data/combat_tables.json +734 -0
  42. osrlib-1.0.0/src/osrlib/data/encounter_tables.json +1717 -0
  43. osrlib-1.0.0/src/osrlib/data/equipment.json +881 -0
  44. osrlib-1.0.0/src/osrlib/data/languages.json +116 -0
  45. osrlib-1.0.0/src/osrlib/data/magic_items.json +9149 -0
  46. osrlib-1.0.0/src/osrlib/data/monsters.json +30752 -0
  47. osrlib-1.0.0/src/osrlib/data/spells.json +6534 -0
  48. osrlib-1.0.0/src/osrlib/data/treasure.json +1507 -0
  49. osrlib-1.0.0/src/osrlib/errors.py +57 -0
  50. osrlib-1.0.0/src/osrlib/messages.py +350 -0
  51. osrlib-1.0.0/src/osrlib/persistence.py +331 -0
  52. osrlib-1.0.0/src/osrlib/py.typed +0 -0
  53. osrlib-1.0.0/src/osrlib/versioning.py +110 -0
osrlib-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marsh Macy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,97 @@
1
+ # Open Game License
2
+
3
+ The files under `srd/` are the scraped [Old-School Essentials System Reference Document](https://oldschoolessentials.necroticgnome.com/srd/) and are Open Game Content under the Open Game License 1.0a, reproduced below with its complete Section 15 copyright notice as published by Necrotic Gnome. The same license applies to `src/osrlib/data/`, the machine-readable content compiled from `srd/`, which ships its own copy of this license (with the osrlib Section 15 entry) inside the package.
4
+
5
+ The osrlib library code is separately licensed under the MIT license (see `LICENSE`) and is not Open Game Content.
6
+
7
+ DESIGNATION OF PRODUCT IDENTITY
8
+
9
+ All artwork, logos, and presentation are product identity. The names “Necrotic Gnome” and “Old-School Essentials” are product identity. All text on the main page (<https://oldschoolessentials.necroticgnome.com/srd>) is product identity.
10
+
11
+ DESIGNATION OF OPEN GAME CONTENT
12
+
13
+ All text and tables not declared as product identity are Open Game Content.
14
+
15
+ OPEN GAME LICENSE Version 1.0a
16
+
17
+ The following text is the property of Wizards of the Coast, Inc. and is Copyright 2000 Wizards of the Coast, Inc (“Wizards”). All Rights Reserved.
18
+
19
+ 1. Definitions: (a)”Contributors” means the copyright and/or trademark owners who have contributed Open Game Content; (b)”Derivative Material” means copyrighted material including derivative works and translations (including into other computer languages), potation, modification, correction, addition, extension, upgrade, improvement, compilation, abridgment or other form in which an existing work may be recast, transformed or adapted; (c) “Distribute” means to reproduce, license, rent, lease, sell, broadcast, publicly display, transmit or otherwise distribute; (d)”Open Game Content” means the game mechanic and includes the methods, procedures, processes and routines to the extent such content does not embody the Product Identity and is an enhancement over the prior art and any additional content clearly identified as Open Game Content by the Contributor, and means any work covered by this License, including translations and derivative works under copyright law, but specifically excludes Product Identity. (e) “Product Identity” means product and product line names, logos and identifying marks including trade dress; artifacts; creatures characters; stories, storylines, plots, thematic elements, dialogue, incidents, language, artwork, symbols, designs, depictions, likenesses, formats, poses, concepts, themes and graphic, photographic and other visual or audio representations; names and descriptions of characters, spells, enchantments, personalities, teams, personas, likenesses and special abilities; places, locations, environments, creatures, equipment, magical or supernatural abilities or effects, logos, symbols, or graphic designs; and any other trademark or registered trademark clearly identified as Product identity by the owner of the Product Identity, and which specifically excludes the Open Game Content; (f) “Trademark” means the logos, names, mark, sign, motto, designs that are used by a Contributor to identify itself or its products or the associated products contributed to the Open Game License by the Contributor (g) “Use”, “Used” or “Using” means to use, Distribute, copy, edit, format, modify, translate and otherwise create Derivative Material of Open Game Content. (h) “You” or “Your” means the licensee in terms of this agreement.
20
+
21
+ 2. The License: This License applies to any Open Game Content that contains a notice indicating that the Open Game Content may only be Used under and in terms of this License. You must affix such a notice to any Open Game Content that you Use. No terms may be added to or subtracted from this License except as described by the License itself. No other terms or conditions may be applied to any Open Game Content distributed using this License.
22
+
23
+ 3.Offer and Acceptance: By Using the Open Game Content You indicate Your acceptance of the terms of this License.
24
+
25
+ 4. Grant and Consideration: In consideration for agreeing to use this License, the Contributors grant You a perpetual, worldwide, royalty-free, non-exclusive license with the exact terms of this License to Use, the Open Game Content.
26
+
27
+ 5.Representation of Authority to Contribute: If You are contributing original material as Open Game Content, You represent that Your Contributions are Your original creation and/or You have sufficient rights to grant the rights conveyed by this License.
28
+
29
+ 6.Notice of License Copyright: You must update the COPYRIGHT NOTICE portion of this License to include the exact text of the COPYRIGHT NOTICE of any Open Game Content You are copying, modifying or distributing, and You must add the title, the copyright date, and the copyright holder’s name to the COPYRIGHT NOTICE of any original Open Game Content you Distribute.
30
+
31
+ 7. Use of Product Identity: You agree not to Use any Product Identity, including as an indication as to compatibility, except as expressly licensed in another, independent Agreement with the owner of each element of that Product Identity. You agree not to indicate compatibility or co-adaptability with any Trademark or Registered Trademark in conjunction with a work containing Open Game Content except as expressly licensed in another, independent Agreement with the owner of such Trademark or Registered Trademark. The use of any Product Identity in Open Game Content does not constitute a challenge to the ownership of that Product Identity. The owner of any Product Identity used in Open Game Content shall retain all rights, title and interest in and to that Product Identity.
32
+
33
+ 8. Identification: If you distribute Open Game Content You must clearly indicate which portions of the work that you are distributing are Open Game Content.
34
+
35
+ 9. Updating the License: Wizards or its designated Agents may publish updated versions of this License. You may use any authorized version of this License to copy, modify and distribute any Open Game Content originally distributed under any version of this License.
36
+
37
+ 10 Copy of this License: You MUST include a copy of this License with every copy of the Open Game Content You Distribute.
38
+
39
+ 11. Use of Contributor Credits: You may not market or advertise the Open Game Content using the name of any Contributor unless You have written permission from the Contributor to do so.
40
+
41
+ 12 Inability to Comply: If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Open Game Content due to statute, judicial order, or governmental regulation then You may not Use any Open Game Material so affected.
42
+
43
+ 13 Termination: This License will terminate automatically if You fail to comply with all terms herein and fail to cure such breach within 30 days of becoming aware of the breach. All sublicenses shall survive the termination of this License.
44
+
45
+ 14 Reformation: If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
46
+
47
+ 15 COPYRIGHT NOTICE
48
+
49
+ Open Game License v 1.0 © 2000, Wizards of the Coast, Inc.
50
+
51
+ System Reference Document © 2000, Wizards of the Coast, Inc.; Authors Jonathan Tweet, Monte Cook, Skip Williams, based on original material by E. Gary Gygax and Dave Arneson.
52
+
53
+ System Reference Document © 2000-2003, Wizards of the Coast, Inc.; Authors Jonathan Tweet, Monte Cook, Skip Williams, Rich Baker, Andy Collins, David Noonan, Rich Redman, Bruce R. Cordell, John D. Rateliff, Thomas Reid, James Wyatt, based on original material by E. Gary Gygax and Dave Arneson.
54
+
55
+ Modern System Reference Document © 2002-2004, Wizards of the Coast, Inc.; Authors Bill Slavicsek, Jeff Grubb, Rich Redman, Charles Ryan, Eric Cagle, David Noonan, Stan!, Christopher Perkins, Rodney Thompson, and JD Wiker, based on material by Jonathan Tweet, Monte Cook, Skip Williams, Richard Baker, Peter Adkison, Bruce R. Cordell, John Tynes, Andy Collins, and JD Wiker.
56
+
57
+ Castles & Crusades: Players Handbook, © 2004, Troll Lord Games; Authors Davis Chenault and Mac Golden.
58
+
59
+ Cave Cricket from the Tome of Horrors, © 2002, Necromancer Games, Inc.; Authors Scott Greene and Clark Peterson, based on original material by Gary Gygax.
60
+
61
+ Crab, Monstrous from the Tome of Horrors, © 2002, Necromancer Games, Inc.; Author Scott Greene, based on original material by Gary Gygax.
62
+
63
+ Fly, Giant from the Tome of Horrors, © 2002, Necromancer Games, Inc.; Author Scott Greene, based on original material by Gary Gygax.
64
+
65
+ Golem, Wood from the Tome of Horrors, © 2002, Necromancer Games, Inc.; Authors Scott Greene and Patrick Lawinger.
66
+
67
+ Kamadan from the Tome of Horrors, © 2002, Necromancer Games, Inc.; Author Scott Greene, based on original material by Nick Louth.
68
+
69
+ Rot Grub from the Tome of Horrors, © 2002, Necromancer Games, Inc.; Authors Scott Greene and Clark Peterson, based on original material by Gary Gygax.
70
+
71
+ Labyrinth Lord™ © 2007-2009, Daniel Proctor. Author Daniel Proctor.
72
+
73
+ B/X Essentials: Core Rules © 2017 Gavin Norman. Author Gavin Norman.
74
+
75
+ B/X Essentials: Classes and Equipment © 2017 Gavin Norman. Author Gavin Norman.
76
+
77
+ B/X Essentials: Cleric and Magic-User Spells © 2017 Gavin Norman. Author Gavin Norman.
78
+
79
+ B/X Essentials: Monsters © 2017 Gavin Norman. Author Gavin Norman.
80
+
81
+ B/X Essentials: Adventures and Treasures © 2018 Gavin Norman. Author Gavin Norman.
82
+
83
+ Old-School Essentials Core Rules © 2018 Gavin Norman.
84
+
85
+ Old-School Essentials Classic Fantasy: Genre Rules © 2018 Gavin Norman.
86
+
87
+ Old-School Essentials Classic Fantasy: Cleric and Magic-User Spells © 2018 Gavin Norman.
88
+
89
+ Old-School Essentials Classic Fantasy: Monsters © 2018 Gavin Norman.
90
+
91
+ Old-School Essentials Classic Fantasy: Treasures © 2018 Gavin Norman.
92
+
93
+ Old-School Essentials Classic Fantasy: Rules Tome © 2019 Gavin Norman.
94
+
95
+ Old-School Essentials System Reference Document © 2019 Gavin Norman. Author Gavin Norman.
96
+
97
+ END OF LICENSE
osrlib-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: osrlib
3
+ Version: 1.0.0
4
+ Summary: B/X (1981 Basic/Expert) tabletop RPG rules engine for turn-based dungeon crawlers
5
+ Keywords: bx,dungeon-crawler,game-engine,old-school-essentials,osr,rpg,srd,ttrpg
6
+ Author: Marsh Macy
7
+ License-Expression: MIT AND LicenseRef-OGL-1.0a
8
+ License-File: LICENSE
9
+ License-File: LICENSE-OGL.md
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Games/Entertainment :: Role-Playing
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: pydantic>=2
19
+ Requires-Python: >=3.14
20
+ Project-URL: Changelog, https://github.com/mmacy/osrlib-python/blob/main/CHANGELOG.md
21
+ Project-URL: Documentation, https://mmacy.github.io/osrlib-python/
22
+ Project-URL: Issues, https://github.com/mmacy/osrlib-python/issues
23
+ Project-URL: Repository, https://github.com/mmacy/osrlib-python
24
+ Description-Content-Type: text/markdown
25
+
26
+ # osrlib
27
+
28
+ A Python library implementing the classic 1981 B/X (Basic/Expert) fantasy adventure game rules for turn-based, grid-based dungeon crawlers in the style of the original Bard's Tale. The rules are sourced from the [Old-School Essentials System Reference Document](https://oldschoolessentials.necroticgnome.com/srd/), an Open Game Content restatement of the B/X rules. osrlib is the rules authority and game-state engine; your game supplies presentation, input, and content.
29
+
30
+ The library is headless and sans-I/O — it never renders, prompts, sleeps, or touches the network — and every game it runs is deterministic: the same seed and the same commands always replay the same game. Four kinds of consumer are first-class: a web or mobile backend (FastAPI over HTTP), a terminal game (a local TUI crawler), an LLM referee or narrator driven by structured events and typed commands, and scripts or simulations using the kernel à la carte.
31
+
32
+ **Status:** released — [osrlib on PyPI](https://pypi.org/project/osrlib/). The public API is frozen, and the [documentation site](https://mmacy.github.io/osrlib-python/) is the place to learn the library — quickstart, guides, front-end walk-throughs, and a full reference for every command, event, rejection code, and content id.
33
+
34
+ ## Installation
35
+
36
+ Requires Python ≥ 3.14. The only runtime dependency is [pydantic](https://docs.pydantic.dev/).
37
+
38
+ ```sh
39
+ uv add osrlib
40
+ ```
41
+
42
+ or, with pip:
43
+
44
+ ```sh
45
+ pip install osrlib
46
+ ```
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ from osrlib.core.alignment import Alignment
52
+ from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
53
+ from osrlib.core.rng import RngStreams
54
+ from osrlib.core.ruleset import Ruleset
55
+ from osrlib.crawl.adventure import Adventure, TownSpec
56
+ from osrlib.crawl.commands import EnterDungeon, MoveParty, SessionMode
57
+ from osrlib.crawl.dungeon import Direction, DungeonSpec, Edge, EdgeKind, LevelSpec
58
+ from osrlib.crawl.party import Party
59
+ from osrlib.crawl.session import GameSession
60
+ from osrlib.messages import format_message
61
+ from osrlib.persistence import load_game, save_game
62
+
63
+ # Roll two 1st-level characters; every random draw comes from a named, seeded stream.
64
+ rules = Ruleset()
65
+ creation = RngStreams(master_seed=7).get(CHARACTER_CREATION_STREAM)
66
+ fighter = create_character(name="Hild", class_id="fighter", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
67
+ cleric = create_character(name="Osric", class_id="cleric", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
68
+ party = Party(members=[fighter.character, cleric.character])
69
+
70
+ # The smallest adventure: a town and a one-corridor dungeon, two cells joined west-east.
71
+ crypt = DungeonSpec(
72
+ id="crypt",
73
+ name="The Old Crypt",
74
+ levels=(LevelSpec(number=1, width=2, height=1, entrance=(0, 0), edges={"1,0:west": Edge(kind=EdgeKind.OPEN)}),),
75
+ )
76
+ town = TownSpec(name="Threshold", travel_turns={"crypt": 1})
77
+ adventure = Adventure(name="A First Delve", town=town, dungeons=(crypt,))
78
+
79
+ # A session starts in town; entering the dungeon switches it to exploring.
80
+ session = GameSession.new(party, adventure, seed=7)
81
+ session.execute(EnterDungeon(dungeon_id="crypt"))
82
+ assert session.mode is SessionMode.EXPLORING
83
+
84
+ # Commands in, events out: every rules resolution is a typed event with a message code.
85
+ result = session.execute(MoveParty(direction=Direction.EAST))
86
+ assert result.accepted
87
+ lines = [format_message(event) for event in result.events]
88
+ assert lines # every event formats to a default English line
89
+
90
+ # The whole session round-trips through JSON: same seed, same commands, same game.
91
+ document = save_game(session)
92
+ restored = load_game(document)
93
+ assert save_game(restored) == document
94
+ ```
95
+
96
+ The [documentation site](https://mmacy.github.io/osrlib-python/) walks this example step by step, then builds out from it: [building an adventure](https://mmacy.github.io/osrlib-python/getting-started/building-an-adventure/), the [session and event loop](https://mmacy.github.io/osrlib-python/guides/sessions-commands-events/), and complete [front-end walk-throughs](https://mmacy.github.io/osrlib-python/front-ends/tui-crawler/) for the two example games in `examples/`.
97
+
98
+ ## Determinism
99
+
100
+ Determinism is a public API guarantee. All randomness flows through named PCG64 streams forked from a master seed, so the same seed and the same key always produce the same stream — independently of any other stream:
101
+
102
+ ```python
103
+ from osrlib.core.dice import roll
104
+ from osrlib.core.rng import RngStreams
105
+
106
+ streams_a = RngStreams(master_seed=42)
107
+ streams_b = RngStreams(master_seed=42)
108
+
109
+ rolls_a = [roll("2d6×10", streams_a.get("treasure")).total for _ in range(3)]
110
+ rolls_b = [roll("2d6×10", streams_b.get("treasure")).total for _ in range(3)]
111
+ assert rolls_a == rolls_b # same seed + same key → identical sequences
112
+ ```
113
+
114
+ Successive rolls on one stream differ, of course; reproducibility across derivations is the contract. Saved games replay from the seed and the command log, so a loaded game is bit-for-bit the game you saved.
115
+
116
+ ## SRD data pipeline
117
+
118
+ The game data in `src/osrlib/data/` is generated from the scraped SRD markdown in `srd/` and is never hand-edited. Regenerate it with:
119
+
120
+ ```sh
121
+ uv run python -m tools.srd_compile
122
+ ```
123
+
124
+ CI regenerates the data and fails on any diff, so `srd/`, the compiler, and the generated data cannot silently drift. Parser corrections belong in `tools/srd_compile/overrides/`, never in the output; every override carries a reason and is recorded in the output entry's `overrides_applied` provenance list. Rules interpretations and adaptations are documented in the [adaptations register](https://mmacy.github.io/osrlib-python/adaptations/).
125
+
126
+ ## Contributing
127
+
128
+ Requires Python ≥ 3.14 and [uv](https://docs.astral.sh/uv/). Install from source and run the checks the way CI does:
129
+
130
+ ```sh
131
+ git clone https://github.com/mmacy/osrlib-python.git
132
+ cd osrlib-python
133
+ uv sync
134
+ uv run ruff format --check
135
+ uv run ruff check
136
+ uv run pyright
137
+ uv run pytest
138
+ uv run mkdocs build --strict
139
+ ```
140
+
141
+ The design is documented in [the specification](docs/spec.md): architecture, contracts, rules scope, and the phased roadmap.
142
+
143
+ ## Licensing
144
+
145
+ This repository contains two kinds of material under two licenses:
146
+
147
+ - **Library code** is licensed under the [MIT license](LICENSE).
148
+ - **SRD-derived content** — the scraped SRD text in `srd/` and the compiled game data in `src/osrlib/data/` — is Open Game Content used under the [Open Game License 1.0a](LICENSE-OGL.md), which includes the complete Section 15 copyright notice. The data package ships its own copy of the license, with the osrlib Section 15 entry, inside the built wheel.
149
+
150
+ osrlib is an independent project, not affiliated with or endorsed by Necrotic Gnome. "Old-School Essentials" is a trademark of Necrotic Gnome, used here only to identify the source document; no claim of compatibility is made.
osrlib-1.0.0/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # osrlib
2
+
3
+ A Python library implementing the classic 1981 B/X (Basic/Expert) fantasy adventure game rules for turn-based, grid-based dungeon crawlers in the style of the original Bard's Tale. The rules are sourced from the [Old-School Essentials System Reference Document](https://oldschoolessentials.necroticgnome.com/srd/), an Open Game Content restatement of the B/X rules. osrlib is the rules authority and game-state engine; your game supplies presentation, input, and content.
4
+
5
+ The library is headless and sans-I/O — it never renders, prompts, sleeps, or touches the network — and every game it runs is deterministic: the same seed and the same commands always replay the same game. Four kinds of consumer are first-class: a web or mobile backend (FastAPI over HTTP), a terminal game (a local TUI crawler), an LLM referee or narrator driven by structured events and typed commands, and scripts or simulations using the kernel à la carte.
6
+
7
+ **Status:** released — [osrlib on PyPI](https://pypi.org/project/osrlib/). The public API is frozen, and the [documentation site](https://mmacy.github.io/osrlib-python/) is the place to learn the library — quickstart, guides, front-end walk-throughs, and a full reference for every command, event, rejection code, and content id.
8
+
9
+ ## Installation
10
+
11
+ Requires Python ≥ 3.14. The only runtime dependency is [pydantic](https://docs.pydantic.dev/).
12
+
13
+ ```sh
14
+ uv add osrlib
15
+ ```
16
+
17
+ or, with pip:
18
+
19
+ ```sh
20
+ pip install osrlib
21
+ ```
22
+
23
+ ## Quickstart
24
+
25
+ ```python
26
+ from osrlib.core.alignment import Alignment
27
+ from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
28
+ from osrlib.core.rng import RngStreams
29
+ from osrlib.core.ruleset import Ruleset
30
+ from osrlib.crawl.adventure import Adventure, TownSpec
31
+ from osrlib.crawl.commands import EnterDungeon, MoveParty, SessionMode
32
+ from osrlib.crawl.dungeon import Direction, DungeonSpec, Edge, EdgeKind, LevelSpec
33
+ from osrlib.crawl.party import Party
34
+ from osrlib.crawl.session import GameSession
35
+ from osrlib.messages import format_message
36
+ from osrlib.persistence import load_game, save_game
37
+
38
+ # Roll two 1st-level characters; every random draw comes from a named, seeded stream.
39
+ rules = Ruleset()
40
+ creation = RngStreams(master_seed=7).get(CHARACTER_CREATION_STREAM)
41
+ fighter = create_character(name="Hild", class_id="fighter", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
42
+ cleric = create_character(name="Osric", class_id="cleric", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
43
+ party = Party(members=[fighter.character, cleric.character])
44
+
45
+ # The smallest adventure: a town and a one-corridor dungeon, two cells joined west-east.
46
+ crypt = DungeonSpec(
47
+ id="crypt",
48
+ name="The Old Crypt",
49
+ levels=(LevelSpec(number=1, width=2, height=1, entrance=(0, 0), edges={"1,0:west": Edge(kind=EdgeKind.OPEN)}),),
50
+ )
51
+ town = TownSpec(name="Threshold", travel_turns={"crypt": 1})
52
+ adventure = Adventure(name="A First Delve", town=town, dungeons=(crypt,))
53
+
54
+ # A session starts in town; entering the dungeon switches it to exploring.
55
+ session = GameSession.new(party, adventure, seed=7)
56
+ session.execute(EnterDungeon(dungeon_id="crypt"))
57
+ assert session.mode is SessionMode.EXPLORING
58
+
59
+ # Commands in, events out: every rules resolution is a typed event with a message code.
60
+ result = session.execute(MoveParty(direction=Direction.EAST))
61
+ assert result.accepted
62
+ lines = [format_message(event) for event in result.events]
63
+ assert lines # every event formats to a default English line
64
+
65
+ # The whole session round-trips through JSON: same seed, same commands, same game.
66
+ document = save_game(session)
67
+ restored = load_game(document)
68
+ assert save_game(restored) == document
69
+ ```
70
+
71
+ The [documentation site](https://mmacy.github.io/osrlib-python/) walks this example step by step, then builds out from it: [building an adventure](https://mmacy.github.io/osrlib-python/getting-started/building-an-adventure/), the [session and event loop](https://mmacy.github.io/osrlib-python/guides/sessions-commands-events/), and complete [front-end walk-throughs](https://mmacy.github.io/osrlib-python/front-ends/tui-crawler/) for the two example games in `examples/`.
72
+
73
+ ## Determinism
74
+
75
+ Determinism is a public API guarantee. All randomness flows through named PCG64 streams forked from a master seed, so the same seed and the same key always produce the same stream — independently of any other stream:
76
+
77
+ ```python
78
+ from osrlib.core.dice import roll
79
+ from osrlib.core.rng import RngStreams
80
+
81
+ streams_a = RngStreams(master_seed=42)
82
+ streams_b = RngStreams(master_seed=42)
83
+
84
+ rolls_a = [roll("2d6×10", streams_a.get("treasure")).total for _ in range(3)]
85
+ rolls_b = [roll("2d6×10", streams_b.get("treasure")).total for _ in range(3)]
86
+ assert rolls_a == rolls_b # same seed + same key → identical sequences
87
+ ```
88
+
89
+ Successive rolls on one stream differ, of course; reproducibility across derivations is the contract. Saved games replay from the seed and the command log, so a loaded game is bit-for-bit the game you saved.
90
+
91
+ ## SRD data pipeline
92
+
93
+ The game data in `src/osrlib/data/` is generated from the scraped SRD markdown in `srd/` and is never hand-edited. Regenerate it with:
94
+
95
+ ```sh
96
+ uv run python -m tools.srd_compile
97
+ ```
98
+
99
+ CI regenerates the data and fails on any diff, so `srd/`, the compiler, and the generated data cannot silently drift. Parser corrections belong in `tools/srd_compile/overrides/`, never in the output; every override carries a reason and is recorded in the output entry's `overrides_applied` provenance list. Rules interpretations and adaptations are documented in the [adaptations register](https://mmacy.github.io/osrlib-python/adaptations/).
100
+
101
+ ## Contributing
102
+
103
+ Requires Python ≥ 3.14 and [uv](https://docs.astral.sh/uv/). Install from source and run the checks the way CI does:
104
+
105
+ ```sh
106
+ git clone https://github.com/mmacy/osrlib-python.git
107
+ cd osrlib-python
108
+ uv sync
109
+ uv run ruff format --check
110
+ uv run ruff check
111
+ uv run pyright
112
+ uv run pytest
113
+ uv run mkdocs build --strict
114
+ ```
115
+
116
+ The design is documented in [the specification](docs/spec.md): architecture, contracts, rules scope, and the phased roadmap.
117
+
118
+ ## Licensing
119
+
120
+ This repository contains two kinds of material under two licenses:
121
+
122
+ - **Library code** is licensed under the [MIT license](LICENSE).
123
+ - **SRD-derived content** — the scraped SRD text in `srd/` and the compiled game data in `src/osrlib/data/` — is Open Game Content used under the [Open Game License 1.0a](LICENSE-OGL.md), which includes the complete Section 15 copyright notice. The data package ships its own copy of the license, with the osrlib Section 15 entry, inside the built wheel.
124
+
125
+ osrlib is an independent project, not affiliated with or endorsed by Necrotic Gnome. "Old-School Essentials" is a trademark of Necrotic Gnome, used here only to identify the source document; no claim of compatibility is made.
@@ -0,0 +1,109 @@
1
+ [project]
2
+ name = "osrlib"
3
+ version = "1.0.0"
4
+ description = "B/X (1981 Basic/Expert) tabletop RPG rules engine for turn-based dungeon crawlers"
5
+ readme = "README.md"
6
+ # The wheel ships Open Game Content (osrlib/data/*.json and its LICENSE-OGL.md
7
+ # copy) beside MIT-licensed code, so the expression names both; the LicenseRef-
8
+ # form is PEP 639's escape hatch for licenses off the SPDX list.
9
+ license = "MIT AND LicenseRef-OGL-1.0a"
10
+ license-files = ["LICENSE", "LICENSE-OGL.md"]
11
+ authors = [{ name = "Marsh Macy" }]
12
+ requires-python = ">=3.14"
13
+ dependencies = ["pydantic>=2"]
14
+ keywords = [
15
+ "bx",
16
+ "dungeon-crawler",
17
+ "game-engine",
18
+ "old-school-essentials",
19
+ "osr",
20
+ "rpg",
21
+ "srd",
22
+ "ttrpg",
23
+ ]
24
+ # No `License ::` classifier beside an SPDX expression, per PEP 639. Only 3.14
25
+ # carries a version classifier because only 3.14 is tested; future minors earn
26
+ # theirs by entering CI.
27
+ classifiers = [
28
+ "Development Status :: 5 - Production/Stable",
29
+ "Intended Audience :: Developers",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.14",
33
+ "Topic :: Games/Entertainment :: Role-Playing",
34
+ "Topic :: Software Development :: Libraries",
35
+ "Typing :: Typed",
36
+ ]
37
+
38
+ [project.urls]
39
+ Documentation = "https://mmacy.github.io/osrlib-python/"
40
+ Repository = "https://github.com/mmacy/osrlib-python"
41
+ Issues = "https://github.com/mmacy/osrlib-python/issues"
42
+ Changelog = "https://github.com/mmacy/osrlib-python/blob/main/CHANGELOG.md"
43
+
44
+ [dependency-groups]
45
+ dev = [
46
+ "hypothesis>=6",
47
+ "pyright>=1.1.411",
48
+ "pytest>=8",
49
+ "pytest-examples>=0.0.18",
50
+ "ruff>=0.11",
51
+ { include-group = "docs" },
52
+ { include-group = "examples" },
53
+ ]
54
+ # The documentation site's toolchain (mkdocs-material + mkdocstrings and the
55
+ # build-time page generators); included by dev so `uv sync` stays one command.
56
+ # gen-files and literate-nav are pinned exactly: their next releases (0.6.1 and
57
+ # 0.6.3) depend on `properdocs`, an unvetted mkdocs fork that ships an import
58
+ # hook redirecting all mkdocs.* imports to itself. Do not bump either pin
59
+ # without inspecting the release's dependency set first.
60
+ docs = [
61
+ "mkdocs>=1.6.1",
62
+ "mkdocs-gen-files==0.5.0",
63
+ "mkdocs-literate-nav==0.6.1",
64
+ "mkdocs-material>=9.7.6",
65
+ "mkdocstrings[python]>=1.0.4",
66
+ ]
67
+ # The example front ends' dependencies (the FastAPI wrapper and its TestClient);
68
+ # the package's runtime dependency remains pydantic alone.
69
+ examples = [
70
+ "fastapi>=0.115",
71
+ "httpx>=0.28",
72
+ "uvicorn>=0.32",
73
+ ]
74
+
75
+ [build-system]
76
+ requires = ["uv_build>=0.8.22,<0.9.0"]
77
+ build-backend = "uv_build"
78
+
79
+ [tool.ruff]
80
+ line-length = 120
81
+ src = ["src", "tests"]
82
+
83
+ [tool.ruff.lint]
84
+ select = [
85
+ "B", # bugbear
86
+ "D", # pydocstyle
87
+ "E", # pycodestyle errors
88
+ "F", # pyflakes
89
+ "I", # isort
90
+ "UP", # pyupgrade
91
+ "W", # pycodestyle warnings
92
+ ]
93
+
94
+ [tool.ruff.lint.pydocstyle]
95
+ convention = "google"
96
+
97
+ [tool.ruff.lint.per-file-ignores]
98
+ "tests/**" = ["D"]
99
+
100
+ [tool.ruff.lint.isort]
101
+ known-first-party = ["osrlib"]
102
+
103
+ [tool.pytest.ini_options]
104
+ testpaths = ["tests"]
105
+
106
+ [tool.pyright]
107
+ include = ["src", "examples"]
108
+ typeCheckingMode = "basic"
109
+ pythonVersion = "3.14"
@@ -0,0 +1,65 @@
1
+ """Old-School Essentials (B/X) rules engine for turn-based dungeon crawlers.
2
+
3
+ osrlib is the rules authority and game-state engine; the game supplies presentation,
4
+ input, and content. The library is headless and sans-I/O: it never renders, prompts,
5
+ sleeps, or touches the network, and all randomness flows through named deterministic
6
+ streams (see [`osrlib.core.rng`][osrlib.core.rng]).
7
+
8
+ Every symbol has exactly one import home: the kernel under `osrlib.core`, the crawl
9
+ framework under `osrlib.crawl`, and the shared services at the top level —
10
+ [`osrlib.data`][osrlib.data] (compiled SRD catalogs), [`osrlib.errors`][osrlib.errors]
11
+ (the typed exception hierarchy), [`osrlib.messages`][osrlib.messages] (message-code
12
+ formatting), [`osrlib.persistence`][osrlib.persistence] (saves and replay), and
13
+ [`osrlib.versioning`][osrlib.versioning] (schema and engine version stamping). The
14
+ package root re-exports nothing.
15
+
16
+ The quickstart below crosses the whole loop — characters, party, adventure, session,
17
+ commands, events, save, and load. Full documentation, including a stepwise version of
18
+ this example: https://mmacy.github.io/osrlib-python/
19
+
20
+ ```python
21
+ from osrlib.core.alignment import Alignment
22
+ from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
23
+ from osrlib.core.rng import RngStreams
24
+ from osrlib.core.ruleset import Ruleset
25
+ from osrlib.crawl.adventure import Adventure, TownSpec
26
+ from osrlib.crawl.commands import EnterDungeon, MoveParty, SessionMode
27
+ from osrlib.crawl.dungeon import Direction, DungeonSpec, Edge, EdgeKind, LevelSpec
28
+ from osrlib.crawl.party import Party
29
+ from osrlib.crawl.session import GameSession
30
+ from osrlib.messages import format_message
31
+ from osrlib.persistence import load_game, save_game
32
+
33
+ # Roll two 1st-level characters; every random draw comes from a named, seeded stream.
34
+ rules = Ruleset()
35
+ creation = RngStreams(master_seed=7).get(CHARACTER_CREATION_STREAM)
36
+ fighter = create_character(name="Hild", class_id="fighter", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
37
+ cleric = create_character(name="Osric", class_id="cleric", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
38
+ party = Party(members=[fighter.character, cleric.character])
39
+
40
+ # The smallest adventure: a town and a one-corridor dungeon, two cells joined west-east.
41
+ crypt = DungeonSpec(
42
+ id="crypt",
43
+ name="The Old Crypt",
44
+ levels=(LevelSpec(number=1, width=2, height=1, entrance=(0, 0), edges={"1,0:west": Edge(kind=EdgeKind.OPEN)}),),
45
+ )
46
+ town = TownSpec(name="Threshold", travel_turns={"crypt": 1})
47
+ adventure = Adventure(name="A First Delve", town=town, dungeons=(crypt,))
48
+
49
+ # A session starts in town; entering the dungeon switches it to exploring.
50
+ session = GameSession.new(party, adventure, seed=7)
51
+ session.execute(EnterDungeon(dungeon_id="crypt"))
52
+ assert session.mode is SessionMode.EXPLORING
53
+
54
+ # Commands in, events out: every rules resolution is a typed event with a message code.
55
+ result = session.execute(MoveParty(direction=Direction.EAST))
56
+ assert result.accepted
57
+ lines = [format_message(event) for event in result.events]
58
+ assert lines # every event formats to a default English line
59
+
60
+ # The whole session round-trips through JSON: same seed, same commands, same game.
61
+ document = save_game(session)
62
+ restored = load_game(document)
63
+ assert save_game(restored) == document
64
+ ```
65
+ """
@@ -0,0 +1,5 @@
1
+ """The rules kernel: pure mechanics with no game loop.
2
+
3
+ Kernel modules never import from `osrlib.crawl` (the framework layer) and are usable
4
+ à la carte — a game that wants only the math can call them directly.
5
+ """