Implement the foundational HeightMap class for procedural generation: - HeightMap(size, fill=0.0) constructor with libtcod backend - Immutable size property after construction - Scalar operations returning self for method chaining: - fill(value), clear() - add_constant(value), scale(factor) - clamp(min=0.0, max=1.0), normalize(min=0.0, max=1.0) Includes procedural generation spec document and unit tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
32 KiB
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
-
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.
-
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.
-
Composability: HeightMaps can be combined (add, multiply, lerp), allowing complex terrain from simple building blocks.
-
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. -
Mutation with Chaining: Operations mutate in place and return
selffor 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
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.
def fill(self, value: float) -> HeightMap
Set all cells to value.
def clear(self) -> HeightMap
Set all cells to 0.0. Equivalent to fill(0.0).
def add_constant(self, value: float) -> HeightMap
Add value to every cell.
def scale(self, factor: float) -> HeightMap
Multiply every cell by factor.
def clamp(self, min: float = 0.0, max: float = 1.0) -> HeightMap
Clamp all values to the range [min, max].
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.
def add(self, other: HeightMap) -> HeightMap
Add other to this heightmap cell-by-cell.
def subtract(self, other: HeightMap) -> HeightMap
Subtract other from this heightmap cell-by-cell.
def multiply(self, other: HeightMap) -> HeightMap
Multiply this heightmap by other cell-by-cell. Useful for masking.
def lerp(self, other: HeightMap, t: float) -> HeightMap
Linear interpolation: self = self + (other - self) * t.
def copy_from(self, other: HeightMap) -> HeightMap
Copy all values from other into this heightmap.
def max(self, other: HeightMap) -> HeightMap
Per-cell maximum of this and other.
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.
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. |
def multiply_noise(self, source: NoiseSource, **kwargs) -> HeightMap
Sample noise and multiply with current values. Same parameters as add_noise.
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. |
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.
def add_hill(self, center: tuple[int, int], radius: float, height: float) -> HeightMap
Add a hill (half-spheroid) at the specified position.
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).
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.
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.
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.
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.
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.
def threshold(self, range: tuple[float, float]) -> HeightMap
Return new HeightMap with original values where in range, 0.0 elsewhere.
def threshold_binary(self,
range: tuple[float, float],
value: float = 1.0) -> HeightMap
Return new HeightMap with value where in range, 0.0 elsewhere.
def inverse(self) -> HeightMap
Return new HeightMap with (1.0 - value) for each cell.
Queries
These methods return values to Python for inspection.
def get(self, pos: tuple[int, int]) -> float
Get value at integer coordinates.
def get_interpolated(self, pos: tuple[float, float]) -> float
Get bilinearly interpolated value at float coordinates.
def get_slope(self, pos: tuple[int, int]) -> float
Get slope (0 to π/2) at position.
def get_normal(self, pos: tuple[int, int]) -> tuple[float, float, float]
Get normalized surface normal vector at position.
def min_max(self) -> tuple[float, float]
Return (minimum_value, maximum_value) across all cells.
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
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
def get(self, pos: tuple[float, ...]) -> float
Get flat noise value at coordinates. Tuple length must match dimensions. Returns -1.0 to 1.0.
def fbm(self, pos: tuple[float, ...], octaves: int = 4) -> float
Get fractal brownian motion value. Returns -1.0 to 1.0.
def turbulence(self, pos: tuple[float, ...], octaves: int = 4) -> float
Get turbulence (absolute fbm) value. Returns -1.0 to 1.0.
Batch Sampling
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
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
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).
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
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
def split_once(self, horizontal: bool, position: int) -> BSP
Split the root node once. Returns self for chaining.
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. |
def clear(self) -> BSP
Remove all children, keeping only the root node with original bounds.
Iteration
def leaves(self) -> Iterator[BSPNode]
Iterate all leaf nodes (the actual rooms).
def traverse(self, order: Traversal = Traversal.LEVEL_ORDER) -> Iterator[BSPNode]
Iterate all nodes in the specified order.
Queries
def find(self, pos: tuple[int, int]) -> BSPNode
Find the smallest (deepest) node containing the position.
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
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
def contains(self, pos: tuple[int, int]) -> bool
Check if position is inside this node's bounds.
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
def inverse(self) -> BSPMap
Return a new BSPMap with walls instead of rooms (inverts the selection within BSP bounds).
def expand(self, amount: int = 1) -> BSPMap
Return a new BSPMap with node bounds grown by amount.
def contract(self, amount: int = 1) -> BSPMap
Return a new BSPMap with node bounds shrunk by additional amount.
Re-querying
def with_nodes(self, nodes: list[BSPNode]) -> BSPMap
Return a new BSPMap with different node selection.
def with_shrink(self, shrink: int) -> BSPMap
Return a new BSPMap with different shrink value.
mcrfpy.Traversal
Enumeration for BSP tree traversal orders.
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
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
def apply_ranges(self,
source: HeightMap,
ranges: list[tuple]) -> Grid
Apply multiple thresholds in a single pass.
# 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
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
def apply_ranges(self,
source: HeightMap,
ranges: list[tuple]) -> TileLayer
Apply multiple tile assignments in a single pass.
# 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
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
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
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.
# 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
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
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
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
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
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
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.
# 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:
-
Serialization: Saving/loading HeightMaps to disk for pre-generated worlds.
-
Region Operations: Applying operations to sub-regions when HeightMap sizes don't match.
-
Corridor Generation: Built-in BSP corridor creation (currently user code using
find_pathanddig_bezieror manual fills). -
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() |