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.
- osrlib-1.0.0/LICENSE +21 -0
- osrlib-1.0.0/LICENSE-OGL.md +97 -0
- osrlib-1.0.0/PKG-INFO +150 -0
- osrlib-1.0.0/README.md +125 -0
- osrlib-1.0.0/pyproject.toml +109 -0
- osrlib-1.0.0/src/osrlib/__init__.py +65 -0
- osrlib-1.0.0/src/osrlib/core/__init__.py +5 -0
- osrlib-1.0.0/src/osrlib/core/abilities.py +447 -0
- osrlib-1.0.0/src/osrlib/core/alignment.py +25 -0
- osrlib-1.0.0/src/osrlib/core/character.py +768 -0
- osrlib-1.0.0/src/osrlib/core/classes.py +827 -0
- osrlib-1.0.0/src/osrlib/core/clock.py +129 -0
- osrlib-1.0.0/src/osrlib/core/combat.py +2166 -0
- osrlib-1.0.0/src/osrlib/core/dice.py +167 -0
- osrlib-1.0.0/src/osrlib/core/effects.py +918 -0
- osrlib-1.0.0/src/osrlib/core/events.py +781 -0
- osrlib-1.0.0/src/osrlib/core/items.py +1724 -0
- osrlib-1.0.0/src/osrlib/core/monsters.py +620 -0
- osrlib-1.0.0/src/osrlib/core/npc.py +326 -0
- osrlib-1.0.0/src/osrlib/core/rng.py +287 -0
- osrlib-1.0.0/src/osrlib/core/ruleset.py +135 -0
- osrlib-1.0.0/src/osrlib/core/spells.py +2292 -0
- osrlib-1.0.0/src/osrlib/core/tables.py +733 -0
- osrlib-1.0.0/src/osrlib/core/treasure.py +969 -0
- osrlib-1.0.0/src/osrlib/core/validation.py +46 -0
- osrlib-1.0.0/src/osrlib/crawl/__init__.py +9 -0
- osrlib-1.0.0/src/osrlib/crawl/adventure.py +172 -0
- osrlib-1.0.0/src/osrlib/crawl/battle.py +2053 -0
- osrlib-1.0.0/src/osrlib/crawl/commands.py +1699 -0
- osrlib-1.0.0/src/osrlib/crawl/dungeon.py +725 -0
- osrlib-1.0.0/src/osrlib/crawl/encounter.py +774 -0
- osrlib-1.0.0/src/osrlib/crawl/events.py +752 -0
- osrlib-1.0.0/src/osrlib/crawl/exploration.py +2903 -0
- osrlib-1.0.0/src/osrlib/crawl/party.py +96 -0
- osrlib-1.0.0/src/osrlib/crawl/session.py +982 -0
- osrlib-1.0.0/src/osrlib/crawl/views.py +418 -0
- osrlib-1.0.0/src/osrlib/data/LICENSE-OGL.md +99 -0
- osrlib-1.0.0/src/osrlib/data/__init__.py +283 -0
- osrlib-1.0.0/src/osrlib/data/abilities.json +307 -0
- osrlib-1.0.0/src/osrlib/data/classes.json +2827 -0
- osrlib-1.0.0/src/osrlib/data/combat_tables.json +734 -0
- osrlib-1.0.0/src/osrlib/data/encounter_tables.json +1717 -0
- osrlib-1.0.0/src/osrlib/data/equipment.json +881 -0
- osrlib-1.0.0/src/osrlib/data/languages.json +116 -0
- osrlib-1.0.0/src/osrlib/data/magic_items.json +9149 -0
- osrlib-1.0.0/src/osrlib/data/monsters.json +30752 -0
- osrlib-1.0.0/src/osrlib/data/spells.json +6534 -0
- osrlib-1.0.0/src/osrlib/data/treasure.json +1507 -0
- osrlib-1.0.0/src/osrlib/errors.py +57 -0
- osrlib-1.0.0/src/osrlib/messages.py +350 -0
- osrlib-1.0.0/src/osrlib/persistence.py +331 -0
- osrlib-1.0.0/src/osrlib/py.typed +0 -0
- 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
|
+
"""
|