Cookbook: draft docs

This commit is contained in:
John McCardle 2026-01-13 19:42:37 -05:00
commit 73230989ad
42 changed files with 3352 additions and 0 deletions

View file

@ -0,0 +1,89 @@
"""HeightMap Hills and Craters Demo
Demonstrates: add_hill, dig_hill
Creates volcanic terrain with mountains and craters using ColorLayer visualization.
"""
import mcrfpy
from mcrfpy import automation
# Full screen grid: 60x48 tiles at 16x16 = 960x768
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def height_to_color(h):
"""Convert height value to terrain color."""
if h < 0.1:
return mcrfpy.Color(20, 40, int(80 + h * 400))
elif h < 0.3:
t = (h - 0.1) / 0.2
return mcrfpy.Color(int(40 + t * 30), int(60 + t * 40), 30)
elif h < 0.5:
t = (h - 0.3) / 0.2
return mcrfpy.Color(int(70 - t * 20), int(100 + t * 50), int(30 + t * 20))
elif h < 0.7:
t = (h - 0.5) / 0.2
return mcrfpy.Color(int(120 + t * 40), int(100 + t * 30), int(60 + t * 20))
elif h < 0.85:
t = (h - 0.7) / 0.15
return mcrfpy.Color(int(140 + t * 40), int(130 + t * 40), int(120 + t * 40))
else:
t = (h - 0.85) / 0.15
return mcrfpy.Color(int(180 + t * 75), int(180 + t * 75), int(180 + t * 75))
# Setup scene
scene = mcrfpy.Scene("hills_demo")
# Create grid with color layer
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
# Create heightmap
hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
# Add volcanic mountains - large hills
hmap.add_hill((15, 24), 18.0, 0.6) # Central volcano base
hmap.add_hill((15, 24), 10.0, 0.3) # Volcano peak
hmap.add_hill((45, 15), 12.0, 0.5) # Eastern mountain
hmap.add_hill((35, 38), 14.0, 0.45) # Southern mountain
hmap.add_hill((8, 10), 8.0, 0.35) # Small northern hill
# Create craters using dig_hill
hmap.dig_hill((15, 24), 5.0, 0.1) # Volcanic crater
hmap.dig_hill((45, 15), 4.0, 0.25) # Eastern crater
hmap.dig_hill((25, 30), 6.0, 0.05) # Impact crater (deep)
hmap.dig_hill((50, 40), 3.0, 0.2) # Small crater
# Add some smaller features for variety
for i in range(8):
x = 5 + (i * 7) % 55
y = 5 + (i * 11) % 40
hmap.add_hill((x, y), float(3 + (i % 4)), 0.15)
# Normalize to use full color range
hmap.normalize(0.0, 1.0)
# Apply heightmap to color layer
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
h = hmap.get((x, y))
color_layer.set((x, y), height_to_color(h))
# Title
title = mcrfpy.Caption(text="HeightMap: add_hill + dig_hill (volcanic terrain)", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
scene.activate()
# Take screenshot directly (works in headless mode)
automation.screenshot("procgen_01_heightmap_hills.png")
print("Screenshot saved: procgen_01_heightmap_hills.png")

View file

@ -0,0 +1,124 @@
"""HeightMap Noise Integration Demo
Demonstrates: add_noise, multiply_noise with NoiseSource
Shows terrain generation using different noise modes (flat, fbm, turbulence).
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def terrain_color(h):
"""Height-based terrain coloring."""
if h < 0.25:
# Water - deep to shallow blue
t = h / 0.25
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 60), int(120 + t * 80))
elif h < 0.35:
# Beach/sand
t = (h - 0.25) / 0.1
return mcrfpy.Color(int(180 + t * 40), int(160 + t * 30), int(100 + t * 20))
elif h < 0.6:
# Grass - varies with height
t = (h - 0.35) / 0.25
return mcrfpy.Color(int(50 + t * 30), int(120 + t * 40), int(40 + t * 20))
elif h < 0.75:
# Forest/hills
t = (h - 0.6) / 0.15
return mcrfpy.Color(int(40 - t * 10), int(80 + t * 20), int(30 + t * 10))
elif h < 0.88:
# Rock/mountain
t = (h - 0.75) / 0.13
return mcrfpy.Color(int(100 + t * 40), int(90 + t * 40), int(80 + t * 40))
else:
# Snow peaks
t = (h - 0.88) / 0.12
return mcrfpy.Color(int(200 + t * 55), int(200 + t * 55), int(210 + t * 45))
def apply_to_layer(hmap, layer):
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
h = hmap.get((x, y))
layer.set(x, y, terrain_color(h))
def run_demo(runtime):
# Create three panels showing different noise modes
panel_width = GRID_WIDTH // 3
right_panel_width = GRID_WIDTH - 2 * panel_width # Handle non-divisible widths
# Create noise source with consistent seed
noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm='simplex',
hurst=0.5,
lacunarity=2.0,
seed=42
)
# Left panel: Flat noise (single octave, raw)
left_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
left_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='flat', octaves=1)
left_hmap.normalize(0.0, 1.0)
# Middle panel: FBM noise (fractal brownian motion - natural terrain)
mid_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
mid_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='fbm', octaves=6)
mid_hmap.normalize(0.0, 1.0)
# Right panel: Turbulence (absolute value - clouds, marble)
right_hmap = mcrfpy.HeightMap((right_panel_width, GRID_HEIGHT), fill=0.0)
right_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='turbulence', octaves=6)
right_hmap.normalize(0.0, 1.0)
# Apply to color layer with panel divisions
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if x < panel_width:
h = left_hmap.get((x, y))
elif x < panel_width * 2:
h = mid_hmap.get((x - panel_width, y))
else:
h = right_hmap.get((x - panel_width * 2, y))
color_layer.set(((x, y)), terrain_color(h))
# Add divider lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_width - 1, y)), mcrfpy.Color(255, 255, 255, 100))
color_layer.set(((panel_width * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 100))
# Setup scene
scene = mcrfpy.Scene("noise_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
# Labels for each panel
labels = [
("FLAT (raw)", 10),
("FBM (terrain)", GRID_WIDTH * CELL_SIZE // 3 + 10),
("TURBULENCE (clouds)", GRID_WIDTH * CELL_SIZE * 2 // 3 + 10)
]
for text, x in labels:
label = mcrfpy.Caption(text=text, pos=(x, 10))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_02_heightmap_noise.png")
print("Screenshot saved: procgen_02_heightmap_noise.png")

View file

@ -0,0 +1,116 @@
"""HeightMap Combination Operations Demo
Demonstrates: add, subtract, multiply, min, max, lerp, copy_from
Shows how heightmaps can be combined for complex terrain effects.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def value_to_color(h):
"""Simple grayscale with color tinting for visibility."""
h = max(0.0, min(1.0, h))
# Blue-white-red gradient for clear visualization
if h < 0.5:
t = h / 0.5
return mcrfpy.Color(int(50 * t), int(100 * t), int(200 - 100 * t))
else:
t = (h - 0.5) / 0.5
return mcrfpy.Color(int(50 + 200 * t), int(100 + 100 * t), int(100 - 50 * t))
def run_demo(runtime):
# Create 6 panels (2 rows x 3 columns)
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Create two base heightmaps for operations
noise1 = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
noise2 = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
base1 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
base1.add_noise(noise1, world_size=(10, 10), mode='fbm', octaves=4)
base1.normalize(0.0, 1.0)
base2 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
base2.add_noise(noise2, world_size=(10, 10), mode='fbm', octaves=4)
base2.normalize(0.0, 1.0)
# Panel 1: ADD operation (combined terrain)
add_result = base1.copy_from(base1) # Actually need to create new
add_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
add_result.copy_from(base1).add(base2).normalize(0.0, 1.0)
# Panel 2: SUBTRACT operation (carving)
sub_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
sub_result.copy_from(base1).subtract(base2).normalize(0.0, 1.0)
# Panel 3: MULTIPLY operation (masking)
mul_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
mul_result.copy_from(base1).multiply(base2).normalize(0.0, 1.0)
# Panel 4: MIN operation (valleys)
min_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
min_result.copy_from(base1).min(base2)
# Panel 5: MAX operation (ridges)
max_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
max_result.copy_from(base1).max(base2)
# Panel 6: LERP operation (blending)
lerp_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
lerp_result.copy_from(base1).lerp(base2, 0.5)
# Apply panels to grid
panels = [
(add_result, 0, 0, "ADD"),
(sub_result, panel_w, 0, "SUBTRACT"),
(mul_result, panel_w * 2, 0, "MULTIPLY"),
(min_result, 0, panel_h, "MIN"),
(max_result, panel_w, panel_h, "MAX"),
(lerp_result, panel_w * 2, panel_h, "LERP(0.5)"),
]
for hmap, ox, oy, name in panels:
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), value_to_color(h))
# Add label
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Draw grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(255, 255, 255, 80))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 80))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(255, 255, 255, 80))
# Setup
scene = mcrfpy.Scene("operations_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_03_heightmap_operations.png")
print("Screenshot saved: procgen_03_heightmap_operations.png")

View file

@ -0,0 +1,116 @@
"""HeightMap Transform Operations Demo
Demonstrates: scale, clamp, normalize, smooth, kernel_transform
Shows value manipulation and convolution effects.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def value_to_color(h):
"""Grayscale with enhanced contrast."""
h = max(0.0, min(1.0, h))
v = int(h * 255)
return mcrfpy.Color(v, v, v)
def run_demo(runtime):
# Create 6 panels showing different transforms
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Source noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
# Create base terrain with features
base = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
base.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=4)
base.add_hill((panel_w // 2, panel_h // 2), 8, 0.5)
base.normalize(0.0, 1.0)
# Panel 1: Original
original = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
original.copy_from(base)
# Panel 2: SCALE (amplify contrast)
scaled = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
scaled.copy_from(base).add_constant(-0.5).scale(2.0).clamp(0.0, 1.0)
# Panel 3: CLAMP (plateau effect)
clamped = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
clamped.copy_from(base).clamp(0.3, 0.7).normalize(0.0, 1.0)
# Panel 4: SMOOTH (blur/average)
smoothed = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
smoothed.copy_from(base).smooth(3)
# Panel 5: SHARPEN kernel
sharpened = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
sharpened.copy_from(base)
sharpen_kernel = {
(0, -1): -1.0, (-1, 0): -1.0, (0, 0): 5.0, (1, 0): -1.0, (0, 1): -1.0
}
sharpened.kernel_transform(sharpen_kernel).clamp(0.0, 1.0)
# Panel 6: EDGE DETECTION kernel
edges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
edges.copy_from(base)
edge_kernel = {
(-1, -1): -1, (0, -1): -1, (1, -1): -1,
(-1, 0): -1, (0, 0): 8, (1, 0): -1,
(-1, 1): -1, (0, 1): -1, (1, 1): -1,
}
edges.kernel_transform(edge_kernel).normalize(0.0, 1.0)
# Apply to grid
panels = [
(original, 0, 0, "ORIGINAL"),
(scaled, panel_w, 0, "SCALE (contrast)"),
(clamped, panel_w * 2, 0, "CLAMP (plateau)"),
(smoothed, 0, panel_h, "SMOOTH (blur)"),
(sharpened, panel_w, panel_h, "SHARPEN kernel"),
(edges, panel_w * 2, panel_h, "EDGE DETECT"),
]
for hmap, ox, oy, name in panels:
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), value_to_color(h))
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 0)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
# Setup
scene = mcrfpy.Scene("transforms_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_04_heightmap_transforms.png")
print("Screenshot saved: procgen_04_heightmap_transforms.png")

View file

@ -0,0 +1,135 @@
"""HeightMap Erosion and Terrain Generation Demo
Demonstrates: rain_erosion, mid_point_displacement, smooth
Shows natural terrain formation through erosion simulation.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def terrain_color(h):
"""Natural terrain coloring."""
if h < 0.2:
# Deep water
t = h / 0.2
return mcrfpy.Color(int(20 + t * 30), int(40 + t * 40), int(100 + t * 55))
elif h < 0.3:
# Shallow water
t = (h - 0.2) / 0.1
return mcrfpy.Color(int(50 + t * 50), int(80 + t * 60), int(155 + t * 40))
elif h < 0.35:
# Beach
t = (h - 0.3) / 0.05
return mcrfpy.Color(int(194 - t * 30), int(178 - t * 30), int(128 - t * 20))
elif h < 0.55:
# Lowland grass
t = (h - 0.35) / 0.2
return mcrfpy.Color(int(80 + t * 20), int(140 - t * 30), int(60 + t * 10))
elif h < 0.7:
# Highland grass/forest
t = (h - 0.55) / 0.15
return mcrfpy.Color(int(50 + t * 30), int(100 + t * 10), int(40 + t * 20))
elif h < 0.85:
# Rock
t = (h - 0.7) / 0.15
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 30), int(85 + t * 35))
else:
# Snow
t = (h - 0.85) / 0.15
return mcrfpy.Color(int(180 + t * 75), int(185 + t * 70), int(190 + t * 65))
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Panel 1: Mid-point displacement (raw)
mpd_raw = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
mpd_raw.mid_point_displacement(roughness=0.6, seed=42)
mpd_raw.normalize(0.0, 1.0)
# Panel 2: Mid-point displacement + smoothing
mpd_smooth = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
mpd_smooth.mid_point_displacement(roughness=0.6, seed=42)
mpd_smooth.smooth(2)
mpd_smooth.normalize(0.0, 1.0)
# Panel 3: Mid-point + light erosion
mpd_light_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
mpd_light_erode.mid_point_displacement(roughness=0.6, seed=42)
mpd_light_erode.rain_erosion(drops=1000, erosion=0.05, sedimentation=0.03, seed=42)
mpd_light_erode.normalize(0.0, 1.0)
# Panel 4: Noise-based + moderate erosion
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
noise_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
noise_erode.add_noise(noise, world_size=(12, 12), mode='fbm', octaves=5)
noise_erode.add_hill((panel_w // 2, panel_h // 2), 10, 0.4)
noise_erode.rain_erosion(drops=3000, erosion=0.1, sedimentation=0.05, seed=42)
noise_erode.normalize(0.0, 1.0)
# Panel 5: Heavy erosion (river valleys)
heavy_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
heavy_erode.mid_point_displacement(roughness=0.7, seed=99)
heavy_erode.rain_erosion(drops=8000, erosion=0.15, sedimentation=0.02, seed=42)
heavy_erode.normalize(0.0, 1.0)
# Panel 6: Extreme erosion (canyon-like)
extreme_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
extreme_erode.mid_point_displacement(roughness=0.5, seed=77)
extreme_erode.rain_erosion(drops=15000, erosion=0.2, sedimentation=0.01, seed=42)
extreme_erode.smooth(1)
extreme_erode.normalize(0.0, 1.0)
# Apply to grid
panels = [
(mpd_raw, 0, 0, "MPD Raw"),
(mpd_smooth, panel_w, 0, "MPD + Smooth"),
(mpd_light_erode, panel_w * 2, 0, "Light Erosion"),
(noise_erode, 0, panel_h, "Noise + Erosion"),
(heavy_erode, panel_w, panel_h, "Heavy Erosion"),
(extreme_erode, panel_w * 2, panel_h, "Extreme Erosion"),
]
for hmap, ox, oy, name in panels:
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), terrain_color(h))
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
# Setup
scene = mcrfpy.Scene("erosion_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_05_heightmap_erosion.png")
print("Screenshot saved: procgen_05_heightmap_erosion.png")

View file

@ -0,0 +1,133 @@
"""HeightMap Voronoi Demo
Demonstrates: add_voronoi with different coefficients
Shows cell-based patterns useful for biomes, regions, and organic structures.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def biome_color(h):
"""Color cells as distinct biomes."""
# Use value ranges to create distinct regions
h = max(0.0, min(1.0, h))
if h < 0.15:
return mcrfpy.Color(30, 60, 120) # Deep water
elif h < 0.25:
return mcrfpy.Color(50, 100, 180) # Shallow water
elif h < 0.35:
return mcrfpy.Color(194, 178, 128) # Beach/desert
elif h < 0.5:
return mcrfpy.Color(80, 160, 60) # Grassland
elif h < 0.65:
return mcrfpy.Color(40, 100, 40) # Forest
elif h < 0.8:
return mcrfpy.Color(100, 80, 60) # Hills
elif h < 0.9:
return mcrfpy.Color(130, 130, 130) # Mountains
else:
return mcrfpy.Color(240, 240, 250) # Snow
def cell_edges_color(h):
"""Highlight cell boundaries."""
h = max(0.0, min(1.0, h))
if h < 0.3:
return mcrfpy.Color(40, 40, 60)
elif h < 0.6:
return mcrfpy.Color(80, 80, 100)
else:
return mcrfpy.Color(200, 200, 220)
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Panel 1: Standard Voronoi (cell centers high)
# coefficients (1, 0) = distance to nearest point
v_standard = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_standard.add_voronoi(num_points=15, coefficients=(1.0, 0.0), seed=42)
v_standard.normalize(0.0, 1.0)
# Panel 2: Inverted (cell centers low, edges high)
# coefficients (-1, 0) = inverted distance
v_inverted = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_inverted.add_voronoi(num_points=15, coefficients=(-1.0, 0.0), seed=42)
v_inverted.normalize(0.0, 1.0)
# Panel 3: Cell difference (creates ridges)
# coefficients (1, -1) = distance to nearest - distance to second nearest
v_ridges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_ridges.add_voronoi(num_points=15, coefficients=(1.0, -1.0), seed=42)
v_ridges.normalize(0.0, 1.0)
# Panel 4: Few large cells (biome-scale)
v_biomes = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_biomes.add_voronoi(num_points=6, coefficients=(1.0, -0.3), seed=99)
v_biomes.normalize(0.0, 1.0)
# Panel 5: Many small cells (texture-scale)
v_texture = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_texture.add_voronoi(num_points=50, coefficients=(1.0, -0.5), seed=77)
v_texture.normalize(0.0, 1.0)
# Panel 6: Voronoi + noise blend (natural regions)
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
v_natural = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_natural.add_voronoi(num_points=12, coefficients=(0.8, -0.4), seed=42)
v_natural.add_noise(noise, world_size=(15, 15), mode='fbm', octaves=3, scale=0.3)
v_natural.normalize(0.0, 1.0)
# Apply to grid
panels = [
(v_standard, 0, 0, "Standard (1,0)", biome_color),
(v_inverted, panel_w, 0, "Inverted (-1,0)", biome_color),
(v_ridges, panel_w * 2, 0, "Ridges (1,-1)", cell_edges_color),
(v_biomes, 0, panel_h, "Biomes (6 pts)", biome_color),
(v_texture, panel_w, panel_h, "Texture (50 pts)", cell_edges_color),
(v_natural, panel_w * 2, panel_h, "Voronoi + Noise", biome_color),
]
for hmap, ox, oy, name, color_func in panels:
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), color_func(h))
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
# Setup
scene = mcrfpy.Scene("voronoi_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_06_heightmap_voronoi.png")
print("Screenshot saved: procgen_06_heightmap_voronoi.png")

View file

@ -0,0 +1,158 @@
"""HeightMap Bezier Curves Demo
Demonstrates: dig_bezier for rivers, roads, and paths
Shows path carving with variable width and depth.
"""
import mcrfpy
from mcrfpy import automation
import math
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def terrain_with_water(h):
"""Terrain coloring with water in low areas."""
if h < 0.15:
# Water (carved paths)
t = h / 0.15
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 50), int(140 + t * 40))
elif h < 0.25:
# Shore/wet ground
t = (h - 0.15) / 0.1
return mcrfpy.Color(int(80 + t * 40), int(100 + t * 30), int(80 - t * 20))
elif h < 0.5:
# Lowland
t = (h - 0.25) / 0.25
return mcrfpy.Color(int(70 + t * 20), int(130 + t * 20), int(50 + t * 10))
elif h < 0.7:
# Highland
t = (h - 0.5) / 0.2
return mcrfpy.Color(int(60 + t * 30), int(110 - t * 20), int(45 + t * 15))
elif h < 0.85:
# Hills
t = (h - 0.7) / 0.15
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 25), int(70 + t * 30))
else:
# Peaks
t = (h - 0.85) / 0.15
return mcrfpy.Color(int(150 + t * 60), int(150 + t * 60), int(155 + t * 60))
def run_demo(runtime):
panel_w = GRID_WIDTH // 2
panel_h = GRID_HEIGHT
# Left panel: River system
river_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
# Add terrain
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
river_map.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=4, scale=0.3)
river_map.add_hill((panel_w // 2, 5), 12, 0.3) # Mountain source
river_map.normalize(0.3, 0.9)
# Main river - wide, flowing from top to bottom
river_map.dig_bezier(
points=((panel_w // 2, 2), (panel_w // 4, 15), (panel_w * 3 // 4, 30), (panel_w // 2, panel_h - 3)),
start_radius=2, end_radius=5,
start_height=0.1, end_height=0.05
)
# Tributary from left
river_map.dig_bezier(
points=((3, 20), (10, 18), (15, 22), (panel_w // 3, 20)),
start_radius=1, end_radius=2,
start_height=0.12, end_height=0.1
)
# Tributary from right
river_map.dig_bezier(
points=((panel_w - 3, 15), (panel_w - 8, 20), (panel_w - 12, 18), (panel_w * 2 // 3, 25)),
start_radius=1, end_radius=2,
start_height=0.12, end_height=0.1
)
# Right panel: Road network
road_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
road_map.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3, scale=0.2)
road_map.normalize(0.35, 0.7)
# Main road - relatively straight
road_map.dig_bezier(
points=((5, panel_h // 2), (15, panel_h // 2 - 3), (panel_w - 15, panel_h // 2 + 3), (panel_w - 5, panel_h // 2)),
start_radius=2, end_radius=2,
start_height=0.25, end_height=0.25
)
# North-south crossing road
road_map.dig_bezier(
points=((panel_w // 2, 5), (panel_w // 2 + 5, 15), (panel_w // 2 - 5, 35), (panel_w // 2, panel_h - 5)),
start_radius=2, end_radius=2,
start_height=0.25, end_height=0.25
)
# Winding mountain path
road_map.dig_bezier(
points=((5, 8), (15, 5), (20, 15), (25, 10)),
start_radius=1, end_radius=1,
start_height=0.28, end_height=0.28
)
# Curved path to settlement
road_map.dig_bezier(
points=((panel_w - 5, panel_h - 8), (panel_w - 15, panel_h - 5), (panel_w - 10, panel_h - 15), (panel_w // 2 + 5, panel_h - 10)),
start_radius=1, end_radius=2,
start_height=0.27, end_height=0.26
)
# Apply to grid
for y in range(panel_h):
for x in range(panel_w):
# Left panel: rivers
h = river_map.get((x, y))
color_layer.set(((x, y)), terrain_with_water(h))
# Right panel: roads (use brown for roads)
h2 = road_map.get((x, y))
if h2 < 0.3:
# Road surface
t = h2 / 0.3
color = mcrfpy.Color(int(140 - t * 40), int(120 - t * 30), int(80 - t * 20))
else:
color = terrain_with_water(h2)
color_layer.set(((panel_w + x, y)), color)
# Divider
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
# Labels
labels = [("Rivers (dig_bezier)", 10, 10), ("Roads & Paths", panel_w * CELL_SIZE + 10, 10)]
for text, x, ypos in labels:
label = mcrfpy.Caption(text=text, pos=(x, ypos))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Setup
scene = mcrfpy.Scene("bezier_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_07_heightmap_bezier.png")
print("Screenshot saved: procgen_07_heightmap_bezier.png")

View file

@ -0,0 +1,148 @@
"""HeightMap Thresholds and ColorLayer Integration Demo
Demonstrates: threshold, threshold_binary, inverse, count_in_range
Also: ColorLayer.apply_ranges for multi-threshold coloring
Shows terrain classification and visualization techniques.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Create source terrain
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
source = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
source.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=5)
source.add_hill((panel_w // 2, panel_h // 2), 8, 0.3)
source.normalize(0.0, 1.0)
# Create derived heightmaps
water_mask = source.threshold((0.0, 0.3)) # Returns NEW heightmap with values only in range
land_binary = source.threshold_binary((0.3, 1.0), value=1.0) # Binary mask
inverted = source.inverse() # Inverted values
# Count cells in ranges for classification stats
water_count = source.count_in_range((0.0, 0.3))
land_count = source.count_in_range((0.3, 0.7))
mountain_count = source.count_in_range((0.7, 1.0))
# IMPORTANT: Render apply_ranges FIRST since it affects the whole layer
# Panel 6: Using ColorLayer.apply_ranges (bottom-right)
# Create a full-size heightmap and copy source data to correct position
panel6_hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=-1.0) # -1 won't match any range
for y in range(panel_h):
for x in range(panel_w):
h = source.get((x, y))
panel6_hmap.fill(h, pos=(panel_w * 2 + x, panel_h + y), size=(1, 1))
# apply_ranges colors cells based on height ranges
# Cells with -1.0 won't match any range and stay unchanged
color_layer.apply_ranges(panel6_hmap, [
((0.0, 0.2), (30, 80, 160)), # Deep water
((0.2, 0.3), ((60, 120, 180), (120, 160, 140))), # Gradient: shallow to shore
((0.3, 0.5), (80, 150, 60)), # Lowland
((0.5, 0.7), ((60, 120, 40), (100, 100, 80))), # Gradient: forest to hills
((0.7, 0.85), (130, 120, 110)), # Rock
((0.85, 1.0), ((180, 180, 190), (250, 250, 255))), # Gradient: rock to snow
])
# Now render the other 5 panels (they will overwrite only their regions)
# Panel 1 (top-left): Original grayscale
for y in range(panel_h):
for x in range(panel_w):
h = source.get((x, y))
v = int(h * 255)
color_layer.set(((x, y)), mcrfpy.Color(v, v, v))
# Panel 2 (top-middle): threshold() - shows only values in range 0.0-0.3
for y in range(panel_h):
for x in range(panel_w):
h = water_mask.get((x, y))
if h > 0:
# Values were preserved in 0.0-0.3 range
t = h / 0.3
color_layer.set(((panel_w + x, y)), mcrfpy.Color(
int(30 + t * 40), int(60 + t * 60), int(150 + t * 50)))
else:
# Outside threshold range - dark
color_layer.set(((panel_w + x, y)), mcrfpy.Color(20, 20, 30))
# Panel 3 (top-right): threshold_binary() - land mask
for y in range(panel_h):
for x in range(panel_w):
h = land_binary.get((x, y))
if h > 0:
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(80, 140, 60)) # Land
else:
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(40, 80, 150)) # Water
# Panel 4 (bottom-left): inverse()
for y in range(panel_h):
for x in range(panel_w):
h = inverted.get((x, y))
v = int(h * 255)
color_layer.set(((x, panel_h + y)), mcrfpy.Color(v, int(v * 0.8), int(v * 0.6)))
# Panel 5 (bottom-middle): Classification using count_in_range results
for y in range(panel_h):
for x in range(panel_w):
h = source.get((x, y))
if h < 0.3:
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(50, 100, 180)) # Water
elif h < 0.7:
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(70, 140, 50)) # Land
else:
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(140, 130, 120)) # Mountain
# Labels
labels = [
("Original (grayscale)", 5, 5),
("threshold(0-0.3)", panel_w * CELL_SIZE + 5, 5),
("threshold_binary(land)", panel_w * 2 * CELL_SIZE + 5, 5),
("inverse()", 5, panel_h * CELL_SIZE + 5),
(f"Classified (W:{water_count} L:{land_count} M:{mountain_count})", panel_w * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
("apply_ranges (biome)", panel_w * 2 * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
]
for text, x, y in labels:
label = mcrfpy.Caption(text=text, pos=(x, y))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid divider lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
# Setup
scene = mcrfpy.Scene("thresholds_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_08_heightmap_thresholds.png")
print("Screenshot saved: procgen_08_heightmap_thresholds.png")

View file

@ -0,0 +1,130 @@
"""BSP Dungeon Generation Demo
Demonstrates: BSP, split_recursive, leaves iteration, to_heightmap
Classic roguelike dungeon generation with rooms.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Create BSP tree covering the map
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
# Split recursively to create rooms
# depth=4 creates up to 16 rooms, min_size ensures rooms aren't too small
bsp.split_recursive(depth=4, min_size=(8, 6), max_ratio=1.5, seed=42)
# Convert to heightmap for visualization
# shrink=1 leaves 1-tile border for walls
rooms_hmap = bsp.to_heightmap(
size=(GRID_WIDTH, GRID_HEIGHT),
select='leaves',
shrink=1,
value=1.0
)
# Fill background (walls)
color_layer.fill(mcrfpy.Color(40, 35, 45))
# Draw rooms
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if rooms_hmap.get((x, y)) > 0:
color_layer.set(((x, y)), mcrfpy.Color(80, 75, 70))
# Add some visual variety to rooms
room_colors = [
mcrfpy.Color(85, 80, 75),
mcrfpy.Color(75, 70, 65),
mcrfpy.Color(90, 85, 80),
mcrfpy.Color(70, 65, 60),
]
for i, leaf in enumerate(bsp.leaves()):
pos = leaf.pos
size = leaf.size
color = room_colors[i % len(room_colors)]
# Fill room interior (with shrink)
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
color_layer.set(((x, y)), color)
# Mark room center
cx, cy = leaf.center()
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
color_layer.set(((cx, cy)), mcrfpy.Color(200, 180, 100))
# Simple corridor generation: connect adjacent rooms
# Using adjacency graph
adjacency = bsp.adjacency
connected = set()
for leaf_idx in range(len(bsp)):
leaf = bsp.get_leaf(leaf_idx)
cx1, cy1 = leaf.center()
for neighbor_idx in adjacency[leaf_idx]:
if (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)) in connected:
continue
connected.add((min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)))
neighbor = bsp.get_leaf(neighbor_idx)
cx2, cy2 = neighbor.center()
# Draw L-shaped corridor
# Horizontal first, then vertical
x1, x2 = min(cx1, cx2), max(cx1, cx2)
for x in range(x1, x2 + 1):
if 0 <= x < GRID_WIDTH and 0 <= cy1 < GRID_HEIGHT:
color_layer.set(((x, cy1)), mcrfpy.Color(100, 95, 90))
y1, y2 = min(cy1, cy2), max(cy1, cy2)
for y in range(y1, y2 + 1):
if 0 <= cx2 < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
color_layer.set(((cx2, y)), mcrfpy.Color(100, 95, 90))
# Draw outer border
for x in range(GRID_WIDTH):
color_layer.set(((x, 0)), mcrfpy.Color(60, 50, 70))
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(60, 50, 70))
for y in range(GRID_HEIGHT):
color_layer.set(((0, y)), mcrfpy.Color(60, 50, 70))
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(60, 50, 70))
# Stats
stats = mcrfpy.Caption(
text=f"BSP Dungeon: {len(bsp)} rooms, depth=4, seed=42",
pos=(10, 10)
)
stats.fill_color = mcrfpy.Color(255, 255, 255)
stats.outline = 1
stats.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(stats)
# Setup
scene = mcrfpy.Scene("bsp_dungeon_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_10_bsp_dungeon.png")
print("Screenshot saved: procgen_10_bsp_dungeon.png")

View file

@ -0,0 +1,178 @@
"""BSP Traversal Orders Demo
Demonstrates: traverse() with different Traversal orders
Shows how traversal order affects leaf enumeration.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
traversal_orders = [
(mcrfpy.Traversal.PRE_ORDER, "PRE_ORDER", "Root first, then children"),
(mcrfpy.Traversal.IN_ORDER, "IN_ORDER", "Left, node, right"),
(mcrfpy.Traversal.POST_ORDER, "POST_ORDER", "Children before parent"),
(mcrfpy.Traversal.LEVEL_ORDER, "LEVEL_ORDER", "Breadth-first by level"),
(mcrfpy.Traversal.INVERTED_LEVEL_ORDER, "INV_LEVEL", "Deepest levels first"),
]
panels = [
(0, 0), (panel_w, 0), (panel_w * 2, 0),
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
]
# Distinct color palette for 8+ leaves
leaf_colors = [
mcrfpy.Color(220, 60, 60), # Red
mcrfpy.Color(60, 180, 60), # Green
mcrfpy.Color(60, 100, 220), # Blue
mcrfpy.Color(220, 180, 40), # Yellow
mcrfpy.Color(180, 60, 180), # Magenta
mcrfpy.Color(60, 200, 200), # Cyan
mcrfpy.Color(220, 120, 60), # Orange
mcrfpy.Color(160, 100, 200), # Purple
mcrfpy.Color(100, 200, 120), # Mint
mcrfpy.Color(200, 100, 140), # Pink
]
for panel_idx, (order, name, desc) in enumerate(traversal_orders):
if panel_idx >= 6:
break
ox, oy = panels[panel_idx]
# Create BSP for this panel
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
# Fill panel background (dark gray = walls)
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
# Traverse and color ONLY LEAVES by their position in traversal
leaf_idx = 0
for node in bsp.traverse(order):
if not node.is_leaf:
continue # Skip branch nodes
color = leaf_colors[leaf_idx % len(leaf_colors)]
pos = node.pos
size = node.size
# Shrink by 1 to show walls between rooms
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
color_layer.set(((x, y)), color)
# Draw leaf index in center
cx, cy = node.center()
# Draw index as a darker spot
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
dark = mcrfpy.Color(color.r // 2, color.g // 2, color.b // 2)
color_layer.set(((cx, cy)), dark)
if cx + 1 < GRID_WIDTH:
color_layer.set(((cx + 1, cy)), dark)
leaf_idx += 1
# Add labels
label = mcrfpy.Caption(text=f"{name}", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
desc_label = mcrfpy.Caption(text=f"{desc} ({leaf_idx} leaves)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
desc_label.outline = 1
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(desc_label)
# Panel 6: Show tree depth levels (branch AND leaf nodes)
ox, oy = panels[5]
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
# Draw by level - deepest first so leaves are on top
level_colors = [
mcrfpy.Color(60, 40, 40), # Level 0 (root) - dark
mcrfpy.Color(80, 60, 50), # Level 1
mcrfpy.Color(100, 80, 60), # Level 2
mcrfpy.Color(140, 120, 80), # Level 3 (leaves usually)
]
# Use INVERTED_LEVEL_ORDER so leaves are drawn last
for node in bsp.traverse(mcrfpy.Traversal.INVERTED_LEVEL_ORDER):
level = node.level
color = level_colors[min(level, len(level_colors) - 1)]
# Make leaves brighter
if node.is_leaf:
color = mcrfpy.Color(
min(255, color.r + 80),
min(255, color.g + 80),
min(255, color.b + 60)
)
pos = node.pos
size = node.size
for y in range(pos[1], pos[1] + size[1]):
for x in range(pos[0], pos[0] + size[0]):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
# Draw border
if x == pos[0] or x == pos[0] + size[0] - 1 or \
y == pos[1] or y == pos[1] + size[1] - 1:
border = mcrfpy.Color(20, 20, 30)
color_layer.set(((x, y)), border)
else:
color_layer.set(((x, y)), color)
label = mcrfpy.Caption(text="BY LEVEL (depth)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
desc_label = mcrfpy.Caption(text="Darker=root, Bright=leaves", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
desc_label.outline = 1
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(desc_label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
# Setup
scene = mcrfpy.Scene("bsp_traversal_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_11_bsp_traversal.png")
print("Screenshot saved: procgen_11_bsp_traversal.png")

View file

@ -0,0 +1,160 @@
"""BSP Adjacency Graph Demo
Demonstrates: adjacency property, get_leaf, adjacent_tiles
Shows room connectivity for corridor generation.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Create dungeon BSP
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.4, seed=42)
# Fill with wall color
color_layer.fill(mcrfpy.Color(50, 45, 55))
# Generate distinct colors for each room
num_rooms = len(bsp)
room_colors = []
for i in range(num_rooms):
hue = (i * 137.5) % 360 # Golden angle for good distribution
# HSV to RGB (simplified, saturation=0.6, value=0.7)
h = hue / 60
c = 0.42 # 0.6 * 0.7
x = c * (1 - abs(h % 2 - 1))
m = 0.28 # 0.7 - c
if h < 1: r, g, b = c, x, 0
elif h < 2: r, g, b = x, c, 0
elif h < 3: r, g, b = 0, c, x
elif h < 4: r, g, b = 0, x, c
elif h < 5: r, g, b = x, 0, c
else: r, g, b = c, 0, x
room_colors.append(mcrfpy.Color(
int((r + m) * 255),
int((g + m) * 255),
int((b + m) * 255)
))
# Draw rooms with unique colors
for i, leaf in enumerate(bsp.leaves()):
pos = leaf.pos
size = leaf.size
color = room_colors[i]
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
color_layer.set(((x, y)), color)
# Room label
cx, cy = leaf.center()
label = mcrfpy.Caption(text=str(i), pos=(cx * CELL_SIZE - 4, cy * CELL_SIZE - 8))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Draw corridors using adjacency graph
adjacency = bsp.adjacency
connected = set()
corridor_color = mcrfpy.Color(100, 95, 90)
door_color = mcrfpy.Color(180, 140, 80)
for leaf_idx in range(num_rooms):
leaf = bsp.get_leaf(leaf_idx)
# Get adjacent_tiles for this leaf
adj_tiles = leaf.adjacent_tiles
for neighbor_idx in adjacency[leaf_idx]:
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
if pair in connected:
continue
connected.add(pair)
neighbor = bsp.get_leaf(neighbor_idx)
# Find shared wall tiles
if neighbor_idx in adj_tiles:
wall_tiles = adj_tiles[neighbor_idx]
if len(wall_tiles) > 0:
# Pick middle tile for door
mid_tile = wall_tiles[len(wall_tiles) // 2]
dx, dy = int(mid_tile.x), int(mid_tile.y)
# Draw door
color_layer.set(((dx, dy)), door_color)
# Simple corridor: connect room centers through door
cx1, cy1 = leaf.center()
cx2, cy2 = neighbor.center()
# Path from room 1 to door
for x in range(min(cx1, dx), max(cx1, dx) + 1):
color_layer.set(((x, cy1)), corridor_color)
for y in range(min(cy1, dy), max(cy1, dy) + 1):
color_layer.set(((dx, y)), corridor_color)
# Path from door to room 2
for x in range(min(dx, cx2), max(dx, cx2) + 1):
color_layer.set(((x, dy)), corridor_color)
for y in range(min(dy, cy2), max(dy, cy2) + 1):
color_layer.set(((cx2, y)), corridor_color)
else:
# Fallback: L-shaped corridor
cx1, cy1 = leaf.center()
cx2, cy2 = neighbor.center()
for x in range(min(cx1, cx2), max(cx1, cx2) + 1):
color_layer.set(((x, cy1)), corridor_color)
for y in range(min(cy1, cy2), max(cy1, cy2) + 1):
color_layer.set(((cx2, y)), corridor_color)
# Title and stats
title = mcrfpy.Caption(
text=f"BSP Adjacency: {num_rooms} rooms, {len(connected)} connections",
pos=(10, 10)
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Legend
legend = mcrfpy.Caption(
text="Numbers = room index, Gold = doors, Brown = corridors",
pos=(10, GRID_HEIGHT * CELL_SIZE - 25)
)
legend.fill_color = mcrfpy.Color(200, 200, 200)
legend.outline = 1
legend.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(legend)
# Setup
scene = mcrfpy.Scene("bsp_adjacency_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_12_bsp_adjacency.png")
print("Screenshot saved: procgen_12_bsp_adjacency.png")

View file

@ -0,0 +1,178 @@
"""BSP Shrink Parameter Demo
Demonstrates: to_heightmap with different shrink values
Shows room padding for walls and varied room sizes.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Use reasonable shrink values relative to room sizes
shrink_values = [
(0, "shrink=0", "Rooms fill BSP bounds"),
(1, "shrink=1", "Standard 1-tile walls"),
(2, "shrink=2", "Thick fortress walls"),
(3, "shrink=3", "Wide hallway spacing"),
(-1, "Random shrink", "Per-room variation"),
(-2, "Gradient", "Shrink by leaf index"),
]
panels = [
(0, 0), (panel_w, 0), (panel_w * 2, 0),
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
]
for panel_idx, (shrink, title, desc) in enumerate(shrink_values):
ox, oy = panels[panel_idx]
# Create BSP - use depth=2 for larger rooms, bigger min_size
bsp = mcrfpy.BSP(pos=(ox + 1, oy + 3), size=(panel_w - 2, panel_h - 4))
bsp.split_recursive(depth=2, min_size=(8, 6), seed=42)
# Fill panel background (stone wall)
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(50, 45, 55))
if shrink >= 0:
# Standard shrink value using to_heightmap
rooms_hmap = bsp.to_heightmap(
size=(GRID_WIDTH, GRID_HEIGHT),
select='leaves',
shrink=shrink,
value=1.0
)
# Draw floors with color based on shrink level
floor_colors = [
mcrfpy.Color(140, 120, 100), # shrink=0: tan/full
mcrfpy.Color(110, 100, 90), # shrink=1: gray-brown
mcrfpy.Color(90, 95, 100), # shrink=2: blue-gray
mcrfpy.Color(80, 90, 110), # shrink=3: slate
]
floor_color = floor_colors[min(shrink, len(floor_colors) - 1)]
for y in range(oy, oy + panel_h):
for x in range(ox, ox + panel_w):
if rooms_hmap.get((x, y)) > 0:
# Add subtle tile pattern
var = ((x + y) % 2) * 8
c = mcrfpy.Color(
floor_color.r + var,
floor_color.g + var,
floor_color.b + var
)
color_layer.set(((x, y)), c)
elif shrink == -1:
# Random shrink per room
import random
rand = random.Random(42)
for leaf in bsp.leaves():
room_shrink = rand.randint(0, 3)
pos = leaf.pos
size = leaf.size
x1 = pos[0] + room_shrink
y1 = pos[1] + room_shrink
x2 = pos[0] + size[0] - room_shrink
y2 = pos[1] + size[1] - room_shrink
if x2 > x1 and y2 > y1:
colors = [
mcrfpy.Color(160, 130, 100), # Full
mcrfpy.Color(130, 120, 100),
mcrfpy.Color(100, 110, 110),
mcrfpy.Color(80, 90, 100), # Most shrunk
]
floor_color = colors[room_shrink]
for y in range(y1, y2):
for x in range(x1, x2):
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
var = ((x + y) % 2) * 6
c = mcrfpy.Color(
floor_color.r + var,
floor_color.g + var,
floor_color.b + var
)
color_layer.set(((x, y)), c)
else:
# Gradient shrink by leaf index
leaves = list(bsp.leaves())
for i, leaf in enumerate(leaves):
# Shrink increases with leaf index
room_shrink = min(3, i)
pos = leaf.pos
size = leaf.size
x1 = pos[0] + room_shrink
y1 = pos[1] + room_shrink
x2 = pos[0] + size[0] - room_shrink
y2 = pos[1] + size[1] - room_shrink
if x2 > x1 and y2 > y1:
# Color gradient: warm to cool as shrink increases
t = i / max(1, len(leaves) - 1)
floor_color = mcrfpy.Color(
int(180 - t * 80),
int(120 + t * 20),
int(80 + t * 60)
)
for y in range(y1, y2):
for x in range(x1, x2):
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
var = ((x + y) % 2) * 6
c = mcrfpy.Color(
floor_color.r + var,
floor_color.g + var - 2,
floor_color.b + var
)
color_layer.set(((x, y)), c)
# Add labels
label = mcrfpy.Caption(text=title, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
desc_label.outline = 1
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(desc_label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(30, 30, 35))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(30, 30, 35))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(30, 30, 35))
# Setup
scene = mcrfpy.Scene("bsp_shrink_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_13_bsp_shrink.png")
print("Screenshot saved: procgen_13_bsp_shrink.png")

View file

@ -0,0 +1,150 @@
"""BSP Manual Split Demo
Demonstrates: split_once for controlled layouts
Shows handcrafted room placement with manual BSP control.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Fill background
color_layer.fill(mcrfpy.Color(50, 45, 55))
# Create main BSP covering most of the map
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
# Manual split strategy for a temple-like layout:
# 1. Split horizontally to create upper/lower sections
# 2. Upper section: main hall (large) + side rooms
# 3. Lower section: entrance + storage areas
# First split: horizontal, creating top (sanctuary) and bottom (entrance) areas
# Split at about 60% height
split_y = 2 + int((GRID_HEIGHT - 4) * 0.6)
bsp.split_once(horizontal=True, position=split_y)
# Now manually color the structure
root = bsp.root
# Get the two main regions
upper = root.left # Sanctuary area
lower = root.right # Entrance area
# Color the sanctuary (upper area) - golden temple floor
if upper:
pos, size = upper.pos, upper.size
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
# Create a pattern
if (x + y) % 4 == 0:
color_layer.set(((x, y)), mcrfpy.Color(180, 150, 80))
else:
color_layer.set(((x, y)), mcrfpy.Color(160, 130, 70))
# Add altar in center of sanctuary
cx, cy = upper.center()
for dy in range(-2, 3):
for dx in range(-3, 4):
nx, ny = cx + dx, cy + dy
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
if abs(dx) <= 1 and abs(dy) <= 1:
color_layer.set(((nx, ny)), mcrfpy.Color(200, 180, 100)) # Altar
else:
color_layer.set(((nx, ny)), mcrfpy.Color(140, 100, 60)) # Altar base
# Color the entrance (lower area) - stone floor
if lower:
pos, size = lower.pos, lower.size
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
base = 80 + ((x * 3 + y * 7) % 20)
color_layer.set(((x, y)), mcrfpy.Color(base, base - 5, base - 10))
# Add entrance path
cx = pos[0] + size[0] // 2
for y in range(pos[1] + size[1] - 1, pos[1], -1):
for dx in range(-2, 3):
nx = cx + dx
if pos[0] < nx < pos[0] + size[0] - 1:
color_layer.set(((nx, y)), mcrfpy.Color(100, 95, 85))
# Add pillars along the sides
if upper:
pos, size = upper.pos, upper.size
for y in range(pos[1] + 3, pos[1] + size[1] - 3, 4):
# Left pillars
color_layer.set(((pos[0] + 3, y)), mcrfpy.Color(120, 110, 100))
color_layer.set(((pos[0] + 3, y + 1)), mcrfpy.Color(120, 110, 100))
# Right pillars
color_layer.set(((pos[0] + size[0] - 4, y)), mcrfpy.Color(120, 110, 100))
color_layer.set(((pos[0] + size[0] - 4, y + 1)), mcrfpy.Color(120, 110, 100))
# Add side chambers using manual rectangles
# Left chamber
chamber_w, chamber_h = 8, 6
for y in range(10, 10 + chamber_h):
for x in range(4, 4 + chamber_w):
if x == 4 or x == 4 + chamber_w - 1 or y == 10 or y == 10 + chamber_h - 1:
continue # Skip border (walls)
color_layer.set(((x, y)), mcrfpy.Color(100, 80, 90)) # Purple-ish storage
# Right chamber
for y in range(10, 10 + chamber_h):
for x in range(GRID_WIDTH - 4 - chamber_w, GRID_WIDTH - 4):
if x == GRID_WIDTH - 4 - chamber_w or x == GRID_WIDTH - 5 or y == 10 or y == 10 + chamber_h - 1:
continue
color_layer.set(((x, y)), mcrfpy.Color(80, 100, 90)) # Green-ish treasury
# Connect chambers to main hall
hall_y = 12
for x in range(4 + chamber_w, 15):
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
for x in range(GRID_WIDTH - 15, GRID_WIDTH - 4 - chamber_w):
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
# Title
title = mcrfpy.Caption(text="BSP split_once: Temple Layout", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Labels for areas
labels = [
("SANCTUARY", GRID_WIDTH // 2 * CELL_SIZE - 40, 80),
("ENTRANCE", GRID_WIDTH // 2 * CELL_SIZE - 35, split_y * CELL_SIZE + 30),
("Storage", 50, 180),
("Treasury", (GRID_WIDTH - 10) * CELL_SIZE - 30, 180),
]
for text, x, y in labels:
lbl = mcrfpy.Caption(text=text, pos=(x, y))
lbl.fill_color = mcrfpy.Color(200, 200, 200)
lbl.outline = 1
lbl.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(lbl)
# Setup
scene = mcrfpy.Scene("bsp_manual_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_14_bsp_manual_split.png")
print("Screenshot saved: procgen_14_bsp_manual_split.png")

View file

@ -0,0 +1,125 @@
"""NoiseSource Algorithms Demo
Demonstrates: simplex, perlin, wavelet noise algorithms
Shows visual differences between noise types.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def value_to_terrain(h):
"""Convert noise value (-1 to 1) to terrain color."""
# Normalize from -1..1 to 0..1
h = (h + 1) / 2
h = max(0.0, min(1.0, h))
if h < 0.3:
t = h / 0.3
return mcrfpy.Color(int(30 + t * 40), int(60 + t * 60), int(140 + t * 40))
elif h < 0.45:
t = (h - 0.3) / 0.15
return mcrfpy.Color(int(70 + t * 120), int(120 + t * 60), int(100 - t * 60))
elif h < 0.6:
t = (h - 0.45) / 0.15
return mcrfpy.Color(int(60 + t * 20), int(130 + t * 20), int(50 + t * 10))
elif h < 0.75:
t = (h - 0.6) / 0.15
return mcrfpy.Color(int(50 + t * 50), int(110 - t * 20), int(40 + t * 20))
elif h < 0.88:
t = (h - 0.75) / 0.13
return mcrfpy.Color(int(100 + t * 40), int(95 + t * 35), int(80 + t * 40))
else:
t = (h - 0.88) / 0.12
return mcrfpy.Color(int(180 + t * 70), int(180 + t * 70), int(190 + t * 60))
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
algorithms = [
('simplex', "SIMPLEX", "Fast, no visible artifacts"),
('perlin', "PERLIN", "Classic, slight grid bias"),
('wavelet', "WAVELET", "Smooth, no tiling"),
]
# Top row: FBM (natural terrain)
# Bottom row: Raw noise (single octave)
for col, (algo, name, desc) in enumerate(algorithms):
ox = col * panel_w
# Create noise source
noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm=algo,
hurst=0.5,
lacunarity=2.0,
seed=42
)
# Top panel: FBM
for y in range(panel_h):
for x in range(panel_w):
# Sample at world coordinates
wx = x * 0.15
wy = y * 0.15
val = noise.fbm((wx, wy), octaves=5)
color_layer.set(((ox + x, y)), value_to_terrain(val))
# Bottom panel: Raw (flat)
for y in range(panel_h):
for x in range(panel_w):
wx = x * 0.15
wy = y * 0.15
val = noise.get((wx, wy))
color_layer.set(((ox + x, panel_h + y)), value_to_terrain(val))
# Labels
top_label = mcrfpy.Caption(text=f"{name} (FBM)", pos=(ox * CELL_SIZE + 5, 5))
top_label.fill_color = mcrfpy.Color(255, 255, 255)
top_label.outline = 1
top_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(top_label)
bottom_label = mcrfpy.Caption(text=f"{name} (raw)", pos=(ox * CELL_SIZE + 5, panel_h * CELL_SIZE + 5))
bottom_label.fill_color = mcrfpy.Color(255, 255, 255)
bottom_label.outline = 1
bottom_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(bottom_label)
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, 22))
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
desc_label.outline = 1
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(desc_label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
# Setup
scene = mcrfpy.Scene("noise_algo_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_20_noise_algorithms.png")
print("Screenshot saved: procgen_20_noise_algorithms.png")

View file

@ -0,0 +1,115 @@
"""NoiseSource Parameters Demo
Demonstrates: hurst (roughness), lacunarity (frequency scaling), octaves
Shows how parameters affect terrain character.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def value_to_gray(h):
"""Simple grayscale visualization."""
h = (h + 1) / 2 # -1..1 to 0..1
h = max(0.0, min(1.0, h))
v = int(h * 255)
return mcrfpy.Color(v, v, v)
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 3
# 3x3 grid showing parameter variations
# Rows: different hurst values (roughness)
# Cols: different lacunarity values
hurst_values = [0.2, 0.5, 0.8]
lacunarity_values = [1.5, 2.0, 3.0]
for row, hurst in enumerate(hurst_values):
for col, lacunarity in enumerate(lacunarity_values):
ox = col * panel_w
oy = row * panel_h
# Create noise with these parameters
noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm='simplex',
hurst=hurst,
lacunarity=lacunarity,
seed=42
)
# Sample using heightmap for efficiency
hmap = noise.sample(
size=(panel_w, panel_h),
world_origin=(0, 0),
world_size=(10, 10),
mode='fbm',
octaves=6
)
# Apply to color layer
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), value_to_gray(h))
# Parameter label
label = mcrfpy.Caption(
text=f"H={hurst} L={lacunarity}",
pos=(ox * CELL_SIZE + 3, oy * CELL_SIZE + 3)
)
label.fill_color = mcrfpy.Color(255, 255, 0)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Row/Column labels
row_labels = ["Low Hurst (rough)", "Mid Hurst (natural)", "High Hurst (smooth)"]
for row, text in enumerate(row_labels):
label = mcrfpy.Caption(text=text, pos=(5, row * panel_h * CELL_SIZE + panel_h * CELL_SIZE - 20))
label.fill_color = mcrfpy.Color(255, 200, 100)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
col_labels = ["Low Lacunarity", "Standard (2.0)", "High Lacunarity"]
for col, text in enumerate(col_labels):
label = mcrfpy.Caption(text=text, pos=(col * panel_w * CELL_SIZE + 5, GRID_HEIGHT * CELL_SIZE - 20))
label.fill_color = mcrfpy.Color(100, 200, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
color_layer.set(((x, panel_h * 2 - 1)), mcrfpy.Color(100, 100, 100))
# Setup
scene = mcrfpy.Scene("noise_params_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_21_noise_parameters.png")
print("Screenshot saved: procgen_21_noise_parameters.png")

View file

@ -0,0 +1,163 @@
"""Advanced: Cave-Carved Dungeon
Combines: BSP (room structure) + Noise (organic cave walls) + Erosion
Creates a dungeon where rooms have been carved from natural cave formations.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Step 1: Create base cave system using noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
cave_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
cave_map.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4)
cave_map.normalize(0.0, 1.0)
# Step 2: Create BSP rooms
bsp = mcrfpy.BSP(pos=(3, 3), size=(GRID_WIDTH - 6, GRID_HEIGHT - 6))
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.5, seed=42)
rooms_hmap = bsp.to_heightmap(
size=(GRID_WIDTH, GRID_HEIGHT),
select='leaves',
shrink=2,
value=1.0
)
# Step 3: Combine - rooms carve into cave, cave affects walls
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
combined.copy_from(cave_map)
# Scale cave values to mid-range so rooms stand out
combined.scale(0.5)
combined.add_constant(0.2)
# Add room interiors (rooms become high values)
combined.max(rooms_hmap)
# Step 4: Apply GENTLE erosion for organic edges
# Use fewer drops and lower erosion rate
combined.rain_erosion(drops=100, erosion=0.02, sedimentation=0.01, seed=42)
# Re-normalize to ensure we use the full value range
combined.normalize(0.0, 1.0)
# Step 5: Create corridor connections
adjacency = bsp.adjacency
connected = set()
corridor_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
for leaf_idx in range(len(bsp)):
leaf = bsp.get_leaf(leaf_idx)
cx1, cy1 = leaf.center()
for neighbor_idx in adjacency[leaf_idx]:
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
if pair in connected:
continue
connected.add(pair)
neighbor = bsp.get_leaf(neighbor_idx)
cx2, cy2 = neighbor.center()
# Draw corridor using bezier for organic feel
mid_x = (cx1 + cx2) // 2 + ((leaf_idx * 3) % 5 - 2)
mid_y = (cy1 + cy2) // 2 + ((neighbor_idx * 7) % 5 - 2)
corridor_map.dig_bezier(
points=((cx1, cy1), (mid_x, cy1), (mid_x, cy2), (cx2, cy2)),
start_radius=1.5, end_radius=1.5,
start_height=0.0, end_height=0.0
)
# Add corridors - dig_bezier creates low values where corridors are
# We want high values there, so invert the corridor map logic
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
corr_val = corridor_map.get((x, y))
if corr_val < 0.5: # Corridor was dug here
current = combined.get((x, y))
combined.fill(max(current, 0.7), pos=(x, y), size=(1, 1))
# Step 6: Render with cave aesthetics
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
h = combined.get((x, y))
if h < 0.30:
# Solid rock/wall - darker
base = 30 + int(cave_map.get((x, y)) * 20)
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
elif h < 0.40:
# Cave wall edge (rough transition)
t = (h - 0.30) / 0.10
base = int(40 + t * 15)
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
elif h < 0.55:
# Cave floor (natural stone)
t = (h - 0.40) / 0.15
base = 65 + int(t * 20)
var = ((x * 7 + y * 11) % 10)
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 10))
elif h < 0.70:
# Corridor/worked passage
base = 85 + ((x + y) % 2) * 5
color_layer.set(((x, y)), mcrfpy.Color(base, base - 3, base - 6))
else:
# Room floor (finely worked stone)
base = 105 + ((x + y) % 2) * 8
color_layer.set(((x, y)), mcrfpy.Color(base, base - 8, base - 12))
# Mark room centers with special tile
for leaf in bsp.leaves():
cx, cy = leaf.center()
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
color_layer.set(((cx, cy)), mcrfpy.Color(160, 140, 120))
# Cross pattern
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cx + dx, cy + dy
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
color_layer.set(((nx, ny)), mcrfpy.Color(140, 125, 105))
# Outer border
for x in range(GRID_WIDTH):
color_layer.set(((x, 0)), mcrfpy.Color(20, 15, 25))
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 15, 25))
for y in range(GRID_HEIGHT):
color_layer.set(((0, y)), mcrfpy.Color(20, 15, 25))
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 15, 25))
# Title
title = mcrfpy.Caption(text="Cave-Carved Dungeon: BSP + Noise + Erosion", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("cave_dungeon")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_30_advanced_cave_dungeon.png")
print("Screenshot saved: procgen_30_advanced_cave_dungeon.png")

View file

@ -0,0 +1,140 @@
"""Advanced: Island Terrain Generation
Combines: Noise (base terrain) + Voronoi (biomes) + Hills + Erosion + Bezier (rivers)
Creates a tropical island with varied biomes and water features.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def biome_color(elevation, moisture):
"""Determine color based on elevation and moisture."""
if elevation < 0.25:
# Water
t = elevation / 0.25
return mcrfpy.Color(int(30 + t * 30), int(80 + t * 40), int(160 + t * 40))
elif elevation < 0.32:
# Beach
return mcrfpy.Color(220, 200, 150)
elif elevation < 0.5:
# Lowland - varies by moisture
if moisture < 0.3:
return mcrfpy.Color(180, 170, 110) # Desert/savanna
elif moisture < 0.6:
return mcrfpy.Color(80, 140, 60) # Grassland
else:
return mcrfpy.Color(40, 100, 50) # Rainforest
elif elevation < 0.7:
# Highland
if moisture < 0.4:
return mcrfpy.Color(100, 90, 70) # Dry hills
else:
return mcrfpy.Color(50, 90, 45) # Forest
elif elevation < 0.85:
# Mountain
return mcrfpy.Color(110, 105, 100)
else:
# Peak
return mcrfpy.Color(220, 225, 230)
def run_demo(runtime):
# Step 1: Create base elevation using noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
elevation = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
elevation.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=5)
elevation.normalize(0.0, 1.0)
# Step 2: Create island shape using radial falloff
cx, cy = GRID_WIDTH / 2, GRID_HEIGHT / 2
max_dist = min(cx, cy) * 0.85
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
dist = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
falloff = max(0, 1 - (dist / max_dist) ** 1.5)
current = elevation.get((x, y))
elevation.fill(current * falloff, pos=(x, y), size=(1, 1))
# Step 3: Add central mountain range
elevation.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 15, 0.5)
elevation.add_hill((GRID_WIDTH // 2 - 8, GRID_HEIGHT // 2 + 3), 8, 0.3)
elevation.add_hill((GRID_WIDTH // 2 + 10, GRID_HEIGHT // 2 - 5), 6, 0.25)
# Step 4: Create moisture map using different noise
moisture_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
moisture = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
moisture.add_noise(moisture_noise, world_size=(8, 8), mode='fbm', octaves=3)
moisture.normalize(0.0, 1.0)
# Step 5: Add voronoi for biome boundaries
biome_regions = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
biome_regions.add_voronoi(num_points=8, coefficients=(0.5, -0.3), seed=77)
biome_regions.normalize(0.0, 1.0)
# Blend voronoi into moisture
moisture.lerp(biome_regions, 0.4)
# Step 6: Apply erosion to elevation
elevation.rain_erosion(drops=2000, erosion=0.08, sedimentation=0.04, seed=42)
elevation.normalize(0.0, 1.0)
# Step 7: Carve rivers from mountains to sea
# Main river
elevation.dig_bezier(
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 - 5),
(GRID_WIDTH // 2 - 10, GRID_HEIGHT // 2),
(GRID_WIDTH // 4, GRID_HEIGHT // 2 + 5),
(5, GRID_HEIGHT // 2 + 8)),
start_radius=0.5, end_radius=2,
start_height=0.3, end_height=0.15
)
# Secondary river
elevation.dig_bezier(
points=((GRID_WIDTH // 2 + 5, GRID_HEIGHT // 2),
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 3),
(GRID_WIDTH - 15, GRID_HEIGHT // 4),
(GRID_WIDTH - 5, GRID_HEIGHT // 4 + 3)),
start_radius=0.5, end_radius=1.5,
start_height=0.32, end_height=0.18
)
# Step 8: Render
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
elev = elevation.get((x, y))
moist = moisture.get((x, y))
color_layer.set(((x, y)), biome_color(elev, moist))
# Title
title = mcrfpy.Caption(text="Island Terrain: Noise + Voronoi + Hills + Erosion + Rivers", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("island")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_31_advanced_island.png")
print("Screenshot saved: procgen_31_advanced_island.png")

View file

@ -0,0 +1,164 @@
"""Advanced: Procedural City Map
Combines: BSP (city blocks/buildings) + Noise (terrain/parks) + Voronoi (districts)
Creates a city map with districts, buildings, roads, and parks.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Step 1: Create district map using voronoi
districts = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
districts.add_voronoi(num_points=6, coefficients=(1.0, 0.0), seed=42)
districts.normalize(0.0, 1.0)
# District types based on value
# 0.0-0.2: Residential (green-ish)
# 0.2-0.4: Commercial (blue-ish)
# 0.4-0.6: Industrial (gray)
# 0.6-0.8: Park/nature
# 0.8-1.0: Downtown (tall buildings)
# Step 2: Create building blocks using BSP
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
bsp.split_recursive(depth=4, min_size=(6, 5), max_ratio=2.0, seed=42)
# Step 3: Create park areas using noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=99)
parks = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
parks.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3)
parks.normalize(0.0, 1.0)
# Step 4: Render base (roads)
color_layer.fill(mcrfpy.Color(60, 60, 65)) # Asphalt
# Step 5: Draw buildings based on BSP and district type
for leaf in bsp.leaves():
pos = leaf.pos
size = leaf.size
cx, cy = leaf.center()
# Get district type at center
district_val = districts.get((cx, cy))
# Shrink for roads between buildings
shrink = 1
# Determine building style based on district
if district_val < 0.2:
# Residential
building_color = mcrfpy.Color(140, 160, 140)
roof_color = mcrfpy.Color(160, 100, 80)
shrink = 2 # More space between houses
elif district_val < 0.4:
# Commercial
building_color = mcrfpy.Color(120, 140, 170)
roof_color = mcrfpy.Color(80, 100, 130)
elif district_val < 0.6:
# Industrial
building_color = mcrfpy.Color(100, 100, 105)
roof_color = mcrfpy.Color(70, 70, 75)
elif district_val < 0.8:
# Park area - check noise for actual park placement
park_val = parks.get((cx, cy))
if park_val > 0.4:
# This block is a park
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
t = parks.get((x, y))
if t > 0.6:
color_layer.set(((x, y)), mcrfpy.Color(50, 120, 50)) # Trees
else:
color_layer.set(((x, y)), mcrfpy.Color(80, 150, 80)) # Grass
continue
else:
building_color = mcrfpy.Color(130, 150, 130)
roof_color = mcrfpy.Color(100, 80, 70)
else:
# Downtown
building_color = mcrfpy.Color(150, 155, 165)
roof_color = mcrfpy.Color(90, 95, 110)
shrink = 1 # Dense buildings
# Draw building
for y in range(pos[1] + shrink, pos[1] + size[1] - shrink):
for x in range(pos[0] + shrink, pos[0] + size[0] - shrink):
# Building edge (roof)
if y == pos[1] + shrink or y == pos[1] + size[1] - shrink - 1:
color_layer.set(((x, y)), roof_color)
elif x == pos[0] + shrink or x == pos[0] + size[0] - shrink - 1:
color_layer.set(((x, y)), roof_color)
else:
color_layer.set(((x, y)), building_color)
# Step 6: Add main roads (cross the city)
road_color = mcrfpy.Color(70, 70, 75)
marking_color = mcrfpy.Color(200, 200, 100)
# Horizontal main road
main_y = GRID_HEIGHT // 2
for x in range(GRID_WIDTH):
for dy in range(-1, 2):
if 0 <= main_y + dy < GRID_HEIGHT:
color_layer.set(((x, main_y + dy)), road_color)
# Road markings
if x % 4 == 0:
color_layer.set(((x, main_y)), marking_color)
# Vertical main road
main_x = GRID_WIDTH // 2
for y in range(GRID_HEIGHT):
for dx in range(-1, 2):
if 0 <= main_x + dx < GRID_WIDTH:
color_layer.set(((main_x + dx, y)), road_color)
if y % 4 == 0:
color_layer.set(((main_x, y)), marking_color)
# Intersection
for dy in range(-1, 2):
for dx in range(-1, 2):
color_layer.set(((main_x + dx, main_y + dy)), road_color)
# Step 7: Add a central plaza
plaza_x, plaza_y = main_x, main_y
for dy in range(-3, 4):
for dx in range(-4, 5):
nx, ny = plaza_x + dx, plaza_y + dy
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
if abs(dx) <= 1 and abs(dy) <= 1:
color_layer.set(((nx, ny)), mcrfpy.Color(180, 160, 140)) # Fountain
else:
color_layer.set(((nx, ny)), mcrfpy.Color(160, 150, 140)) # Plaza tiles
# Title
title = mcrfpy.Caption(text="Procedural City: BSP + Voronoi Districts + Noise Parks", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("city")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_32_advanced_city.png")
print("Screenshot saved: procgen_32_advanced_city.png")

View file

@ -0,0 +1,163 @@
"""Advanced: Natural Cave System
Combines: Noise (cave formation) + Threshold (open areas) + Kernel (smoothing) + BSP (structured areas)
Creates organic cave networks with some structured rooms.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Step 1: Generate cave base using turbulent noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
cave_noise = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
cave_noise.add_noise(noise, world_size=(10, 8), mode='turbulence', octaves=4)
cave_noise.normalize(0.0, 1.0)
# Step 2: Create cave mask via threshold
# Values > 0.45 become open cave, rest is rock
cave_mask = cave_noise.threshold_binary((0.4, 1.0), 1.0)
# Step 3: Apply smoothing kernel to remove isolated pixels
smooth_kernel = {
(-1, -1): 1, (0, -1): 2, (1, -1): 1,
(-1, 0): 2, (0, 0): 4, (1, 0): 2,
(-1, 1): 1, (0, 1): 2, (1, 1): 1,
}
cave_mask.kernel_transform(smooth_kernel)
cave_mask.normalize(0.0, 1.0)
# Re-threshold after smoothing
cave_mask = cave_mask.threshold_binary((0.5, 1.0), 1.0)
# Step 4: Add some structured rooms using BSP in one corner
# This represents ancient ruins within the caves
bsp = mcrfpy.BSP(pos=(GRID_WIDTH - 22, GRID_HEIGHT - 18), size=(18, 14))
bsp.split_recursive(depth=2, min_size=(6, 5), seed=42)
ruins_hmap = bsp.to_heightmap(
size=(GRID_WIDTH, GRID_HEIGHT),
select='leaves',
shrink=1,
value=1.0
)
# Step 5: Combine caves and ruins
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
combined.copy_from(cave_mask)
combined.max(ruins_hmap)
# Step 6: Add connecting tunnels from ruins to main cave
# Find a cave entrance point
tunnel_points = []
for y in range(GRID_HEIGHT - 18, GRID_HEIGHT - 10):
for x in range(GRID_WIDTH - 25, GRID_WIDTH - 20):
if cave_mask.get((x, y)) > 0.5:
tunnel_points.append((x, y))
break
if tunnel_points:
break
if tunnel_points:
tx, ty = tunnel_points[0]
# Carve tunnel to ruins entrance
combined.dig_bezier(
points=((tx, ty), (tx + 3, ty), (GRID_WIDTH - 22, ty + 2), (GRID_WIDTH - 20, GRID_HEIGHT - 15)),
start_radius=1.5, end_radius=1.5,
start_height=1.0, end_height=1.0
)
# Step 7: Add large cavern (central chamber)
combined.add_hill((GRID_WIDTH // 3, GRID_HEIGHT // 2), 8, 0.6)
# Step 8: Create water pools in low noise areas
water_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=99)
water_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
water_map.add_noise(water_noise, world_size=(15, 12), mode='fbm', octaves=3)
water_map.normalize(0.0, 1.0)
# Step 9: Render
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cave_val = combined.get((x, y))
water_val = water_map.get((x, y))
original_noise = cave_noise.get((x, y))
# Check if in ruins area
in_ruins = (x >= GRID_WIDTH - 22 and x < GRID_WIDTH - 4 and
y >= GRID_HEIGHT - 18 and y < GRID_HEIGHT - 4)
if cave_val < 0.3:
# Solid rock
base = 30 + int(original_noise * 25)
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
elif cave_val < 0.5:
# Cave wall edge
color_layer.set(((x, y)), mcrfpy.Color(45, 40, 50))
else:
# Open cave floor
if water_val > 0.7 and not in_ruins:
# Water pool
t = (water_val - 0.7) / 0.3
color_layer.set(((x, y)), mcrfpy.Color(
int(30 + t * 20), int(50 + t * 30), int(100 + t * 50)
))
elif in_ruins and ruins_hmap.get((x, y)) > 0.5:
# Ruins floor (worked stone)
base = 85 + ((x + y) % 3) * 5
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base))
else:
# Natural cave floor
base = 55 + int(original_noise * 20)
var = ((x * 3 + y * 5) % 8)
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 8))
# Glowing fungi spots
fungi_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=777)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if combined.get((x, y)) > 0.5: # Only in open areas
fungi_val = fungi_noise.get((x * 0.5, y * 0.5))
if fungi_val > 0.8:
color_layer.set(((x, y)), mcrfpy.Color(80, 180, 120))
# Border
for x in range(GRID_WIDTH):
color_layer.set(((x, 0)), mcrfpy.Color(20, 18, 25))
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 18, 25))
for y in range(GRID_HEIGHT):
color_layer.set(((0, y)), mcrfpy.Color(20, 18, 25))
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 18, 25))
# Title
title = mcrfpy.Caption(text="Cave System: Noise + Threshold + Kernel + BSP Ruins", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("caves")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_33_advanced_caves.png")
print("Screenshot saved: procgen_33_advanced_caves.png")

View file

@ -0,0 +1,187 @@
"""Advanced: Volcanic Crater Region
Combines: Hills (mountains) + dig_hill (craters) + Voronoi (lava flows) + Erosion + Noise
Creates a volcanic landscape with active lava, ash fields, and rocky terrain.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def volcanic_color(elevation, lava_intensity, ash_level):
"""Color based on elevation, lava presence, and ash coverage."""
# Lava overrides everything
if lava_intensity > 0.6:
t = (lava_intensity - 0.6) / 0.4
return mcrfpy.Color(
int(200 + t * 55),
int(80 + t * 80),
int(20 + t * 30)
)
elif lava_intensity > 0.4:
# Cooling lava
t = (lava_intensity - 0.4) / 0.2
return mcrfpy.Color(
int(80 + t * 120),
int(30 + t * 50),
int(20)
)
# Check for crater interior (very low elevation)
if elevation < 0.15:
t = elevation / 0.15
return mcrfpy.Color(int(40 + t * 30), int(20 + t * 20), int(10 + t * 15))
# Ash coverage
if ash_level > 0.6:
t = (ash_level - 0.6) / 0.4
base = int(60 + t * 40)
return mcrfpy.Color(base, base - 5, base - 10)
# Normal terrain by elevation
if elevation < 0.3:
# Volcanic plain
t = (elevation - 0.15) / 0.15
return mcrfpy.Color(int(50 + t * 30), int(40 + t * 25), int(35 + t * 20))
elif elevation < 0.5:
# Rocky slopes
t = (elevation - 0.3) / 0.2
return mcrfpy.Color(int(70 + t * 20), int(60 + t * 15), int(50 + t * 15))
elif elevation < 0.7:
# Mountain sides
t = (elevation - 0.5) / 0.2
return mcrfpy.Color(int(85 + t * 25), int(75 + t * 20), int(65 + t * 20))
elif elevation < 0.85:
# High slopes
t = (elevation - 0.7) / 0.15
return mcrfpy.Color(int(100 + t * 30), int(90 + t * 25), int(80 + t * 25))
else:
# Peaks
t = (elevation - 0.85) / 0.15
return mcrfpy.Color(int(130 + t * 50), int(120 + t * 50), int(115 + t * 50))
def run_demo(runtime):
# Step 1: Create base terrain with noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
terrain = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
terrain.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4, scale=0.2)
# Step 2: Add volcanic mountains
# Main volcano
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 20, 0.7)
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 12, 0.3) # Steep peak
# Secondary volcanoes
terrain.add_hill((15, 15), 10, 0.4)
terrain.add_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 8, 0.35)
terrain.add_hill((10, GRID_HEIGHT - 10), 6, 0.25)
# Step 3: Create craters
terrain.dig_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 6, 0.1) # Main crater
terrain.dig_hill((15, 15), 4, 0.15) # Secondary crater
terrain.dig_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 3, 0.18) # Third crater
# Step 4: Create lava flow pattern using voronoi
lava = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
lava.add_voronoi(num_points=12, coefficients=(1.0, -0.8), seed=77)
lava.normalize(0.0, 1.0)
# Lava originates from craters - enhance around crater centers
lava.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 8, 0.5)
lava.add_hill((15, 15), 5, 0.3)
# Lava flows downhill - multiply by inverted terrain
terrain_inv = terrain.inverse()
terrain_inv.normalize(0.0, 1.0)
lava.multiply(terrain_inv)
lava.normalize(0.0, 1.0)
# Step 5: Create ash distribution using noise
ash_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
ash = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
ash.add_noise(ash_noise, world_size=(8, 6), mode='turbulence', octaves=3)
ash.normalize(0.0, 1.0)
# Ash settles on lower areas
ash.multiply(terrain_inv)
# Step 6: Apply erosion for realistic channels
terrain.rain_erosion(drops=1500, erosion=0.1, sedimentation=0.03, seed=42)
terrain.normalize(0.0, 1.0)
# Step 7: Add lava rivers from craters
lava.dig_bezier(
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 + 5),
(GRID_WIDTH // 2 - 5, GRID_HEIGHT // 2 + 15),
(GRID_WIDTH // 3, GRID_HEIGHT - 10),
(10, GRID_HEIGHT - 5)),
start_radius=2, end_radius=3,
start_height=0.9, end_height=0.7
)
lava.dig_bezier(
points=((GRID_WIDTH // 2 + 3, GRID_HEIGHT // 2 + 3),
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 2 + 8),
(GRID_WIDTH - 15, GRID_HEIGHT // 2 + 5),
(GRID_WIDTH - 5, GRID_HEIGHT // 2 + 10)),
start_radius=1.5, end_radius=2.5,
start_height=0.85, end_height=0.65
)
# Step 8: Render
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
elev = terrain.get((x, y))
lava_val = lava.get((x, y))
ash_val = ash.get((x, y))
color_layer.set(((x, y)), volcanic_color(elev, lava_val, ash_val))
# Add smoke/steam particles around crater rims
crater_centers = [
(GRID_WIDTH // 2, GRID_HEIGHT // 2, 6),
(15, 15, 4),
(GRID_WIDTH - 12, GRID_HEIGHT - 15, 3)
]
import math
for cx, cy, radius in crater_centers:
for angle in range(0, 360, 30):
rad = math.radians(angle)
px = int(cx + math.cos(rad) * radius)
py = int(cy + math.sin(rad) * radius)
if 0 <= px < GRID_WIDTH and 0 <= py < GRID_HEIGHT:
# Smoke color
color_layer.set(((px, py)), mcrfpy.Color(150, 140, 130, 180))
# Title
title = mcrfpy.Caption(text="Volcanic Region: Hills + Craters + Voronoi Lava + Erosion", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("volcanic")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_34_advanced_volcanic.png")
print("Screenshot saved: procgen_34_advanced_volcanic.png")

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -0,0 +1,83 @@
"""Run all procgen demos and capture screenshots.
Execute this script from the build directory.
"""
import os
import sys
import subprocess
DEMOS = [
"01_heightmap_hills.py",
"02_heightmap_noise.py",
"03_heightmap_operations.py",
"04_heightmap_transforms.py",
"05_heightmap_erosion.py",
"06_heightmap_voronoi.py",
"07_heightmap_bezier.py",
"08_heightmap_thresholds.py",
"10_bsp_dungeon.py",
"11_bsp_traversal.py",
"12_bsp_adjacency.py",
"13_bsp_shrink.py",
"14_bsp_manual_split.py",
"20_noise_algorithms.py",
"21_noise_parameters.py",
"30_advanced_cave_dungeon.py",
"31_advanced_island.py",
"32_advanced_city.py",
"33_advanced_caves.py",
"34_advanced_volcanic.py",
]
def main():
script_dir = os.path.dirname(os.path.abspath(__file__))
build_dir = os.path.abspath(os.path.join(script_dir, "../../../build"))
if not os.path.exists(os.path.join(build_dir, "mcrogueface")):
print(f"Error: mcrogueface not found in {build_dir}")
print("Please run from the build directory or adjust paths.")
return 1
os.chdir(build_dir)
success = 0
failed = 0
for demo in DEMOS:
demo_path = os.path.join(script_dir, demo)
if not os.path.exists(demo_path):
print(f"SKIP: {demo} (not found)")
continue
print(f"Running: {demo}...", end=" ", flush=True)
try:
result = subprocess.run(
["./mcrogueface", "--headless", "--exec", demo_path],
timeout=30,
capture_output=True,
text=True
)
# Check if screenshot was created
png_name = f"procgen_{demo.replace('.py', '.png')}"
if os.path.exists(png_name):
print(f"OK -> {png_name}")
success += 1
else:
print(f"FAIL (no screenshot)")
if result.stderr:
print(f" stderr: {result.stderr[:200]}")
failed += 1
except subprocess.TimeoutExpired:
print("TIMEOUT")
failed += 1
except Exception as e:
print(f"ERROR: {e}")
failed += 1
print(f"\nResults: {success} passed, {failed} failed")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,397 @@
"""McRogueFace - Sprite Labeling Tool
A development tool for rapid sprite sheet labeling during game jams.
Creates a dictionary mapping sprite indices to custom labels.
Usage:
./mcrogueface docs/cookbook/tools/sprite_labeler.py
Console commands (while running):
# Save labels to file
import json; json.dump(labels, open('sprite_labels.json', 'w'), indent=2)
# Load labels from file
labels.update(json.load(open('sprite_labels.json')))
# Print current labels
for k, v in sorted(labels.items()): print(f"{k}: {v}")
"""
import mcrfpy
# === Global State ===
labels = {} # sprite_index (int) -> label (str)
selected_label = None # Currently selected label name
current_sprite_index = 0 # Currently hovered sprite
# Label categories - customize these for your game!
DEFAULT_LABELS = [
"player",
"enemy",
"wall",
"floor",
"door",
"item",
"trap",
"decoration",
]
# === Configuration ===
# Change these to match your texture!
TEXTURE_PATH = "assets/kenney_tinydungeon.png" # Path relative to build dir
TILE_SIZE = 16 # Size of each tile in the texture
GRID_COLS = 12 # Number of sprite columns in texture (texture_width / tile_size)
GRID_ROWS = 11 # Number of sprite rows in texture (texture_height / tile_size)
# UI Layout
WINDOW_WIDTH = 1024
WINDOW_HEIGHT = 768
GRID_X, GRID_Y = 50, 50
GRID_WIDTH, GRID_HEIGHT = 12 * 16 * 2, 11 * 16 * 2
PREVIEW_X, PREVIEW_Y = 480, 50
PREVIEW_SCALE = 4.0
PANEL_X = 480 + 180
PANEL_Y = 50
# Colors
BG_COLOR = mcrfpy.Color(30, 30, 40)
PANEL_COLOR = mcrfpy.Color(40, 45, 55)
BUTTON_COLOR = mcrfpy.Color(60, 65, 80)
BUTTON_HOVER = mcrfpy.Color(80, 85, 100)
BUTTON_SELECTED = mcrfpy.Color(80, 140, 80)
TEXT_COLOR = mcrfpy.Color(220, 220, 230)
LABEL_COLOR = mcrfpy.Color(100, 180, 255)
INPUT_BG = mcrfpy.Color(25, 25, 35)
INPUT_ACTIVE = mcrfpy.Color(35, 35, 50)
# === Scene Setup ===
scene = mcrfpy.Scene("sprite_labeler")
ui = scene.children
# Background
bg = mcrfpy.Frame(pos=(0, 0), size=(WINDOW_WIDTH, WINDOW_HEIGHT))
bg.fill_color = BG_COLOR
ui.append(bg)
# Load texture
texture = mcrfpy.Texture(TEXTURE_PATH, TILE_SIZE, TILE_SIZE)
# === Grid (shows all sprites) ===
grid = mcrfpy.Grid(
grid_size=(GRID_COLS, GRID_ROWS),
pos=(GRID_X, GRID_Y),
size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture,
zoom=2.0
)
grid.fill_color = mcrfpy.Color(20, 20, 30)
# Initialize grid with all sprites
for row in range(GRID_ROWS):
for col in range(GRID_COLS):
sprite_index = row * GRID_COLS + col
cell = grid.at(col, row)
if cell:
cell.tilesprite = sprite_index
ui.append(grid)
# === Preview Section ===
preview_frame = mcrfpy.Frame(pos=(PREVIEW_X, PREVIEW_Y), size=(100, 100))
preview_frame.fill_color = PANEL_COLOR
preview_frame.outline = 1
preview_frame.outline_color = mcrfpy.Color(80, 80, 100)
ui.append(preview_frame)
preview_sprite = mcrfpy.Sprite((18, 18), texture, 0)
preview_sprite.scale = PREVIEW_SCALE
preview_frame.children.append(preview_sprite)
# Index caption
index_caption = mcrfpy.Caption(pos=(PREVIEW_X, PREVIEW_Y + 110), text="Index: 0")
index_caption.font_size = 18
index_caption.fill_color = TEXT_COLOR
ui.append(index_caption)
# Label display (shows current label for hovered sprite)
label_display = mcrfpy.Caption(pos=(PREVIEW_X, PREVIEW_Y + 135), text="Label: (none)")
label_display.font_size = 16
label_display.fill_color = LABEL_COLOR
ui.append(label_display)
# === Input Section (for adding new labels) ===
input_panel = mcrfpy.Frame(pos=(PANEL_X, PANEL_Y), size=(300, 80))
input_panel.fill_color = PANEL_COLOR
input_panel.outline = 1
input_panel.outline_color = mcrfpy.Color(80, 80, 100)
ui.append(input_panel)
input_title = mcrfpy.Caption(pos=(10, 8), text="Add New Label:")
input_title.font_size = 14
input_title.fill_color = TEXT_COLOR
input_panel.children.append(input_title)
# Text input field (frame + caption)
input_field = mcrfpy.Frame(pos=(10, 35), size=(200, 30))
input_field.fill_color = INPUT_BG
input_field.outline = 1
input_field.outline_color = mcrfpy.Color(60, 60, 80)
input_panel.children.append(input_field)
input_text = mcrfpy.Caption(pos=(8, 6), text="")
input_text.font_size = 14
input_text.fill_color = TEXT_COLOR
input_field.children.append(input_text)
# Text input state
input_buffer = ""
input_active = False
# Submit button
submit_btn = mcrfpy.Frame(pos=(220, 35), size=(70, 30))
submit_btn.fill_color = BUTTON_COLOR
submit_btn.outline = 1
submit_btn.outline_color = mcrfpy.Color(100, 100, 120)
input_panel.children.append(submit_btn)
submit_text = mcrfpy.Caption(pos=(12, 6), text="Add")
submit_text.font_size = 14
submit_text.fill_color = TEXT_COLOR
submit_btn.children.append(submit_text)
# === Label Selection Panel ===
labels_panel = mcrfpy.Frame(pos=(PANEL_X, PANEL_Y + 100), size=(300, 350))
labels_panel.fill_color = PANEL_COLOR
labels_panel.outline = 1
labels_panel.outline_color = mcrfpy.Color(80, 80, 100)
ui.append(labels_panel)
labels_title = mcrfpy.Caption(pos=(10, 8), text="Select Label (click to choose):")
labels_title.font_size = 14
labels_title.fill_color = TEXT_COLOR
labels_panel.children.append(labels_title)
# Store label button references for updating selection highlight
label_buttons = [] # list of (frame, caption, label_name)
def create_label_button(label_name, index):
"""Create a clickable label button"""
row = index // 2
col = index % 2
btn_x = 10 + col * 145
btn_y = 35 + row * 40
btn = mcrfpy.Frame(pos=(btn_x, btn_y), size=(135, 32))
btn.fill_color = BUTTON_COLOR
btn.outline = 1
btn.outline_color = mcrfpy.Color(80, 80, 100)
btn_caption = mcrfpy.Caption(pos=(8, 7), text=label_name[:14])
btn_caption.font_size = 12
btn_caption.fill_color = TEXT_COLOR
btn.children.append(btn_caption)
labels_panel.children.append(btn)
label_buttons.append((btn, btn_caption, label_name))
# Set click handler
def on_label_click(x, y, button):
select_label(label_name)
#return True
btn.on_click = on_label_click
return btn
def select_label(label_name):
"""Select a label for applying to sprites"""
global selected_label
selected_label = label_name
# Update button colors
for btn, caption, name in label_buttons:
if name == selected_label:
btn.fill_color = BUTTON_SELECTED
else:
btn.fill_color = BUTTON_COLOR
def add_new_label(label_name):
"""Add a new label to the selection list"""
global input_buffer
# Don't add duplicates or empty labels
label_name = label_name.strip()
if not label_name:
return
for _, _, existing in label_buttons:
if existing == label_name:
return
# Create button for new label
create_label_button(label_name, len(label_buttons))
# Clear input
input_buffer = ""
input_text.text = ""
# Select the new label
select_label(label_name)
# Initialize default labels
for i, label in enumerate(DEFAULT_LABELS):
create_label_button(label, i)
# Select first label by default
if DEFAULT_LABELS:
select_label(DEFAULT_LABELS[0])
# === Event Handlers ===
def on_cell_enter(cell):
"""Handle mouse hovering over grid cells"""
global current_sprite_index
x, y = int(cell[0]), int(cell[1])
sprite_index = y * GRID_COLS + x
current_sprite_index = sprite_index
# Update preview
preview_sprite.sprite_index = sprite_index
index_caption.text = f"Index: {sprite_index}"
# Update label display
if sprite_index in labels:
label_display.text = f"Label: {labels[sprite_index]}"
label_display.fill_color = BUTTON_SELECTED
else:
label_display.text = "Label: (none)"
label_display.fill_color = LABEL_COLOR
def on_cell_click(cell):
"""Handle clicking on grid cells to apply labels"""
global labels
x, y = int(cell[0]), int(cell[1])
sprite_index = y * GRID_COLS + x
if selected_label:
labels[sprite_index] = selected_label
print(f"Labeled sprite {sprite_index} as '{selected_label}'")
# Update display if this is the current sprite
if sprite_index == current_sprite_index:
label_display.text = f"Label: {selected_label}"
label_display.fill_color = BUTTON_SELECTED
grid.on_cell_enter = on_cell_enter
grid.on_cell_click = on_cell_click
# Submit button click handler
def on_submit_click(x, y, button):
"""Handle submit button click"""
add_new_label(input_buffer)
#return True
submit_btn.on_click = on_submit_click
# Input field click handler (activate text input)
def on_input_click(x, y, button):
"""Handle input field click to activate typing"""
global input_active
input_active = True
input_field.fill_color = INPUT_ACTIVE
#return True
input_field.on_click = on_input_click
# === Keyboard Handler ===
def on_keypress(key, state):
"""Handle keyboard input for text entry"""
global input_buffer, input_active
if state != "start":
return
# Escape clears input focus
if key == "Escape":
input_active = False
input_field.fill_color = INPUT_BG
return
# Enter submits the label
if key == "Return":
if input_active and input_buffer:
add_new_label(input_buffer)
input_active = False
input_field.fill_color = INPUT_BG
return
# Only process text input when field is active
if not input_active:
return
# Backspace
if key == "BackSpace":
if input_buffer:
input_buffer = input_buffer[:-1]
input_text.text = input_buffer
return
# Handle alphanumeric and common characters
# Map key names to characters
if len(key) == 1 and key.isalpha():
input_buffer += key.lower()
input_text.text = input_buffer
elif key.startswith("Num") and len(key) == 4:
# Numpad numbers
input_buffer += key[3]
input_text.text = input_buffer
elif key == "Space":
input_buffer += " "
input_text.text = input_buffer
elif key == "Minus":
input_buffer += "-"
input_text.text = input_buffer
elif key == "Period":
input_buffer += "."
input_text.text = input_buffer
scene.on_key = on_keypress
# === Instructions Caption ===
#instructions = mcrfpy.Caption(
# pos=(GRID_X, GRID_Y + GRID_HEIGHT + 20),
# text="Hover: preview sprite | Click grid: apply selected label | Type: add new labels"
#)
#instructions.font_size = 12
#instructions.fill_color = mcrfpy.Color(150, 150, 160)
#ui.append(instructions)
#
#instructions2 = mcrfpy.Caption(
# pos=(GRID_X, GRID_Y + GRID_HEIGHT + 40),
# text="Console: labels (dict), json.dump(labels, open('labels.json','w'))"
#)
#instructions2.font_size = 12
#instructions2.fill_color = mcrfpy.Color(150, 150, 160)
#ui.append(instructions2)
# Activate the scene
scene.activate()
#print("=== Sprite Labeler Tool ===")
#print(f"Texture: {TEXTURE_PATH}")
#print(f"Grid: {GRID_COLS}x{GRID_ROWS} = {GRID_COLS * GRID_ROWS} sprites")
#print("")
#print("Usage:")
#print(" - Hover over grid to preview sprites")
#print(" - Click a label button to select it")
#print(" - Click on grid cells to apply the selected label")
#print(" - Type in the text field to add new labels")
#print("")
#print("Console commands:")
#print(" labels # View all labels")
#print(" labels[42] = 'custom' # Manual labeling")
#print(" import json")
#print(" json.dump(labels, open('sprite_labels.json', 'w'), indent=2) # Save")
#print(" labels.update(json.load(open('sprite_labels.json'))) # Load")