McRogueFace/docs/PROCEDURAL_GENERATION_SPEC.md

1026 lines
32 KiB
Markdown
Raw Normal View History

# McRogueFace Procedural Generation System Specification
**Version:** 1.0 Draft
**Date:** 2026-01-11
**Status:** Design Complete, Pending Implementation
## Overview
This specification defines the procedural generation system for McRogueFace, exposing libtcod's noise, BSP, and heightmap capabilities through a Pythonic interface optimized for batch operations on Grid and Layer objects.
### Design Philosophy
1. **HeightMap as Universal Canvas**: All procedural data flows through HeightMap objects. Noise and BSP structures generate data *onto* HeightMaps, and Grids/Layers consume data *from* HeightMaps.
2. **Data Stays in C++**: Python defines rules and configuration; C++ executes batch operations. Map data (potentially 1M+ cells) never crosses the Python/C++ boundary during generation.
3. **Composability**: HeightMaps can be combined (add, multiply, lerp), allowing complex terrain from simple building blocks.
4. **Vector-Based Coordinates**: All positions use `(x, y)` tuples. Regions use `((x, y), (w, h))` for bounds or `((x1, y1), (x2, y2))` for world regions.
5. **Mutation with Chaining**: Operations mutate in place and return `self` for method chaining. Threshold operations return new HeightMaps to preserve originals.
---
## Architecture
```
┌─────────────────────────────────────────────────────────────────────┐
│ SOURCES │
│ │
│ NoiseSource BSP │
│ (infinite function, (structural partition, │
│ stateless) tree of nodes) │
│ │ │ │
│ │ .sample() │ .to_heightmap() │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ NoiseSample │ │ BSPMap │ │
│ │ (HeightMap) │ │ (HeightMap) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ ▲ │ ▲ │
│ │ │ .add_noise() │ │ .add_bsp() │
│ │ │ .multiply_noise() │ │ .multiply_bsp() │
│ ▼ │ ▼ │ │
│ └────┴───────────┬───────────────────┴────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ HeightMap │ │
│ │ (2D float[]) │ │
│ │ │ │
│ │ Operations: │ │
│ │ • add/multiply │ │
│ │ • hills/erosion │ │
│ │ • threshold │ │
│ │ • normalize │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Grid │ │ TileLayer │ │ColorLayer │ │
│ │ │ │ │ │ │ │
│ │ walkable │ │ tile_index│ │ color │ │
│ │transparent│ │ │ │ gradient │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ TARGETS │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Class Reference
### mcrfpy.HeightMap
The universal canvas for procedural generation. A 2D grid of float values that can be generated, manipulated, combined, and applied to game objects.
#### Constructor
```python
HeightMap(size: tuple[int, int], fill: float = 0.0)
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `size` | `(int, int)` | Width and height in cells. Immutable after creation. |
| `fill` | `float` | Initial value for all cells. Default 0.0. |
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| `size` | `(int, int)` | Read-only. Width and height of the heightmap. |
#### Scalar Operations
All scalar operations mutate in place and return `self` for chaining.
```python
def fill(self, value: float) -> HeightMap
```
Set all cells to `value`.
```python
def clear(self) -> HeightMap
```
Set all cells to 0.0. Equivalent to `fill(0.0)`.
```python
def add_constant(self, value: float) -> HeightMap
```
Add `value` to every cell.
```python
def scale(self, factor: float) -> HeightMap
```
Multiply every cell by `factor`.
```python
def clamp(self, min: float = 0.0, max: float = 1.0) -> HeightMap
```
Clamp all values to the range [min, max].
```python
def normalize(self, min: float = 0.0, max: float = 1.0) -> HeightMap
```
Linearly rescale values so the current minimum becomes `min` and current maximum becomes `max`.
#### HeightMap Combination
All combination operations mutate in place and return `self`. The `other` HeightMap must have the same size.
```python
def add(self, other: HeightMap) -> HeightMap
```
Add `other` to this heightmap cell-by-cell.
```python
def subtract(self, other: HeightMap) -> HeightMap
```
Subtract `other` from this heightmap cell-by-cell.
```python
def multiply(self, other: HeightMap) -> HeightMap
```
Multiply this heightmap by `other` cell-by-cell. Useful for masking.
```python
def lerp(self, other: HeightMap, t: float) -> HeightMap
```
Linear interpolation: `self = self + (other - self) * t`.
```python
def copy_from(self, other: HeightMap) -> HeightMap
```
Copy all values from `other` into this heightmap.
```python
def max(self, other: HeightMap) -> HeightMap
```
Per-cell maximum of this and `other`.
```python
def min(self, other: HeightMap) -> HeightMap
```
Per-cell minimum of this and `other`.
#### Direct Source Sampling
These methods sample from sources directly onto the heightmap without intermediate allocation.
```python
def add_noise(self,
source: NoiseSource,
world_region: tuple = None,
mode: str = "fbm",
octaves: int = 4,
scale: float = 1.0) -> HeightMap
```
Sample noise and add to current values.
| Parameter | Type | Description |
|-----------|------|-------------|
| `source` | `NoiseSource` | The noise generator to sample from. |
| `world_region` | `((x1,y1), (x2,y2))` | World coordinates to sample. Default: `((0,0), size)`. |
| `mode` | `str` | `"flat"`, `"fbm"`, or `"turbulence"`. Default: `"fbm"`. |
| `octaves` | `int` | Octaves for fbm/turbulence. Default: 4. |
| `scale` | `float` | Multiplier for sampled values. Default: 1.0. |
```python
def multiply_noise(self, source: NoiseSource, **kwargs) -> HeightMap
```
Sample noise and multiply with current values. Same parameters as `add_noise`.
```python
def add_bsp(self,
bsp: BSP,
select: str = "leaves",
nodes: list[BSPNode] = None,
shrink: int = 0,
value: float = 1.0) -> HeightMap
```
Add BSP node regions to heightmap.
| Parameter | Type | Description |
|-----------|------|-------------|
| `bsp` | `BSP` | The BSP tree to sample from. |
| `select` | `str` | `"leaves"`, `"all"`, or `"internal"`. Default: `"leaves"`. |
| `nodes` | `list[BSPNode]` | Override: specific nodes only. Default: None (use select). |
| `shrink` | `int` | Pixels to shrink from node bounds. Default: 0. |
| `value` | `float` | Value to add inside selected regions. Default: 1.0. |
```python
def multiply_bsp(self, bsp: BSP, **kwargs) -> HeightMap
```
Multiply by BSP regions. Same parameters as `add_bsp`.
#### Terrain Generation
These methods implement libtcod heightmap generation algorithms.
```python
def add_hill(self, center: tuple[int, int], radius: float, height: float) -> HeightMap
```
Add a hill (half-spheroid) at the specified position.
```python
def dig_hill(self, center: tuple[int, int], radius: float, depth: float) -> HeightMap
```
Dig a depression. Takes minimum of current value and hill shape (for carving rivers, pits).
```python
def add_voronoi(self,
num_points: int,
coefficients: tuple[float, ...] = (1.0, -0.5),
seed: int = None) -> HeightMap
```
Add Voronoi diagram values. Coefficients weight distance to Nth-closest point.
```python
def mid_point_displacement(self,
roughness: float = 0.5,
seed: int = None) -> HeightMap
```
Generate fractal terrain using diamond-square algorithm. Roughness should be 0.4-0.6.
```python
def rain_erosion(self,
drops: int,
erosion: float = 0.1,
sedimentation: float = 0.05,
seed: int = None) -> HeightMap
```
Simulate rain erosion. `drops` should be at least `width * height`.
```python
def dig_bezier(self,
points: tuple[tuple[int, int], ...],
start_radius: float,
end_radius: float,
start_depth: float,
end_depth: float) -> HeightMap
```
Carve a path along a cubic Bezier curve. Requires exactly 4 control points.
```python
def smooth(self, iterations: int = 1) -> HeightMap
```
Apply smoothing kernel. Each iteration averages cells with neighbors.
#### Threshold Operations
These methods return **new** HeightMap objects, preserving the original.
```python
def threshold(self, range: tuple[float, float]) -> HeightMap
```
Return new HeightMap with original values where in range, 0.0 elsewhere.
```python
def threshold_binary(self,
range: tuple[float, float],
value: float = 1.0) -> HeightMap
```
Return new HeightMap with `value` where in range, 0.0 elsewhere.
```python
def inverse(self) -> HeightMap
```
Return new HeightMap with `(1.0 - value)` for each cell.
#### Queries
These methods return values to Python for inspection.
```python
def get(self, pos: tuple[int, int]) -> float
```
Get value at integer coordinates.
```python
def get_interpolated(self, pos: tuple[float, float]) -> float
```
Get bilinearly interpolated value at float coordinates.
```python
def get_slope(self, pos: tuple[int, int]) -> float
```
Get slope (0 to π/2) at position.
```python
def get_normal(self, pos: tuple[int, int]) -> tuple[float, float, float]
```
Get normalized surface normal vector at position.
```python
def min_max(self) -> tuple[float, float]
```
Return (minimum_value, maximum_value) across all cells.
```python
def count_in_range(self, range: tuple[float, float]) -> int
```
Count cells with values in the specified range.
---
### mcrfpy.NoiseSource
A configured noise generator function. Stateless and infinite - the same coordinates always produce the same value.
#### Constructor
```python
NoiseSource(dimensions: int = 2,
algorithm: str = "simplex",
hurst: float = 0.5,
lacunarity: float = 2.0,
seed: int = None)
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `dimensions` | `int` | 1-4. Number of input dimensions. Default: 2. |
| `algorithm` | `str` | `"simplex"`, `"perlin"`, or `"wavelet"`. Default: `"simplex"`. |
| `hurst` | `float` | Fractal Hurst exponent for fbm/turbulence. Default: 0.5. |
| `lacunarity` | `float` | Frequency multiplier between octaves. Default: 2.0. |
| `seed` | `int` | Random seed. None for random. |
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| `dimensions` | `int` | Read-only. |
| `algorithm` | `str` | Read-only. |
| `hurst` | `float` | Read-only. |
| `lacunarity` | `float` | Read-only. |
| `seed` | `int` | Read-only. |
#### Point Queries
```python
def get(self, pos: tuple[float, ...]) -> float
```
Get flat noise value at coordinates. Tuple length must match dimensions. Returns -1.0 to 1.0.
```python
def fbm(self, pos: tuple[float, ...], octaves: int = 4) -> float
```
Get fractal brownian motion value. Returns -1.0 to 1.0.
```python
def turbulence(self, pos: tuple[float, ...], octaves: int = 4) -> float
```
Get turbulence (absolute fbm) value. Returns -1.0 to 1.0.
#### Batch Sampling
```python
def sample(self,
size: tuple[int, int],
world_region: tuple = None,
mode: str = "fbm",
octaves: int = 4) -> NoiseSample
```
Create a NoiseSample (HeightMap subclass) by sampling a region.
| Parameter | Type | Description |
|-----------|------|-------------|
| `size` | `(int, int)` | Output dimensions in cells. |
| `world_region` | `((x1,y1), (x2,y2))` | World coordinates to sample. Default: `((0,0), size)`. |
| `mode` | `str` | `"flat"`, `"fbm"`, or `"turbulence"`. Default: `"fbm"`. |
| `octaves` | `int` | Octaves for fbm/turbulence. Default: 4. |
---
### mcrfpy.NoiseSample
A HeightMap created by sampling a NoiseSource. Tracks its origin for convenient adjacent sampling and rescaling.
#### Inheritance
`NoiseSample` extends `HeightMap` - all HeightMap methods are available.
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| `source` | `NoiseSource` | Read-only. The generator that created this sample. |
| `world_region` | `((x1,y1), (x2,y2))` | Read-only. World coordinates that were sampled. |
| `mode` | `str` | Read-only. `"flat"`, `"fbm"`, or `"turbulence"`. |
| `octaves` | `int` | Read-only. Octaves used for sampling. |
#### Adjacent Sampling
```python
def next_left(self) -> NoiseSample
def next_right(self) -> NoiseSample
def next_up(self) -> NoiseSample
def next_down(self) -> NoiseSample
```
Sample the adjacent region in the specified direction. Returns a new NoiseSample with the same size, mode, and octaves, but shifted world_region.
#### Rescaling
```python
def resample(self, size: tuple[int, int]) -> NoiseSample
```
Resample the same world_region at a different output resolution. Useful for minimaps (smaller size) or detail views (larger size).
```python
def zoom(self, factor: float) -> NoiseSample
```
Sample a different-sized world region at the same output size.
- `factor > 1.0`: Zoom in (smaller world region, more detail)
- `factor < 1.0`: Zoom out (larger world region, overview)
---
### mcrfpy.BSP
Binary space partition tree for rectangular regions. Useful for dungeon rooms, zones, or any hierarchical spatial organization.
#### Constructor
```python
BSP(bounds: tuple[tuple[int, int], tuple[int, int]])
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `bounds` | `((x, y), (w, h))` | Position and size of the root region. |
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| `bounds` | `((x, y), (w, h))` | Read-only. Root node bounds. |
| `root` | `BSPNode` | Read-only. Reference to the root node. |
#### Splitting
```python
def split_once(self, horizontal: bool, position: int) -> BSP
```
Split the root node once. Returns self for chaining.
```python
def split_recursive(self,
depth: int,
min_size: tuple[int, int],
max_ratio: float = 1.5,
seed: int = None) -> BSP
```
Recursively split to the specified depth.
| Parameter | Type | Description |
|-----------|------|-------------|
| `depth` | `int` | Maximum recursion depth. Creates up to 2^depth leaves. |
| `min_size` | `(int, int)` | Minimum (width, height) for a node to be split. |
| `max_ratio` | `float` | Maximum aspect ratio before forcing split direction. Default: 1.5. |
| `seed` | `int` | Random seed. None for random. |
```python
def clear(self) -> BSP
```
Remove all children, keeping only the root node with original bounds.
#### Iteration
```python
def leaves(self) -> Iterator[BSPNode]
```
Iterate all leaf nodes (the actual rooms).
```python
def traverse(self, order: Traversal = Traversal.LEVEL_ORDER) -> Iterator[BSPNode]
```
Iterate all nodes in the specified order.
#### Queries
```python
def find(self, pos: tuple[int, int]) -> BSPNode
```
Find the smallest (deepest) node containing the position.
```python
def find_path(self, start: BSPNode, end: BSPNode) -> tuple[BSPNode, ...]
```
Find a sequence of sibling-connected nodes between start and end leaves. Useful for corridor generation.
#### HeightMap Generation
```python
def to_heightmap(self,
size: tuple[int, int] = None,
select: str = "leaves",
nodes: list[BSPNode] = None,
shrink: int = 0,
value: float = 1.0) -> BSPMap
```
Create a BSPMap (HeightMap subclass) from selected nodes.
| Parameter | Type | Description |
|-----------|------|-------------|
| `size` | `(int, int)` | Output size. Default: bounds size. |
| `select` | `str` | `"leaves"`, `"all"`, or `"internal"`. Default: `"leaves"`. |
| `nodes` | `list[BSPNode]` | Override: specific nodes only. |
| `shrink` | `int` | Pixels to shrink from each node's bounds. Default: 0. |
| `value` | `float` | Value inside selected regions. Default: 1.0. |
---
### mcrfpy.BSPNode
A lightweight reference to a node in a BSP tree. Read-only.
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| `bounds` | `((x, y), (w, h))` | Position and size of this node. |
| `level` | `int` | Depth in tree (0 for root). |
| `index` | `int` | Unique index within the tree. |
| `is_leaf` | `bool` | True if this node has no children. |
| `split_horizontal` | `bool \| None` | Split orientation. None if leaf. |
| `split_position` | `int \| None` | Split coordinate. None if leaf. |
#### Navigation
| Property | Type | Description |
|----------|------|-------------|
| `left` | `BSPNode \| None` | Left child, or None if leaf. |
| `right` | `BSPNode \| None` | Right child, or None if leaf. |
| `parent` | `BSPNode \| None` | Parent node, or None if root. |
| `sibling` | `BSPNode \| None` | Other child of parent, or None. |
#### Methods
```python
def contains(self, pos: tuple[int, int]) -> bool
```
Check if position is inside this node's bounds.
```python
def center(self) -> tuple[int, int]
```
Return the center point of this node's bounds.
---
### mcrfpy.BSPMap
A HeightMap created from BSP node selection. Tracks its origin for convenient re-querying.
#### Inheritance
`BSPMap` extends `HeightMap` - all HeightMap methods are available.
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| `bsp` | `BSP` | Read-only. The tree that created this map. |
| `nodes` | `tuple[BSPNode, ...]` | Read-only. Nodes represented in this map. |
| `shrink` | `int` | Read-only. Margin applied from bounds. |
#### BSP-Specific Operations
```python
def inverse(self) -> BSPMap
```
Return a new BSPMap with walls instead of rooms (inverts the selection within BSP bounds).
```python
def expand(self, amount: int = 1) -> BSPMap
```
Return a new BSPMap with node bounds grown by amount.
```python
def contract(self, amount: int = 1) -> BSPMap
```
Return a new BSPMap with node bounds shrunk by additional amount.
#### Re-querying
```python
def with_nodes(self, nodes: list[BSPNode]) -> BSPMap
```
Return a new BSPMap with different node selection.
```python
def with_shrink(self, shrink: int) -> BSPMap
```
Return a new BSPMap with different shrink value.
---
### mcrfpy.Traversal
Enumeration for BSP tree traversal orders.
```python
class Traversal(Enum):
PRE_ORDER = "pre" # Node, then left, then right
IN_ORDER = "in" # Left, then node, then right
POST_ORDER = "post" # Left, then right, then node
LEVEL_ORDER = "level" # Top to bottom, left to right
INVERTED_LEVEL_ORDER = "level_inverted" # Bottom to top, right to left
```
---
## Application to Grid and Layers
### Grid.apply_threshold
```python
def apply_threshold(self,
source: HeightMap,
range: tuple[float, float],
walkable: bool = None,
transparent: bool = None) -> Grid
```
Set walkable/transparent properties where source value is in range.
| Parameter | Type | Description |
|-----------|------|-------------|
| `source` | `HeightMap` | Must match grid size. |
| `range` | `(float, float)` | (min, max) inclusive range. |
| `walkable` | `bool \| None` | Set walkable to this value. None = don't change. |
| `transparent` | `bool \| None` | Set transparent to this value. None = don't change. |
### Grid.apply_ranges
```python
def apply_ranges(self,
source: HeightMap,
ranges: list[tuple]) -> Grid
```
Apply multiple thresholds in a single pass.
```python
# Example
grid.apply_ranges(terrain, [
((0.0, 0.3), {"walkable": False, "transparent": True}), # Water
((0.3, 0.8), {"walkable": True, "transparent": True}), # Land
((0.8, 1.0), {"walkable": False, "transparent": False}), # Mountains
])
```
### TileLayer.apply_threshold
```python
def apply_threshold(self,
source: HeightMap,
range: tuple[float, float],
tile: int) -> TileLayer
```
Set tile index where source value is in range.
### TileLayer.apply_ranges
```python
def apply_ranges(self,
source: HeightMap,
ranges: list[tuple]) -> TileLayer
```
Apply multiple tile assignments in a single pass.
```python
# Example
tiles.apply_ranges(terrain, [
((0.0, 0.2), DEEP_WATER),
((0.2, 0.3), SHALLOW_WATER),
((0.3, 0.5), SAND),
((0.5, 0.7), GRASS),
((0.7, 0.85), ROCK),
((0.85, 1.0), SNOW),
])
```
### ColorLayer.apply_threshold
```python
def apply_threshold(self,
source: HeightMap,
range: tuple[float, float],
color: tuple[int, ...]) -> ColorLayer
```
Set fixed color where source value is in range. Color is (R, G, B) or (R, G, B, A).
### ColorLayer.apply_gradient
```python
def apply_gradient(self,
source: HeightMap,
range: tuple[float, float],
color_low: tuple[int, ...],
color_high: tuple[int, ...]) -> ColorLayer
```
Interpolate between colors based on source value within range.
- At range minimum → color_low
- At range maximum → color_high
### ColorLayer.apply_ranges
```python
def apply_ranges(self,
source: HeightMap,
ranges: list[tuple]) -> ColorLayer
```
Apply multiple color assignments. Each range maps to either a fixed color or a gradient.
```python
# Example
colors.apply_ranges(terrain, [
((0.0, 0.3), (0, 0, 180)), # Fixed blue
((0.3, 0.7), ((50, 120, 50), (100, 200, 100))), # Gradient green (tuple of 2)
((0.7, 1.0), ((100, 100, 100), (255, 255, 255))), # Gradient gray to white
])
```
---
## Usage Examples
### Example 1: Simple Dungeon
```python
import mcrfpy
# Create BSP dungeon layout
bsp = mcrfpy.BSP(bounds=((0, 0), (80, 50)))
bsp.split_recursive(depth=4, min_size=(6, 6), max_ratio=1.5)
# Generate room mask (1.0 inside rooms, 0.0 in walls)
rooms = bsp.to_heightmap(select="leaves", shrink=1)
# Apply to grid
grid.apply_threshold(rooms, range=(0.5, 1.0), walkable=True, transparent=True)
# Apply floor tiles
tiles.apply_threshold(rooms, range=(0.5, 1.0), tile=FLOOR_TILE)
# Walls are left as default (not in threshold range)
```
### Example 2: Natural Caves
```python
import mcrfpy
# Create noise generator
noise = mcrfpy.NoiseSource(algorithm="simplex", seed=42)
# Sample onto heightmap
cave = noise.sample(
size=(100, 100),
world_region=((0, 0), (10, 10)), # 10x zoom for smooth features
mode="fbm",
octaves=4
)
cave.normalize()
# Open areas where noise > 0.45
grid.apply_threshold(cave, range=(0.45, 1.0), walkable=True, transparent=True)
tiles.apply_threshold(cave, range=(0.45, 1.0), tile=CAVE_FLOOR)
tiles.apply_threshold(cave, range=(0.0, 0.45), tile=CAVE_WALL)
```
### Example 3: Overworld with Rivers
```python
import mcrfpy
# Base terrain
terrain = mcrfpy.HeightMap(size=(200, 200))
terrain.mid_point_displacement(roughness=0.5)
# Add hills
terrain.add_hill(center=(50, 80), radius=20, height=0.4)
terrain.add_hill(center=(150, 120), radius=25, height=0.5)
# Carve river
terrain.dig_bezier(
points=((10, 100), (60, 80), (140, 120), (190, 100)),
start_radius=2, end_radius=5,
start_depth=0.5, end_depth=0.5
)
# Erosion for realism
terrain.rain_erosion(drops=8000)
terrain.normalize()
# Apply terrain bands
grid.apply_ranges(terrain, [
((0.0, 0.25), {"walkable": False, "transparent": True}), # Water
((0.25, 0.75), {"walkable": True, "transparent": True}), # Land
((0.75, 1.0), {"walkable": False, "transparent": False}), # Mountains
])
tiles.apply_ranges(terrain, [
((0.0, 0.25), WATER_TILE),
((0.25, 0.4), SAND_TILE),
((0.4, 0.65), GRASS_TILE),
((0.65, 0.8), ROCK_TILE),
((0.8, 1.0), SNOW_TILE),
])
colors.apply_ranges(terrain, [
((0.0, 0.25), ((0, 50, 150), (0, 100, 200))),
((0.25, 0.65), ((80, 160, 80), (120, 200, 120))),
((0.65, 1.0), ((120, 120, 120), (255, 255, 255))),
])
```
### Example 4: Dungeon with Varied Room Terrain
```python
import mcrfpy
# Create dungeon structure
bsp = mcrfpy.BSP(bounds=((0, 0), (100, 100)))
bsp.split_recursive(depth=5, min_size=(8, 8))
# Room mask
rooms = bsp.to_heightmap(select="leaves", shrink=1)
# Create terrain variation
noise = mcrfpy.NoiseSource(seed=123)
terrain = mcrfpy.HeightMap(size=(100, 100))
terrain.add_noise(noise, mode="fbm", octaves=4)
terrain.normalize()
# Mask terrain to rooms only
terrain.multiply(rooms)
# Apply varied floor tiles within rooms
tiles.apply_threshold(rooms, range=(0.5, 1.0), tile=STONE_FLOOR) # Base floor
tiles.apply_threshold(terrain, range=(0.3, 0.5), tile=MOSSY_FLOOR)
tiles.apply_threshold(terrain, range=(0.5, 0.7), tile=CRACKED_FLOOR)
# Walkability from rooms mask
grid.apply_threshold(rooms, range=(0.5, 1.0), walkable=True, transparent=True)
```
### Example 5: Infinite World with Chunks
```python
import mcrfpy
# Persistent world generator
WORLD_SEED = 12345
world_noise = mcrfpy.NoiseSource(seed=WORLD_SEED)
CHUNK_SIZE = 64
def generate_chunk(chunk_x: int, chunk_y: int) -> mcrfpy.NoiseSample:
"""Generate terrain for a world chunk."""
return world_noise.sample(
size=(CHUNK_SIZE, CHUNK_SIZE),
world_region=(
(chunk_x * CHUNK_SIZE, chunk_y * CHUNK_SIZE),
((chunk_x + 1) * CHUNK_SIZE, (chunk_y + 1) * CHUNK_SIZE)
),
mode="fbm",
octaves=6
)
# Generate current chunk
current_chunk = generate_chunk(0, 0)
# When player moves right, get adjacent chunk
next_chunk = current_chunk.next_right()
# Generate minimap of large area
minimap = world_noise.sample(
size=(100, 100),
world_region=((0, 0), (1000, 1000)),
mode="fbm",
octaves=3
)
```
### Example 6: Biome Blending
```python
import mcrfpy
# Multiple noise layers
elevation_noise = mcrfpy.NoiseSource(seed=1)
moisture_noise = mcrfpy.NoiseSource(seed=2)
# Sample both
elevation = mcrfpy.HeightMap(size=(200, 200))
elevation.add_noise(elevation_noise, mode="fbm", octaves=6)
elevation.normalize()
moisture = mcrfpy.HeightMap(size=(200, 200))
moisture.add_noise(moisture_noise, mode="fbm", octaves=4)
moisture.normalize()
# Desert: low moisture AND medium elevation
desert_mask = moisture.threshold((0.0, 0.3))
desert_mask.multiply(elevation.threshold((0.2, 0.6)))
# Forest: high moisture AND low elevation
forest_mask = moisture.threshold((0.6, 1.0))
forest_mask.multiply(elevation.threshold((0.1, 0.5)))
# Swamp: high moisture AND very low elevation
swamp_mask = moisture.threshold((0.7, 1.0))
swamp_mask.multiply(elevation.threshold((0.0, 0.2)))
# Apply biome tiles (later biomes override earlier)
tiles.apply_threshold(elevation, range=(0.0, 1.0), tile=GRASS_TILE) # Default
tiles.apply_threshold(desert_mask, range=(0.5, 1.0), tile=SAND_TILE)
tiles.apply_threshold(forest_mask, range=(0.5, 1.0), tile=TREE_TILE)
tiles.apply_threshold(swamp_mask, range=(0.5, 1.0), tile=SWAMP_TILE)
```
---
## Implementation Notes
### Size Matching
All HeightMap operations between two heightmaps require matching sizes. The `apply_*` methods on Grid/Layer also require the source HeightMap to match the Grid's `grid_size`.
```python
# This will raise ValueError
small = mcrfpy.HeightMap(size=(50, 50))
large = mcrfpy.HeightMap(size=(100, 100))
large.add(small) # Error: size mismatch
```
### Value Ranges
- NoiseSource outputs: -1.0 to 1.0
- HeightMap after `normalize()`: 0.0 to 1.0 (by default)
- Threshold operations: work on any float range
Recommendation: Always `normalize()` before applying to Grid/Layer for predictable threshold behavior.
### Performance Considerations
For grids up to 1000×1000 (1M cells):
| Operation | Approximate Cost |
|-----------|------------------|
| HeightMap creation | O(n) |
| add/multiply/scale | O(n), parallelizable |
| add_noise | O(n × octaves), parallelizable |
| mid_point_displacement | O(n log n) |
| rain_erosion | O(drops) |
| apply_ranges | O(n × num_ranges), single pass |
All operations execute entirely in C++ with no Python callbacks.
---
## Future Considerations
The following features are explicitly **out of scope** for the initial implementation but should not be precluded by the design:
1. **Serialization**: Saving/loading HeightMaps to disk for pre-generated worlds.
2. **Region Operations**: Applying operations to sub-regions when HeightMap sizes don't match.
3. **Corridor Generation**: Built-in BSP corridor creation (currently user code using `find_path` and `dig_bezier` or manual fills).
4. **Custom Kernels**: User-defined kernel transforms beyond simple smoothing.
---
## Appendix: libtcod Function Mapping
| libtcod Function | McRogueFace Equivalent |
|------------------|------------------------|
| `TCOD_noise_new` | `NoiseSource()` |
| `TCOD_noise_get` | `NoiseSource.get()` |
| `TCOD_noise_get_fbm` | `NoiseSource.fbm()` |
| `TCOD_noise_get_turbulence` | `NoiseSource.turbulence()` |
| `TCOD_heightmap_new` | `HeightMap()` |
| `TCOD_heightmap_add_hill` | `HeightMap.add_hill()` |
| `TCOD_heightmap_dig_hill` | `HeightMap.dig_hill()` |
| `TCOD_heightmap_rain_erosion` | `HeightMap.rain_erosion()` |
| `TCOD_heightmap_add_voronoi` | `HeightMap.add_voronoi()` |
| `TCOD_heightmap_mid_point_displacement` | `HeightMap.mid_point_displacement()` |
| `TCOD_heightmap_dig_bezier` | `HeightMap.dig_bezier()` |
| `TCOD_heightmap_add_fbm` | `HeightMap.add_noise(..., mode="fbm")` |
| `TCOD_heightmap_scale_fbm` | `HeightMap.multiply_noise(..., mode="fbm")` |
| `TCOD_heightmap_normalize` | `HeightMap.normalize()` |
| `TCOD_heightmap_clamp` | `HeightMap.clamp()` |
| `TCOD_heightmap_add` | `HeightMap.add()` |
| `TCOD_heightmap_multiply` | `HeightMap.multiply()` |
| `TCOD_heightmap_lerp` | `HeightMap.lerp()` |
| `TCOD_bsp_new_with_size` | `BSP()` |
| `TCOD_bsp_split_once` | `BSP.split_once()` |
| `TCOD_bsp_split_recursive` | `BSP.split_recursive()` |
| `TCOD_bsp_traverse_*` | `BSP.traverse(order=...)` |
| `TCOD_bsp_is_leaf` | `BSPNode.is_leaf` |
| `TCOD_bsp_contains` | `BSPNode.contains()` |
| `TCOD_bsp_find_node` | `BSP.find()` |