Skip to content

osrlib.core

The rules kernel: the B/X mechanics as pure functions over frozen models.

The kernel is the half of osrlib that has no game loop. You hand a kernel function the inputs a rule needs, plus a seeded random-number stream and a Ruleset (the frozen set of optional-rule flags a game plays under), and it returns the outcome together with the typed events that describe it. Nothing here starts a session, keeps a turn order, or remembers where the party is standing. You call the kernel directly when you want the rules without a game: a combat simulator, a balance harness, a script that checks authored content. The dungeon-crawl framework in osrlib.crawl is one consumer of the kernel, and yours is another. Kernel modules never import from osrlib.crawl, so what you build on the kernel keeps working whatever the crawl layer does above it.

The modules, in the order you meet them:

The compiled SRD catalogs the kernel reads come from osrlib.data, and the exceptions it raises are in osrlib.errors.

Typical usage:

from osrlib.core.abilities import ability_check
from osrlib.core.clock import GameClock, TimeUnit
from osrlib.core.dice import roll
from osrlib.core.rng import RngStreams
from osrlib.data import load_ability_tables

# One master seed forks every stream the kernel draws from.
stream = RngStreams(master_seed=7).get("character_creation")

# Roll a strength score, then read what the SRD's table grants at that score.
strength = roll("3d6", stream)
assert strength.rolls == (2, 6, 6)
assert strength.total == 14

tables = load_ability_tables()
assert tables.melee_modifier(strength.total) == 1
assert tables.open_doors_chance(strength.total) == 3

# An ability check rolls 1d20 and succeeds on equal-or-under the score.
check = ability_check(strength.total, stream)
assert (check.roll, check.success) == (13, True)

# The clock counts rounds and reports the turn and day boundaries an advance crosses.
clock = GameClock()
crossings = clock.advance(1, TimeUnit.TURN)
assert clock.rounds == 60
assert [(crossing.unit, crossing.index) for crossing in crossings] == [(TimeUnit.TURN, 1)]