Skip to content

osrforge.geometry

Stage 5: deterministic geometry synthesis from the extracted room graph, map proposals reconciled.

Deterministic code only; recomputed inside every assembly (no cache), completing the run.json geometry entry. Input: the survey index, the levels' content caches, and (when present) the map-reading cache — merged with the prose connections through osrforge.reconcile, the shared policy the eval scorer consumes too; output: a frozen per-level result (area cell clusters, the edges dict, entrance, transitions, width/height, the dispute channels) that assembly folds into osrlib LevelSpecs and map_disputed flags.

The impassability hazard, defused here: osrlib grids are walls by default — an edge absent from LevelSpec.edges is a wall, and the boundary is an implicit wall — so synthesis emits open edges between every orthogonally adjacent pair of same-area cells, along every corridor path, and at every room-corridor junction. A connection whose stated mechanism is a door kind realizes as a door edge on the stating area's wall; overrides remain the last word.

Every choice below is pinned for determinism: BFS visit order, candidate placement order, component ordering, edge-key ordering, and row-major cell sorting — the byte-stability tests rely on all of them.

DEFAULT_ROOM_CELLS module-attribute

DEFAULT_ROOM_CELLS = (2, 2)

The room size in cells when an area's description states no dimensions.

LevelGeometry dataclass

LevelGeometry(
    dungeon_id: str,
    level_number: int,
    width: int,
    height: int,
    areas: dict[str, tuple[Position, ...]],
    corridors: tuple[Position, ...],
    edges: dict[str, Edge],
    entrance: Position | None,
    transitions: tuple[TransitionSpec, ...],
    unresolved_connections: tuple[tuple[str, str], ...],
    unknown_direction_connections: tuple[tuple[str, str], ...],
    disconnected_areas: tuple[str, ...],
    guessed_transitions: tuple[tuple[str, str], ...],
    map_disputes: tuple[tuple[str, str], ...],
    map_dropped: tuple[str, ...],
)

One level's synthesized geometry, ready for LevelSpec assembly.

Attributes:

Name Type Description
dungeon_id str

The canonical dungeon id.

level_number int

The 1-based level number.

width int

The grid width (bounding box).

height int

The grid height (bounding box).

areas dict[str, tuple[Position, ...]]

Area key → cell cluster, in survey order; cells sorted row-major (y, then x).

corridors tuple[Position, ...]

Corridor cells (cells in no area), sorted row-major.

edges dict[str, Edge]

The open edge map in osrlib's canonical edge_key form, keys sorted by (y, x, side).

entrance Position | None

The entrance area's first cell on the dungeon's entrance level; None elsewhere.

transitions tuple[TransitionSpec, ...]

This level's transition specs, in link derivation order (stairs reciprocal, trapdoors and chutes one-way).

unresolved_connections tuple[tuple[str, str], ...]

(area key, flag detail) pairs geometry dropped — assembly emits each detail verbatim as a connection_ambiguous flag.

unknown_direction_connections tuple[tuple[str, str], ...]

(area key, resolved target key) pairs whose extracted direction was unknown — assembly flags them.

disconnected_areas tuple[str, ...]

Area keys joined by a synthetic component link — assembly flags them not connected to the entrance.

guessed_transitions tuple[tuple[str, str], ...]

(area key, far-end address) pairs for this level's to_level-derived transitions — assembly flags them transition_guessed, the badge that asks a human to confirm or correct the landing.

map_disputes tuple[tuple[str, str], ...]

(area key, detail) pairs for this level's map/prose disagreements and adoptions — edge disputes on the survey-order-first endpoint, the entrance dispute on the selected entrance area — assembly emits each detail verbatim as a map_disputed flag.

map_dropped tuple[str, ...]

Module-scope dropped-proposal details (unresolvable keys, self-pairs, secondary entrance proposals), each carrying its level address — assembly emits them verbatim as module-scope map_disputed flags.

dungeon_id instance-attribute

dungeon_id: str

level_number instance-attribute

level_number: int

width instance-attribute

width: int

height instance-attribute

height: int

areas instance-attribute

areas: dict[str, tuple[Position, ...]]

corridors instance-attribute

corridors: tuple[Position, ...]

edges instance-attribute

edges: dict[str, Edge]

entrance instance-attribute

entrance: Position | None

transitions instance-attribute

transitions: tuple[TransitionSpec, ...]

unresolved_connections instance-attribute

unresolved_connections: tuple[tuple[str, str], ...]

unknown_direction_connections instance-attribute

unknown_direction_connections: tuple[tuple[str, str], ...]

disconnected_areas instance-attribute

disconnected_areas: tuple[str, ...]

guessed_transitions instance-attribute

guessed_transitions: tuple[tuple[str, str], ...]

map_disputes instance-attribute

map_disputes: tuple[tuple[str, str], ...]

map_dropped instance-attribute

map_dropped: tuple[str, ...]

edge_sort_key

edge_sort_key(key: str) -> tuple[int, int, str]

The pinned edge-map key ordering — (y, x, side) over canonical keys.

Synthesis emits its edge map in this order, and override application re-sorts the merged map with it, so the serialized edges dict stays byte-stable regardless of where an edge came from.

Parameters:

Name Type Description Default
key str

A canonical edge key, x,y:side.

required

Returns:

Type Description
tuple[int, int, str]

The sort key.

parse_dimensions

parse_dimensions(description: str) -> tuple[int, int] | None

Parse an area's stated dimensions into a cell-count rectangle.

The first match of a feet-by-feet pattern (30' x 40', a multiplication sign, 30 feet by 40 feet) wins; at least one unit marker is required. Cells are ceil(feet / 10) per axis (the pinned 10' cell), minimum 1; the first number is width (east-west), the second height (north-south).

Parameters:

Name Type Description Default
description str

The area's extracted description.

required

Returns:

Type Description
tuple[int, int] | None

(width, height) in cells, or None when no dimensions are stated.

synthesize_geometry

synthesize_geometry(
    index: SurveyIndex, levels: Sequence[LevelContent], map_reading: MapReading | None = None
) -> tuple[LevelGeometry, ...]

Synthesize every level's geometry, in survey order, map proposals reconciled.

Parameters:

Name Type Description Default
index SurveyIndex

The normalized survey index.

required
levels Sequence[LevelContent]

The available content caches; a level absent here gets default-sized rooms and no connections. Assembly and the preview path both enforce cache completeness upstream — the tolerance serves direct callers (tests, future partial-cache paths).

required
map_reading MapReading | None

The map-reading cache, or None — a pre-phase-11 workdir (or a pending mapread stage) reconciles as no proposals, the honest prose-only path.

None

Returns:

Type Description
tuple[LevelGeometry, ...]

One result per survey level, in survey order, postconditions asserted.

transition_via

transition_via(via: str) -> str

Narrow a mention's via to a transition family: trapdoors and chutes as themselves, else stairs.

Public because the eval scorer's transition family narrows each mention through this exact function (the usable_stat_block precedent: the metric shares geometry's own predicate so the two can never disagree).

Parameters:

Name Type Description Default
via str

A connection mention's stated mechanism.

required

Returns:

Type Description
str

trapdoor, chute, or stairs.