Test suite modernization

This commit is contained in:
John McCardle 2026-02-09 08:15:18 -05:00
commit 52fdfd0347
141 changed files with 9947 additions and 4665 deletions

View file

@ -0,0 +1,8 @@
"""Demo implementations for interactive procedural generation."""
from .cave_demo import CaveDemo
from .dungeon_demo import DungeonDemo
from .terrain_demo import TerrainDemo
from .town_demo import TownDemo
__all__ = ['CaveDemo', 'DungeonDemo', 'TerrainDemo', 'TownDemo']

View file

@ -0,0 +1,362 @@
"""Cave Generation Demo - Cellular Automata
Demonstrates cellular automata cave generation with:
1. Random noise fill (based on seed + fill_percent)
2. Binary threshold application
3. Cellular automata smoothing passes
4. Flood fill to find connected regions
5. Keep largest connected region
"""
import mcrfpy
from typing import List
from ..core.demo_base import ProcgenDemoBase, StepDef, LayerDef
from ..core.parameter import Parameter
class CaveDemo(ProcgenDemoBase):
"""Interactive cellular automata cave generation demo."""
name = "Cave Generation"
description = "Cellular automata cave carving with noise and smoothing"
MAP_SIZE = (256, 256)
def define_steps(self) -> List[StepDef]:
"""Define the generation steps."""
return [
StepDef("Fill with noise", self.step_fill_noise,
"Initialize grid with random noise based on seed and fill percentage"),
StepDef("Apply threshold", self.step_threshold,
"Convert noise to binary wall/floor based on threshold"),
StepDef("Automata pass 1", self.step_automata_1,
"First cellular automata smoothing pass"),
StepDef("Automata pass 2", self.step_automata_2,
"Second cellular automata smoothing pass"),
StepDef("Automata pass 3", self.step_automata_3,
"Third cellular automata smoothing pass"),
StepDef("Find regions", self.step_find_regions,
"Flood fill to identify connected regions"),
StepDef("Keep largest", self.step_keep_largest,
"Keep only the largest connected region"),
]
def define_parameters(self) -> List[Parameter]:
"""Define configurable parameters."""
return [
Parameter(
name="seed",
display="Seed",
type="int",
default=42,
min_val=0,
max_val=99999,
step=1,
affects_step=0,
description="Random seed for noise generation"
),
Parameter(
name="fill_percent",
display="Fill %",
type="float",
default=0.45,
min_val=0.30,
max_val=0.70,
step=0.05,
affects_step=0,
description="Initial noise fill percentage"
),
Parameter(
name="threshold",
display="Threshold",
type="float",
default=0.50,
min_val=0.30,
max_val=0.70,
step=0.05,
affects_step=1,
description="Wall/floor threshold value"
),
Parameter(
name="wall_rule",
display="Wall Rule",
type="int",
default=5,
min_val=3,
max_val=7,
step=1,
affects_step=2,
description="Neighbors needed to become wall"
),
]
def define_layers(self) -> List[LayerDef]:
"""Define visualization layers."""
return [
LayerDef("final", "Final Cave", "color", z_index=-1, visible=True,
description="Final cave result"),
LayerDef("raw_noise", "Raw Noise", "color", z_index=0, visible=False,
description="Initial random noise"),
LayerDef("regions", "Regions", "color", z_index=1, visible=False,
description="Connected regions colored by ID"),
]
def __init__(self):
"""Initialize cave demo with heightmaps."""
super().__init__()
# Create working heightmaps
self.hmap_noise = self.create_heightmap("noise", 0.0)
self.hmap_binary = self.create_heightmap("binary", 0.0)
self.hmap_regions = self.create_heightmap("regions", 0.0)
# Region tracking
self.region_ids = [] # List of (id, size) tuples
self.largest_region_id = 0
# Noise source
self.noise = None
def _apply_colors_to_layer(self, layer, hmap, wall_color, floor_color, alpha=255):
"""Apply binary wall/floor colors to a layer based on heightmap."""
w, h = self.MAP_SIZE
for y in range(h):
for x in range(w):
val = hmap[x, y]
if val > 0.5:
c = mcrfpy.Color(wall_color.r, wall_color.g, wall_color.b, alpha)
layer.set((x, y), c)
else:
c = mcrfpy.Color(floor_color.r, floor_color.g, floor_color.b, alpha)
layer.set((x, y), c)
def _apply_gradient_to_layer(self, layer, hmap, alpha=255):
"""Apply gradient visualization to layer."""
w, h = self.MAP_SIZE
for y in range(h):
for x in range(w):
val = hmap[x, y]
v = int(val * 255)
layer.set((x, y), mcrfpy.Color(v, v, v, alpha))
# === Step Implementations ===
def step_fill_noise(self):
"""Step 1: Fill with random noise."""
seed = self.get_param("seed")
fill_pct = self.get_param("fill_percent")
# Create noise source with seed
self.noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm='simplex',
seed=seed
)
# Fill heightmap with noise
self.hmap_noise.fill(0.0)
self.hmap_noise.add_noise(
self.noise,
world_size=(50, 50), # Higher frequency for cave-like noise
mode='fbm',
octaves=1
)
self.hmap_noise.normalize(0.0, 1.0)
# Show on raw_noise layer (alpha=128 for overlay)
layer = self.get_layer("raw_noise")
self._apply_gradient_to_layer(layer, self.hmap_noise, alpha=128)
# Also show on final layer (full opacity)
final = self.get_layer("final")
self._apply_gradient_to_layer(final, self.hmap_noise, alpha=255)
def step_threshold(self):
"""Step 2: Apply binary threshold."""
threshold = self.get_param("threshold")
# Copy noise to binary and threshold
self.hmap_binary.copy_from(self.hmap_noise)
# Manual threshold since we want a specific cutoff
w, h = self.MAP_SIZE
for y in range(h):
for x in range(w):
if self.hmap_binary[x, y] >= threshold:
self.hmap_binary[x, y] = 1.0 # Wall
else:
self.hmap_binary[x, y] = 0.0 # Floor
# Visualize
final = self.get_layer("final")
wall = mcrfpy.Color(60, 55, 50)
floor = mcrfpy.Color(140, 130, 115)
self._apply_colors_to_layer(final, self.hmap_binary, wall, floor)
def _run_automata_pass(self):
"""Run one cellular automata pass."""
wall_rule = self.get_param("wall_rule")
w, h = self.MAP_SIZE
# Create copy of current state
old_data = []
for y in range(h):
row = []
for x in range(w):
row.append(self.hmap_binary[x, y])
old_data.append(row)
# Apply rules
for y in range(h):
for x in range(w):
# Count wall neighbors (including self)
walls = 0
for dy in range(-1, 2):
for dx in range(-1, 2):
nx, ny = x + dx, y + dy
if 0 <= nx < w and 0 <= ny < h:
if old_data[ny][nx] > 0.5:
walls += 1
else:
# Out of bounds counts as wall
walls += 1
# Apply rule: if neighbors >= wall_rule, become wall
if walls >= wall_rule:
self.hmap_binary[x, y] = 1.0
else:
self.hmap_binary[x, y] = 0.0
# Visualize
final = self.get_layer("final")
wall = mcrfpy.Color(60, 55, 50)
floor = mcrfpy.Color(140, 130, 115)
self._apply_colors_to_layer(final, self.hmap_binary, wall, floor)
def step_automata_1(self):
"""Step 3: First automata pass."""
self._run_automata_pass()
def step_automata_2(self):
"""Step 4: Second automata pass."""
self._run_automata_pass()
def step_automata_3(self):
"""Step 5: Third automata pass."""
self._run_automata_pass()
def step_find_regions(self):
"""Step 6: Flood fill to find connected floor regions."""
w, h = self.MAP_SIZE
# Reset region data
self.hmap_regions.fill(0.0)
self.region_ids = []
# Track visited cells
visited = [[False] * w for _ in range(h)]
region_id = 0
# Region colors (for visualization) - alpha=128 for overlay
region_colors = [
mcrfpy.Color(200, 80, 80, 128),
mcrfpy.Color(80, 200, 80, 128),
mcrfpy.Color(80, 80, 200, 128),
mcrfpy.Color(200, 200, 80, 128),
mcrfpy.Color(200, 80, 200, 128),
mcrfpy.Color(80, 200, 200, 128),
mcrfpy.Color(180, 120, 60, 128),
mcrfpy.Color(120, 60, 180, 128),
]
# Find all floor regions
for start_y in range(h):
for start_x in range(w):
if visited[start_y][start_x]:
continue
if self.hmap_binary[start_x, start_y] > 0.5:
# Wall cell
visited[start_y][start_x] = True
continue
# Flood fill this region
region_id += 1
region_size = 0
stack = [(start_x, start_y)]
while stack:
x, y = stack.pop()
if visited[y][x]:
continue
if self.hmap_binary[x, y] > 0.5:
continue
visited[y][x] = True
self.hmap_regions[x, y] = region_id
region_size += 1
# Add neighbors
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < w and 0 <= ny < h and not visited[ny][nx]:
stack.append((nx, ny))
self.region_ids.append((region_id, region_size))
# Sort by size descending
self.region_ids.sort(key=lambda x: x[1], reverse=True)
if self.region_ids:
self.largest_region_id = self.region_ids[0][0]
# Visualize regions (alpha=128 for overlay)
regions_layer = self.get_layer("regions")
for y in range(h):
for x in range(w):
rid = int(self.hmap_regions[x, y])
if rid > 0:
color = region_colors[(rid - 1) % len(region_colors)]
regions_layer.set((x, y), color)
else:
regions_layer.set((x, y), mcrfpy.Color(30, 30, 35, 128))
# Show region count
print(f"Found {len(self.region_ids)} regions")
def step_keep_largest(self):
"""Step 7: Keep only the largest connected region."""
if not self.region_ids:
return
w, h = self.MAP_SIZE
# Fill all non-largest regions with wall
for y in range(h):
for x in range(w):
rid = int(self.hmap_regions[x, y])
if rid == 0 or rid != self.largest_region_id:
self.hmap_binary[x, y] = 1.0 # Make wall
# else: keep as floor
# Visualize final result
final = self.get_layer("final")
wall = mcrfpy.Color(45, 40, 38)
floor = mcrfpy.Color(160, 150, 130)
self._apply_colors_to_layer(final, self.hmap_binary, wall, floor)
# Also update regions visualization (alpha=128 for overlay)
regions_layer = self.get_layer("regions")
for y in range(h):
for x in range(w):
if self.hmap_binary[x, y] > 0.5:
regions_layer.set((x, y), mcrfpy.Color(30, 30, 35, 128))
else:
regions_layer.set((x, y), mcrfpy.Color(80, 200, 80, 128))
def main():
"""Run the cave demo standalone."""
demo = CaveDemo()
demo.activate()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,532 @@
"""Dungeon Generation Demo - BSP + Corridors
Demonstrates BSP dungeon generation with:
1. Create BSP and split recursively
2. Visualize all BSP partitions (educational)
3. Extract leaf nodes as rooms
4. Shrink leaves to create room margins
5. Build adjacency graph (which rooms neighbor)
6. Connect adjacent rooms with corridors
7. Composite rooms + corridors
"""
import mcrfpy
from typing import List, Dict, Tuple, Set
from ..core.demo_base import ProcgenDemoBase, StepDef, LayerDef
from ..core.parameter import Parameter
class DungeonDemo(ProcgenDemoBase):
"""Interactive BSP dungeon generation demo."""
name = "Dungeon (BSP)"
description = "Binary Space Partitioning with adjacency-based corridors"
MAP_SIZE = (128, 96) # Smaller for better visibility of rooms
def define_steps(self) -> List[StepDef]:
"""Define the generation steps."""
return [
StepDef("Create BSP tree", self.step_create_bsp,
"Initialize BSP and split recursively"),
StepDef("Show all partitions", self.step_show_partitions,
"Visualize the full BSP tree structure"),
StepDef("Extract rooms", self.step_extract_rooms,
"Get leaf nodes as potential room spaces"),
StepDef("Shrink rooms", self.step_shrink_rooms,
"Add margins between rooms"),
StepDef("Build adjacency", self.step_build_adjacency,
"Find which rooms are neighbors"),
StepDef("Dig corridors", self.step_dig_corridors,
"Connect adjacent rooms with corridors"),
StepDef("Composite", self.step_composite,
"Combine rooms and corridors for final dungeon"),
]
def define_parameters(self) -> List[Parameter]:
"""Define configurable parameters."""
return [
Parameter(
name="seed",
display="Seed",
type="int",
default=42,
min_val=0,
max_val=99999,
step=1,
affects_step=0,
description="Random seed for BSP splits"
),
Parameter(
name="depth",
display="BSP Depth",
type="int",
default=4,
min_val=2,
max_val=6,
step=1,
affects_step=0,
description="BSP recursion depth"
),
Parameter(
name="min_room_w",
display="Min Room W",
type="int",
default=8,
min_val=4,
max_val=16,
step=2,
affects_step=0,
description="Minimum room width"
),
Parameter(
name="min_room_h",
display="Min Room H",
type="int",
default=6,
min_val=4,
max_val=12,
step=2,
affects_step=0,
description="Minimum room height"
),
Parameter(
name="shrink",
display="Room Shrink",
type="int",
default=2,
min_val=0,
max_val=4,
step=1,
affects_step=3,
description="Room inset from leaf bounds"
),
Parameter(
name="corridor_width",
display="Corridor W",
type="int",
default=2,
min_val=1,
max_val=3,
step=1,
affects_step=5,
description="Corridor thickness"
),
]
def define_layers(self) -> List[LayerDef]:
"""Define visualization layers."""
return [
LayerDef("final", "Final Dungeon", "color", z_index=-1, visible=True,
description="Combined rooms and corridors"),
LayerDef("bsp_tree", "BSP Tree", "color", z_index=0, visible=False,
description="All BSP partition boundaries"),
LayerDef("rooms", "Rooms Only", "color", z_index=1, visible=False,
description="Room areas without corridors"),
LayerDef("corridors", "Corridors", "color", z_index=2, visible=False,
description="Corridor paths only"),
LayerDef("adjacency", "Adjacency", "color", z_index=3, visible=False,
description="Lines between adjacent room centers"),
]
def __init__(self):
"""Initialize dungeon demo."""
super().__init__()
# BSP data
self.bsp = None
self.leaves = []
self.rooms = [] # List of (x, y, w, h) tuples
self.room_centers = [] # List of (cx, cy) tuples
self.adjacencies = [] # List of (room_idx_1, room_idx_2) pairs
# HeightMaps for visualization
self.hmap_rooms = self.create_heightmap("rooms", 0.0)
self.hmap_corridors = self.create_heightmap("corridors", 0.0)
def _clear_layers(self):
"""Clear all visualization layers."""
for layer in self.layers.values():
layer.fill(mcrfpy.Color(30, 28, 26))
def _draw_rect(self, layer, x, y, w, h, color, outline_only=False, alpha=None):
"""Draw a rectangle on a layer."""
map_w, map_h = self.MAP_SIZE
# Apply alpha if specified
if alpha is not None:
color = mcrfpy.Color(color.r, color.g, color.b, alpha)
if outline_only:
# Draw just the outline
for px in range(x, x + w):
if 0 <= px < map_w:
if 0 <= y < map_h:
layer.set((px, y), color)
if 0 <= y + h - 1 < map_h:
layer.set((px, y + h - 1), color)
for py in range(y, y + h):
if 0 <= py < map_h:
if 0 <= x < map_w:
layer.set((x, py), color)
if 0 <= x + w - 1 < map_w:
layer.set((x + w - 1, py), color)
else:
# Fill the rectangle
for py in range(y, y + h):
for px in range(x, x + w):
if 0 <= px < map_w and 0 <= py < map_h:
layer.set((px, py), color)
def _draw_line(self, layer, x0, y0, x1, y1, color, width=1, alpha=None):
"""Draw a line on a layer using Bresenham's algorithm."""
map_w, map_h = self.MAP_SIZE
# Apply alpha if specified
if alpha is not None:
color = mcrfpy.Color(color.r, color.g, color.b, alpha)
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
# Draw width around center point
for wo in range(-(width // 2), width // 2 + 1):
for ho in range(-(width // 2), width // 2 + 1):
px, py = x0 + wo, y0 + ho
if 0 <= px < map_w and 0 <= py < map_h:
layer.set((px, py), color)
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
# === Step Implementations ===
def step_create_bsp(self):
"""Step 1: Create and split BSP tree."""
seed = self.get_param("seed")
depth = self.get_param("depth")
min_w = self.get_param("min_room_w")
min_h = self.get_param("min_room_h")
w, h = self.MAP_SIZE
# Create BSP covering the map (with margin)
margin = 2
self.bsp = mcrfpy.BSP(
pos=(margin, margin),
size=(w - margin * 2, h - margin * 2)
)
# Split recursively
self.bsp.split_recursive(
depth=depth,
min_size=(min_w, min_h),
seed=seed
)
# Clear and show initial state
self._clear_layers()
final = self.get_layer("final")
final.fill(mcrfpy.Color(30, 28, 26))
# Draw BSP root bounds
bsp_layer = self.get_layer("bsp_tree")
bsp_layer.fill(mcrfpy.Color(30, 28, 26))
x, y = self.bsp.pos
w, h = self.bsp.size
self._draw_rect(bsp_layer, x, y, w, h, mcrfpy.Color(80, 80, 100), outline_only=True)
def step_show_partitions(self):
"""Step 2: Visualize all BSP partitions."""
bsp_layer = self.get_layer("bsp_tree")
bsp_layer.fill(mcrfpy.Color(30, 28, 26, 128)) # alpha=128 for overlay
# Color palette for different depths (alpha=128 for overlay)
depth_colors = [
mcrfpy.Color(120, 60, 60, 128),
mcrfpy.Color(60, 120, 60, 128),
mcrfpy.Color(60, 60, 120, 128),
mcrfpy.Color(120, 120, 60, 128),
mcrfpy.Color(120, 60, 120, 128),
mcrfpy.Color(60, 120, 120, 128),
]
def draw_node(node, depth=0):
"""Recursively draw BSP nodes."""
x, y = node.pos
w, h = node.size
color = depth_colors[depth % len(depth_colors)]
# Draw outline
self._draw_rect(bsp_layer, x, y, w, h, color, outline_only=True)
# Draw children using left/right
if node.left:
draw_node(node.left, depth + 1)
if node.right:
draw_node(node.right, depth + 1)
# Start from root
root = self.bsp.root
if root:
draw_node(root)
# Also show on final layer
final = self.get_layer("final")
# Copy bsp_tree to final
w, h = self.MAP_SIZE
for y in range(h):
for x in range(w):
c = bsp_layer.at(x, y)
final.set((x, y), c)
def step_extract_rooms(self):
"""Step 3: Extract leaf nodes as rooms."""
# Get all leaves
self.leaves = list(self.bsp.leaves())
self.rooms = []
self.room_centers = []
rooms_layer = self.get_layer("rooms")
rooms_layer.fill(mcrfpy.Color(30, 28, 26, 128))
# Draw each leaf as a room (alpha=128 for overlay)
room_colors = [
mcrfpy.Color(100, 80, 60, 128),
mcrfpy.Color(80, 100, 60, 128),
mcrfpy.Color(60, 80, 100, 128),
mcrfpy.Color(100, 100, 60, 128),
]
for i, leaf in enumerate(self.leaves):
x, y = leaf.pos
w, h = leaf.size
self.rooms.append((x, y, w, h))
self.room_centers.append((x + w // 2, y + h // 2))
color = room_colors[i % len(room_colors)]
self._draw_rect(rooms_layer, x, y, w, h, color)
# Also show on final
final = self.get_layer("final")
map_w, map_h = self.MAP_SIZE
for y in range(map_h):
for x in range(map_w):
c = rooms_layer.at(x, y)
final.set((x, y), c)
print(f"Extracted {len(self.rooms)} rooms")
def step_shrink_rooms(self):
"""Step 4: Shrink rooms to add margins."""
shrink = self.get_param("shrink")
rooms_layer = self.get_layer("rooms")
rooms_layer.fill(mcrfpy.Color(30, 28, 26, 128))
# Shrink each room
shrunk_rooms = []
shrunk_centers = []
room_color = mcrfpy.Color(120, 100, 80, 128) # alpha=128 for overlay
for x, y, w, h in self.rooms:
# Apply shrink
nx = x + shrink
ny = y + shrink
nw = w - shrink * 2
nh = h - shrink * 2
# Ensure minimum size
if nw >= 3 and nh >= 3:
shrunk_rooms.append((nx, ny, nw, nh))
shrunk_centers.append((nx + nw // 2, ny + nh // 2))
self._draw_rect(rooms_layer, nx, ny, nw, nh, room_color)
# Store in heightmap for later
map_w, map_h = self.MAP_SIZE
for py in range(ny, ny + nh):
for px in range(nx, nx + nw):
if 0 <= px < map_w and 0 <= py < map_h:
self.hmap_rooms[px, py] = 1.0
self.rooms = shrunk_rooms
self.room_centers = shrunk_centers
# Update final
final = self.get_layer("final")
map_w, map_h = self.MAP_SIZE
for y in range(map_h):
for x in range(map_w):
c = rooms_layer.at(x, y)
final.set((x, y), c)
print(f"Shrunk to {len(self.rooms)} valid rooms")
def step_build_adjacency(self):
"""Step 5: Build adjacency graph between rooms."""
self.adjacencies = []
# Simple adjacency: rooms whose bounding boxes are close enough
# In a real implementation, use BSP adjacency
# For each pair of rooms, check if they share an edge
for i in range(len(self.rooms)):
for j in range(i + 1, len(self.rooms)):
r1 = self.rooms[i]
r2 = self.rooms[j]
# Check if rooms are adjacent (share edge or close)
if self._rooms_adjacent(r1, r2):
self.adjacencies.append((i, j))
# Visualize adjacency lines (alpha=128 for overlay)
adj_layer = self.get_layer("adjacency")
adj_layer.fill(mcrfpy.Color(30, 28, 26, 128))
line_color = mcrfpy.Color(200, 100, 100, 160) # semi-transparent overlay
for i, j in self.adjacencies:
c1 = self.room_centers[i]
c2 = self.room_centers[j]
self._draw_line(adj_layer, c1[0], c1[1], c2[0], c2[1], line_color, width=1)
# Show room centers as dots
center_color = mcrfpy.Color(255, 200, 0, 200) # more visible
for cx, cy in self.room_centers:
for dx in range(-1, 2):
for dy in range(-1, 2):
px, py = cx + dx, cy + dy
map_w, map_h = self.MAP_SIZE
if 0 <= px < map_w and 0 <= py < map_h:
adj_layer.set((px, py), center_color)
print(f"Found {len(self.adjacencies)} adjacencies")
def _rooms_adjacent(self, r1, r2) -> bool:
"""Check if two rooms are adjacent."""
x1, y1, w1, h1 = r1
x2, y2, w2, h2 = r2
# Horizontal adjacency (side by side)
h_gap = max(x1, x2) - min(x1 + w1, x2 + w2)
v_overlap = min(y1 + h1, y2 + h2) - max(y1, y2)
if h_gap <= 4 and v_overlap > 2:
return True
# Vertical adjacency (stacked)
v_gap = max(y1, y2) - min(y1 + h1, y2 + h2)
h_overlap = min(x1 + w1, x2 + w2) - max(x1, x2)
if v_gap <= 4 and h_overlap > 2:
return True
return False
def step_dig_corridors(self):
"""Step 6: Connect adjacent rooms with corridors."""
corridor_width = self.get_param("corridor_width")
corridors_layer = self.get_layer("corridors")
corridors_layer.fill(mcrfpy.Color(30, 28, 26, 128)) # alpha=128 for overlay
corridor_color = mcrfpy.Color(90, 85, 75, 128) # alpha=128 for overlay
for i, j in self.adjacencies:
c1 = self.room_centers[i]
c2 = self.room_centers[j]
# L-shaped corridor (horizontal then vertical)
mid_x = c1[0]
mid_y = c2[1]
# Horizontal segment
self._draw_line(corridors_layer, c1[0], c1[1], mid_x, mid_y,
corridor_color, width=corridor_width)
# Vertical segment
self._draw_line(corridors_layer, mid_x, mid_y, c2[0], c2[1],
corridor_color, width=corridor_width)
# Store in heightmap
map_w, map_h = self.MAP_SIZE
# Mark corridor cells
self._mark_line(c1[0], c1[1], mid_x, mid_y, corridor_width)
self._mark_line(mid_x, mid_y, c2[0], c2[1], corridor_width)
# Update final to show rooms + corridors
final = self.get_layer("final")
rooms_layer = self.get_layer("rooms")
map_w, map_h = self.MAP_SIZE
for y in range(map_h):
for x in range(map_w):
room_c = rooms_layer.at(x, y)
corr_c = corridors_layer.at(x, y)
# Prioritize rooms, then corridors, then background
if room_c.r > 50 or room_c.g > 50 or room_c.b > 50:
final.set((x, y), room_c)
elif corr_c.r > 50 or corr_c.g > 50 or corr_c.b > 50:
final.set((x, y), corr_c)
else:
final.set((x, y), mcrfpy.Color(30, 28, 26))
def _mark_line(self, x0, y0, x1, y1, width):
"""Mark corridor cells in heightmap."""
map_w, map_h = self.MAP_SIZE
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
for wo in range(-(width // 2), width // 2 + 1):
for ho in range(-(width // 2), width // 2 + 1):
px, py = x0 + wo, y0 + ho
if 0 <= px < map_w and 0 <= py < map_h:
self.hmap_corridors[px, py] = 1.0
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
def step_composite(self):
"""Step 7: Create final composite dungeon."""
final = self.get_layer("final")
map_w, map_h = self.MAP_SIZE
wall_color = mcrfpy.Color(40, 38, 35)
floor_color = mcrfpy.Color(140, 130, 115)
for y in range(map_h):
for x in range(map_w):
is_room = self.hmap_rooms[x, y] > 0.5
is_corridor = self.hmap_corridors[x, y] > 0.5
if is_room or is_corridor:
final.set((x, y), floor_color)
else:
final.set((x, y), wall_color)
def main():
"""Run the dungeon demo standalone."""
demo = DungeonDemo()
demo.activate()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,311 @@
"""Terrain Generation Demo - Multi-layer Elevation
Demonstrates terrain generation with:
1. Generate base elevation with simplex FBM
2. Normalize to 0-1 range
3. Apply water level (flatten below threshold)
4. Add mountain enhancement (boost peaks)
5. Optional erosion simulation
6. Apply terrain color ranges (biomes)
"""
import mcrfpy
from typing import List
from ..core.demo_base import ProcgenDemoBase, StepDef, LayerDef
from ..core.parameter import Parameter
class TerrainDemo(ProcgenDemoBase):
"""Interactive multi-layer terrain generation demo."""
name = "Terrain"
description = "Multi-layer elevation with noise and biome coloring"
MAP_SIZE = (256, 256)
# Terrain color ranges (elevation -> color gradient)
TERRAIN_COLORS = [
(0.00, 0.15, (30, 50, 120), (50, 80, 150)), # Deep water -> Shallow water
(0.15, 0.22, (50, 80, 150), (180, 170, 130)), # Shallow water -> Beach
(0.22, 0.35, (180, 170, 130), (80, 140, 60)), # Beach -> Grass low
(0.35, 0.55, (80, 140, 60), (50, 110, 40)), # Grass low -> Grass high
(0.55, 0.70, (50, 110, 40), (100, 90, 70)), # Grass high -> Rock low
(0.70, 0.85, (100, 90, 70), (140, 130, 120)), # Rock low -> Rock high
(0.85, 1.00, (140, 130, 120), (220, 220, 225)), # Rock high -> Snow
]
def define_steps(self) -> List[StepDef]:
"""Define the generation steps."""
return [
StepDef("Generate base elevation", self.step_base_elevation,
"Create initial terrain using simplex FBM noise"),
StepDef("Normalize heights", self.step_normalize,
"Normalize elevation values to 0-1 range"),
StepDef("Apply water level", self.step_water_level,
"Flatten terrain below water threshold"),
StepDef("Enhance mountains", self.step_mountains,
"Boost high elevation areas for dramatic peaks"),
StepDef("Apply erosion", self.step_erosion,
"Smooth terrain with erosion simulation"),
StepDef("Color biomes", self.step_biomes,
"Apply biome colors based on elevation"),
]
def define_parameters(self) -> List[Parameter]:
"""Define configurable parameters."""
return [
Parameter(
name="seed",
display="Seed",
type="int",
default=42,
min_val=0,
max_val=99999,
step=1,
affects_step=0,
description="Noise seed"
),
Parameter(
name="octaves",
display="Octaves",
type="int",
default=6,
min_val=1,
max_val=8,
step=1,
affects_step=0,
description="FBM detail octaves"
),
Parameter(
name="world_size",
display="Scale",
type="float",
default=8.0,
min_val=2.0,
max_val=20.0,
step=1.0,
affects_step=0,
description="Noise scale (larger = more zoomed out)"
),
Parameter(
name="water_level",
display="Water Level",
type="float",
default=0.20,
min_val=0.0,
max_val=0.40,
step=0.02,
affects_step=2,
description="Sea level threshold"
),
Parameter(
name="mountain_boost",
display="Mt. Boost",
type="float",
default=0.25,
min_val=0.0,
max_val=0.50,
step=0.05,
affects_step=3,
description="Mountain height enhancement"
),
Parameter(
name="erosion_passes",
display="Erosion",
type="int",
default=2,
min_val=0,
max_val=5,
step=1,
affects_step=4,
description="Erosion smoothing passes"
),
]
def define_layers(self) -> List[LayerDef]:
"""Define visualization layers."""
return [
LayerDef("colored", "Colored Terrain", "color", z_index=-1, visible=True,
description="Final terrain with biome colors"),
LayerDef("elevation", "Elevation", "color", z_index=0, visible=False,
description="Grayscale height values"),
LayerDef("water_mask", "Water Mask", "color", z_index=1, visible=False,
description="Binary water regions"),
]
def __init__(self):
"""Initialize terrain demo."""
super().__init__()
# Create working heightmaps
self.hmap_elevation = self.create_heightmap("elevation", 0.0)
self.hmap_water = self.create_heightmap("water", 0.0)
# Noise source
self.noise = None
def _apply_grayscale(self, layer, hmap, alpha=255):
"""Apply grayscale visualization to layer."""
w, h = self.MAP_SIZE
for y in range(h):
for x in range(w):
val = hmap[x, y]
v = int(max(0, min(255, val * 255)))
layer.set((x, y), mcrfpy.Color(v, v, v, alpha))
def _apply_terrain_colors(self, layer, hmap, alpha=255):
"""Apply terrain biome colors based on elevation."""
w, h = self.MAP_SIZE
for y in range(h):
for x in range(w):
val = hmap[x, y]
color = self._elevation_to_color(val, alpha)
layer.set((x, y), color)
def _elevation_to_color(self, val, alpha=255):
"""Convert elevation value to terrain color."""
for low, high, c1, c2 in self.TERRAIN_COLORS:
if low <= val <= high:
# Interpolate between c1 and c2
t = (val - low) / (high - low) if high > low else 0
r = int(c1[0] + t * (c2[0] - c1[0]))
g = int(c1[1] + t * (c2[1] - c1[1]))
b = int(c1[2] + t * (c2[2] - c1[2]))
return mcrfpy.Color(r, g, b, alpha)
# Default for out of range
return mcrfpy.Color(128, 128, 128)
# === Step Implementations ===
def step_base_elevation(self):
"""Step 1: Generate base elevation with FBM noise."""
seed = self.get_param("seed")
octaves = self.get_param("octaves")
world_size = self.get_param("world_size")
# Create noise source
self.noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm='simplex',
seed=seed
)
# Fill with FBM noise
self.hmap_elevation.fill(0.0)
self.hmap_elevation.add_noise(
self.noise,
world_size=(world_size, world_size),
mode='fbm',
octaves=octaves
)
# Show raw noise (elevation layer alpha=128 for overlay)
elevation_layer = self.get_layer("elevation")
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
# Also on colored layer (full opacity for final)
colored_layer = self.get_layer("colored")
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
def step_normalize(self):
"""Step 2: Normalize elevation to 0-1 range."""
self.hmap_elevation.normalize(0.0, 1.0)
# Update visualization
elevation_layer = self.get_layer("elevation")
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
colored_layer = self.get_layer("colored")
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
def step_water_level(self):
"""Step 3: Flatten terrain below water level."""
water_level = self.get_param("water_level")
w, h = self.MAP_SIZE
# Create water mask
self.hmap_water.fill(0.0)
for y in range(h):
for x in range(w):
val = self.hmap_elevation[x, y]
if val < water_level:
# Flatten to water level
self.hmap_elevation[x, y] = water_level
self.hmap_water[x, y] = 1.0
# Update water mask layer (alpha=128 for overlay)
water_layer = self.get_layer("water_mask")
for y in range(h):
for x in range(w):
if self.hmap_water[x, y] > 0.5:
water_layer.set((x, y), mcrfpy.Color(80, 120, 200, 128))
else:
water_layer.set((x, y), mcrfpy.Color(30, 30, 35, 128))
# Update other layers
elevation_layer = self.get_layer("elevation")
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
colored_layer = self.get_layer("colored")
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
def step_mountains(self):
"""Step 4: Enhance mountain peaks."""
mountain_boost = self.get_param("mountain_boost")
w, h = self.MAP_SIZE
if mountain_boost <= 0:
return # Skip if no boost
for y in range(h):
for x in range(w):
val = self.hmap_elevation[x, y]
# Boost high elevations more than low ones
# Using a power curve
if val > 0.5:
boost = (val - 0.5) * 2 # 0 to 1 for upper half
boost = boost * boost * mountain_boost # Squared for sharper peaks
self.hmap_elevation[x, y] = min(1.0, val + boost)
# Re-normalize to ensure 0-1 range
self.hmap_elevation.normalize(0.0, 1.0)
# Update visualization
elevation_layer = self.get_layer("elevation")
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
colored_layer = self.get_layer("colored")
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
def step_erosion(self):
"""Step 5: Apply erosion/smoothing."""
erosion_passes = self.get_param("erosion_passes")
if erosion_passes <= 0:
return # Skip if no erosion
for _ in range(erosion_passes):
self.hmap_elevation.smooth(iterations=1)
# Update visualization
elevation_layer = self.get_layer("elevation")
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
colored_layer = self.get_layer("colored")
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
def step_biomes(self):
"""Step 6: Apply biome colors based on elevation."""
colored_layer = self.get_layer("colored")
self._apply_terrain_colors(colored_layer, self.hmap_elevation, alpha=255)
def main():
"""Run the terrain demo standalone."""
demo = TerrainDemo()
demo.activate()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,509 @@
"""Town Generation Demo - Voronoi Districts + Bezier Roads
Demonstrates town generation with:
1. Generate base terrain elevation
2. Add Voronoi districts using HeightMap.add_voronoi()
3. Find district centers
4. Connect centers with roads using HeightMap.dig_bezier()
5. Place building footprints in districts
6. Composite: terrain + roads + buildings
"""
import mcrfpy
import random
from typing import List, Tuple
from ..core.demo_base import ProcgenDemoBase, StepDef, LayerDef
from ..core.parameter import Parameter
class TownDemo(ProcgenDemoBase):
"""Interactive Voronoi town generation demo."""
name = "Town"
description = "Voronoi districts with Bezier roads and building placement"
MAP_SIZE = (128, 96) # Smaller for clearer visualization
def define_steps(self) -> List[StepDef]:
"""Define the generation steps."""
return [
StepDef("Generate terrain", self.step_terrain,
"Create base terrain elevation"),
StepDef("Create districts", self.step_districts,
"Add Voronoi districts for zoning"),
StepDef("Find centers", self.step_find_centers,
"Locate district center points"),
StepDef("Build roads", self.step_roads,
"Connect districts with Bezier roads"),
StepDef("Place buildings", self.step_buildings,
"Add building footprints in districts"),
StepDef("Composite", self.step_composite,
"Combine all layers for final town"),
]
def define_parameters(self) -> List[Parameter]:
"""Define configurable parameters."""
return [
Parameter(
name="seed",
display="Seed",
type="int",
default=42,
min_val=0,
max_val=99999,
step=1,
affects_step=0,
description="Random seed for all generation"
),
Parameter(
name="num_districts",
display="Districts",
type="int",
default=12,
min_val=5,
max_val=25,
step=1,
affects_step=1,
description="Number of Voronoi districts"
),
Parameter(
name="road_width",
display="Road Width",
type="float",
default=2.0,
min_val=1.0,
max_val=4.0,
step=0.5,
affects_step=3,
description="Bezier road thickness"
),
Parameter(
name="building_density",
display="Building %",
type="float",
default=0.40,
min_val=0.20,
max_val=0.70,
step=0.05,
affects_step=4,
description="Building coverage density"
),
Parameter(
name="building_min",
display="Min Building",
type="int",
default=3,
min_val=2,
max_val=5,
step=1,
affects_step=4,
description="Minimum building size"
),
Parameter(
name="building_max",
display="Max Building",
type="int",
default=6,
min_val=4,
max_val=10,
step=1,
affects_step=4,
description="Maximum building size"
),
]
def define_layers(self) -> List[LayerDef]:
"""Define visualization layers."""
return [
LayerDef("final", "Final Town", "color", z_index=-1, visible=True,
description="Complete town composite"),
LayerDef("districts", "Districts", "color", z_index=0, visible=False,
description="Voronoi district regions"),
LayerDef("roads", "Roads", "color", z_index=1, visible=False,
description="Road network"),
LayerDef("buildings", "Buildings", "color", z_index=2, visible=False,
description="Building footprints"),
LayerDef("control_pts", "Control Points", "color", z_index=3, visible=False,
description="Bezier control points (educational)"),
]
def __init__(self):
"""Initialize town demo."""
super().__init__()
# Working heightmaps
self.hmap_terrain = self.create_heightmap("terrain", 0.0)
self.hmap_districts = self.create_heightmap("districts", 0.0)
self.hmap_roads = self.create_heightmap("roads", 0.0)
self.hmap_buildings = self.create_heightmap("buildings", 0.0)
# District data
self.district_points = [] # Voronoi seed points
self.district_centers = [] # Calculated centroids
self.connections = [] # List of (idx1, idx2) for roads
# Random state
self.rng = None
def _init_random(self):
"""Initialize random generator with seed."""
seed = self.get_param("seed")
self.rng = random.Random(seed)
def _get_district_color(self, district_id: int) -> Tuple[int, int, int]:
"""Get a color for a district ID."""
colors = [
(180, 160, 120), # Tan
(160, 180, 130), # Sage
(170, 150, 140), # Mauve
(150, 170, 160), # Seafoam
(175, 165, 125), # Sand
(165, 175, 135), # Moss
(155, 155, 155), # Gray
(180, 150, 130), # Terracotta
(140, 170, 170), # Teal
(170, 160, 150), # Warm gray
]
return colors[district_id % len(colors)]
# === Step Implementations ===
def step_terrain(self):
"""Step 1: Generate base terrain."""
self._init_random()
seed = self.get_param("seed")
# Create subtle terrain noise
noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm='simplex',
seed=seed
)
self.hmap_terrain.fill(0.0)
self.hmap_terrain.add_noise(
noise,
world_size=(15, 15),
mode='fbm',
octaves=4
)
self.hmap_terrain.normalize(0.3, 0.7) # Keep in mid range
# Visualize as subtle green-brown gradient
final = self.get_layer("final")
w, h = self.MAP_SIZE
for y in range(h):
for x in range(w):
val = self.hmap_terrain[x, y]
# Grass color range
r = int(80 + val * 40)
g = int(120 + val * 30)
b = int(60 + val * 20)
final.set((x, y), mcrfpy.Color(r, g, b))
def step_districts(self):
"""Step 2: Create Voronoi districts."""
num_districts = self.get_param("num_districts")
w, h = self.MAP_SIZE
# Generate random points for Voronoi seeds
margin = 10
self.district_points = []
for i in range(num_districts):
x = self.rng.randint(margin, w - margin)
y = self.rng.randint(margin, h - margin)
self.district_points.append((x, y))
# Use add_voronoi to create district values
# Each cell gets the ID of its nearest point
self.hmap_districts.fill(0.0)
for y in range(h):
for x in range(w):
min_dist = float('inf')
nearest_id = 0
for i, (px, py) in enumerate(self.district_points):
dist = (x - px) ** 2 + (y - py) ** 2
if dist < min_dist:
min_dist = dist
nearest_id = i + 1 # 1-indexed to distinguish from 0
self.hmap_districts[x, y] = nearest_id
# Visualize districts (alpha=128 for overlay)
districts_layer = self.get_layer("districts")
for y in range(h):
for x in range(w):
district_id = int(self.hmap_districts[x, y])
if district_id > 0:
color = self._get_district_color(district_id - 1)
districts_layer.set((x, y), mcrfpy.Color(color[0], color[1], color[2], 128))
else:
districts_layer.set((x, y), mcrfpy.Color(50, 50, 50, 128))
# Also show on final
final = self.get_layer("final")
for y in range(h):
for x in range(w):
c = districts_layer.at(x, y)
final.set((x, y), c)
def step_find_centers(self):
"""Step 3: Find district center points."""
num_districts = self.get_param("num_districts")
w, h = self.MAP_SIZE
# Calculate centroid of each district
self.district_centers = []
for did in range(1, num_districts + 1):
sum_x, sum_y, count = 0, 0, 0
for y in range(h):
for x in range(w):
if int(self.hmap_districts[x, y]) == did:
sum_x += x
sum_y += y
count += 1
if count > 0:
cx = sum_x // count
cy = sum_y // count
self.district_centers.append((cx, cy))
else:
# Use the original point if district is empty
if did - 1 < len(self.district_points):
self.district_centers.append(self.district_points[did - 1])
# Build connections (minimum spanning tree-like)
self.connections = []
if len(self.district_centers) > 1:
# Simple approach: connect each district to its nearest neighbor
# that hasn't been connected yet (Prim's-like)
connected = {0} # Start with first district
while len(connected) < len(self.district_centers):
best_dist = float('inf')
best_pair = None
for i in connected:
for j in range(len(self.district_centers)):
if j in connected:
continue
ci = self.district_centers[i]
cj = self.district_centers[j]
dist = (ci[0] - cj[0]) ** 2 + (ci[1] - cj[1]) ** 2
if dist < best_dist:
best_dist = dist
best_pair = (i, j)
if best_pair:
self.connections.append(best_pair)
connected.add(best_pair[1])
# Add a few extra connections for redundancy
for _ in range(min(3, len(self.district_centers) // 4)):
i = self.rng.randint(0, len(self.district_centers) - 1)
j = self.rng.randint(0, len(self.district_centers) - 1)
if i != j and (i, j) not in self.connections and (j, i) not in self.connections:
self.connections.append((i, j))
# Visualize centers and connections (alpha=128 for overlay)
control_layer = self.get_layer("control_pts")
control_layer.fill(mcrfpy.Color(30, 28, 26, 128))
# Draw center points
for cx, cy in self.district_centers:
for dx in range(-2, 3):
for dy in range(-2, 3):
px, py = cx + dx, cy + dy
if 0 <= px < w and 0 <= py < h:
control_layer.set((px, py), mcrfpy.Color(255, 200, 0, 200))
# Draw connection lines
for i, j in self.connections:
c1 = self.district_centers[i]
c2 = self.district_centers[j]
self._draw_line(control_layer, c1[0], c1[1], c2[0], c2[1],
mcrfpy.Color(200, 100, 100, 160), 1)
def _draw_line(self, layer, x0, y0, x1, y1, color, width):
"""Draw a line on a layer."""
w, h = self.MAP_SIZE
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
for wo in range(-(width // 2), width // 2 + 1):
for ho in range(-(width // 2), width // 2 + 1):
px, py = x0 + wo, y0 + ho
if 0 <= px < w and 0 <= py < h:
layer.set((px, py), color)
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
def step_roads(self):
"""Step 4: Build roads between districts."""
road_width = self.get_param("road_width")
w, h = self.MAP_SIZE
self.hmap_roads.fill(0.0)
roads_layer = self.get_layer("roads")
roads_layer.fill(mcrfpy.Color(30, 28, 26, 128)) # alpha=128 for overlay
road_color = mcrfpy.Color(80, 75, 65, 160) # alpha=160 for better visibility
for i, j in self.connections:
c1 = self.district_centers[i]
c2 = self.district_centers[j]
# Create bezier-like curve by adding a control point
mid_x = (c1[0] + c2[0]) // 2
mid_y = (c1[1] + c2[1]) // 2
# Offset the midpoint slightly for curve
offset_x = (c2[1] - c1[1]) // 8 # Perpendicular offset
offset_y = -(c2[0] - c1[0]) // 8
ctrl_x = mid_x + offset_x
ctrl_y = mid_y + offset_y
# Draw quadratic bezier approximation
self._draw_bezier(roads_layer, c1, (ctrl_x, ctrl_y), c2,
road_color, int(road_width))
# Also mark in heightmap
self._mark_bezier(c1, (ctrl_x, ctrl_y), c2, int(road_width))
# Update final with roads
final = self.get_layer("final")
districts_layer = self.get_layer("districts")
for y in range(h):
for x in range(w):
if self.hmap_roads[x, y] > 0.5:
final.set((x, y), road_color)
else:
c = districts_layer.at(x, y)
final.set((x, y), c)
def _draw_bezier(self, layer, p0, p1, p2, color, width):
"""Draw a quadratic bezier curve."""
w, h = self.MAP_SIZE
# Approximate with line segments
steps = 20
prev = None
for t in range(steps + 1):
t = t / steps
# Quadratic bezier formula
x = int((1-t)**2 * p0[0] + 2*(1-t)*t * p1[0] + t**2 * p2[0])
y = int((1-t)**2 * p0[1] + 2*(1-t)*t * p1[1] + t**2 * p2[1])
if prev:
self._draw_line(layer, prev[0], prev[1], x, y, color, width)
prev = (x, y)
def _mark_bezier(self, p0, p1, p2, width):
"""Mark bezier curve in roads heightmap."""
w, h = self.MAP_SIZE
steps = 20
for t in range(steps + 1):
t = t / steps
x = int((1-t)**2 * p0[0] + 2*(1-t)*t * p1[0] + t**2 * p2[0])
y = int((1-t)**2 * p0[1] + 2*(1-t)*t * p1[1] + t**2 * p2[1])
for wo in range(-(width // 2), width // 2 + 1):
for ho in range(-(width // 2), width // 2 + 1):
px, py = x + wo, y + ho
if 0 <= px < w and 0 <= py < h:
self.hmap_roads[px, py] = 1.0
def step_buildings(self):
"""Step 5: Place building footprints."""
density = self.get_param("building_density")
min_size = self.get_param("building_min")
max_size = self.get_param("building_max")
w, h = self.MAP_SIZE
self.hmap_buildings.fill(0.0)
buildings_layer = self.get_layer("buildings")
buildings_layer.fill(mcrfpy.Color(30, 28, 26, 128)) # alpha=128 for overlay
# Building colors (alpha=160 for better visibility)
building_colors = [
mcrfpy.Color(140, 120, 100, 160),
mcrfpy.Color(130, 130, 120, 160),
mcrfpy.Color(150, 130, 110, 160),
mcrfpy.Color(120, 120, 130, 160),
]
# Attempt to place buildings
attempts = int(w * h * density * 0.1)
for _ in range(attempts):
# Random position
bx = self.rng.randint(5, w - max_size - 5)
by = self.rng.randint(5, h - max_size - 5)
bw = self.rng.randint(min_size, max_size)
bh = self.rng.randint(min_size, max_size)
# Check if location is valid (not on road, not overlapping)
valid = True
for py in range(by - 1, by + bh + 1):
for px in range(bx - 1, bx + bw + 1):
if 0 <= px < w and 0 <= py < h:
if self.hmap_roads[px, py] > 0.5:
valid = False
break
if self.hmap_buildings[px, py] > 0.5:
valid = False
break
if not valid:
break
if not valid:
continue
# Place building
color = self.rng.choice(building_colors)
for py in range(by, by + bh):
for px in range(bx, bx + bw):
if 0 <= px < w and 0 <= py < h:
self.hmap_buildings[px, py] = 1.0
buildings_layer.set((px, py), color)
def step_composite(self):
"""Step 6: Create final composite."""
final = self.get_layer("final")
districts_layer = self.get_layer("districts")
buildings_layer = self.get_layer("buildings")
w, h = self.MAP_SIZE
road_color = mcrfpy.Color(80, 75, 65)
for y in range(h):
for x in range(w):
# Priority: buildings > roads > districts
if self.hmap_buildings[x, y] > 0.5:
c = buildings_layer.at(x, y)
final.set((x, y), c)
elif self.hmap_roads[x, y] > 0.5:
final.set((x, y), road_color)
else:
c = districts_layer.at(x, y)
final.set((x, y), c)
def main():
"""Run the town demo standalone."""
demo = TownDemo()
demo.activate()
if __name__ == "__main__":
main()