Skip to content

osrlib.crawl.battle

The range-track battle state machine and the pluggable monster action policy.

start_battle opens battle — from the encounter procedure's own stance handling, a pursuit collapsing to melee range, or the party's own choice through the EngageBattle command — and each round after that runs through ResolveBattleRound, which carries one BattleDeclaration per living, able party member. A round wraps the kernel's combat functions in the OSE SRD's sequence: declaration, initiative, then per side — monster morale, movement, missiles, magic, melee — with slow-weapon actors last. The combat space is the abstract per-group range track (the Bard's Tale convention), as a documented adaptation — see the adaptations register: each monster group sits at a distance from the party, closing at encounter rate and meleeing at 5'; party ranks derive from marching order under the formation_width_limit flag (width 3 inside a keyed area, 2 in a corridor).

The machine detects spell disruption (a declared caster is successfully attacked, or fails a save, after initiative resolves against them but before their action), auto-invokes morale, consumes marker conditions and battle-bound spell effects, and resolves area footprints deterministically: an area's capacity in creatures is ceil(span / 10) × width, filled in stable spawn order, cones reach-limited, with the engaged party front rank appended under aoe_friendly_fire.

Monster and NPC-party sides act through a pluggable ActionPolicy, substitutable per encounter side; the shipped ScriptedPolicy and NpcPartyPolicy draw only from the monster_action stream, so swapping in a custom policy never shifts attack or damage draws. Initiative still resolves in side blocks — the sides order by their best individual total, since the SRD's phase sequence runs per side, not per combatant.

FLEE_EXIT_FEET module-attribute

FLEE_EXIT_FEET = 120

HANDLERS module-attribute

HANDLERS = {ResolveBattleRound: _handle_resolve_battle_round}

MELEE_RANGE_FEET module-attribute

MELEE_RANGE_FEET = 5

NPC_PARTY_MORALE module-attribute

NPC_PARTY_MORALE = 9

NPC adventurer groups check morale at ML 9, the Veteran stat block's printed score. osrlib anchors on the OSE SRD's own low-level-adventurer monster rather than an invented number, as a documented adaptation (see the adaptations register).

ActionPolicy

Bases: Protocol

The pluggable monster brain — substituted per encounter side by games.

Policies draw only from the monster_action stream, so a policy change never shifts attack or damage draws. A policy may return any actions the kernel validates; the default never casts (monster casting tags are manual data).

choose

choose(session, group, stream) -> list[MonsterAction]

Choose this round's actions for one group's living, able monsters.

BattleState

Bases: BaseModel

The serialized battle overlay; the groups live on the encounter state.

round class-attribute instance-attribute

round: int = 0

started_round instance-attribute

started_round: int

monsters_hold_rounds class-attribute instance-attribute

monsters_hold_rounds: int = 0

morale class-attribute instance-attribute

morale_acted class-attribute instance-attribute

morale_acted: dict[str, list[str]] = {}

fired_last_round class-attribute instance-attribute

fired_last_round: list[str] = []

melee_engagements class-attribute instance-attribute

melee_engagements: dict[str, list[str]] = {}

concentration class-attribute instance-attribute

concentration: dict[str, list[str]] = {}

MonsterAction

Bases: BaseModel

One combatant's chosen action for the round (monster or NPC adventurer).

monster_id instance-attribute

monster_id: str

kind instance-attribute

kind: str

target_id class-attribute instance-attribute

target_id: str | None = None

spell_id class-attribute instance-attribute

spell_id: str | None = None

spell_mode class-attribute instance-attribute

spell_mode: str | None = None

item_id class-attribute instance-attribute

item_id: str | None = None

NpcPartyPolicy

The default ActionPolicy for NPC adventurer sides.

The OSE SRD gives no tactics of its own for an opposing party of adventurers, so osrlib supplies one, as a documented adaptation (see the adaptations register).

Per living member each round, in priority order: (1) a caster holding a memorized cure spell heals the group's most-wounded member below half hp (lowest hp ratio, ties by id); (1½) a member below half hp with no cure left in the group drinks a carried healing potion — the one item-use the default policy performs; (2) a caster holding a memorized wired offensive spell casts it — highest spell level first, ties by spell id — at the party (areas through the footprint rule, single-target picks uniform from the reachable rank); (3) a member with a missile weapon and a gap beyond 5' shoots; (4) otherwise close and melee, the monster default. Policy draws come only from the monster_action stream; NPC casts post declarations and are disruptable exactly like party casts (RAW's trigger doesn't care which side declares).

choose

choose(session, group, stream) -> list[MonsterAction]

Choose this round's actions (see ActionPolicy).

ScriptedPolicy

The default ActionPolicy: scripted breath, then close-and-melee.

Monsters with a scripted pattern in their data follow it: an uses_per_day breath weapon opens with breath, then breath or melee with equal chance while daily uses remain (the dragons, per the OSE SRD); a per_round_chance_in_six gate rolls each round (the hellhound). Otherwise a group beyond 5' closes; at 5' it melees, each monster picking its target uniformly from the reachable party rank. Monster missile routines carry no structured range data, so osrlib treats them the same as melee — close, then attack at 5' — as a documented adaptation (see the adaptations register). Groups never cast.

choose

choose(session, group, stream) -> list[MonsterAction]

Choose actions for the group (see ActionPolicy).

start_battle

start_battle(session, *, party_free_round: bool = False, monsters_free_round: bool = False) -> list[Event]

Open battle from the current encounter: the range track takes over.

ML 2 groups rout when battle starts (RAW). A surprise advantage becomes a free round: the monsters' free round resolves immediately (machine-run, the party cannot act); the party's free round holds the monsters through the first ResolveBattleRound.

Parameters:

Name Type Description Default
session GameSession

The running session.

required
party_free_round bool

The monsters were surprised.

False
monsters_free_round bool

The party was surprised with a hostile/attacking stance.

False

Returns:

Type Description
list[Event]

The battle-opening events.