diff --git a/docs/cookbook/procgen/01_heightmap_hills.py b/docs/cookbook/procgen/01_heightmap_hills.py new file mode 100644 index 0000000..4f4a360 --- /dev/null +++ b/docs/cookbook/procgen/01_heightmap_hills.py @@ -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") diff --git a/docs/cookbook/procgen/02_heightmap_noise.py b/docs/cookbook/procgen/02_heightmap_noise.py new file mode 100644 index 0000000..5d4db43 --- /dev/null +++ b/docs/cookbook/procgen/02_heightmap_noise.py @@ -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") diff --git a/docs/cookbook/procgen/03_heightmap_operations.py b/docs/cookbook/procgen/03_heightmap_operations.py new file mode 100644 index 0000000..e98239e --- /dev/null +++ b/docs/cookbook/procgen/03_heightmap_operations.py @@ -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") diff --git a/docs/cookbook/procgen/04_heightmap_transforms.py b/docs/cookbook/procgen/04_heightmap_transforms.py new file mode 100644 index 0000000..792df1a --- /dev/null +++ b/docs/cookbook/procgen/04_heightmap_transforms.py @@ -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") diff --git a/docs/cookbook/procgen/05_heightmap_erosion.py b/docs/cookbook/procgen/05_heightmap_erosion.py new file mode 100644 index 0000000..817030a --- /dev/null +++ b/docs/cookbook/procgen/05_heightmap_erosion.py @@ -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") diff --git a/docs/cookbook/procgen/06_heightmap_voronoi.py b/docs/cookbook/procgen/06_heightmap_voronoi.py new file mode 100644 index 0000000..5e2a996 --- /dev/null +++ b/docs/cookbook/procgen/06_heightmap_voronoi.py @@ -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") diff --git a/docs/cookbook/procgen/07_heightmap_bezier.py b/docs/cookbook/procgen/07_heightmap_bezier.py new file mode 100644 index 0000000..15ae80c --- /dev/null +++ b/docs/cookbook/procgen/07_heightmap_bezier.py @@ -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") diff --git a/docs/cookbook/procgen/08_heightmap_thresholds.py b/docs/cookbook/procgen/08_heightmap_thresholds.py new file mode 100644 index 0000000..bfc6bb5 --- /dev/null +++ b/docs/cookbook/procgen/08_heightmap_thresholds.py @@ -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") diff --git a/docs/cookbook/procgen/10_bsp_dungeon.py b/docs/cookbook/procgen/10_bsp_dungeon.py new file mode 100644 index 0000000..87fcfce --- /dev/null +++ b/docs/cookbook/procgen/10_bsp_dungeon.py @@ -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") diff --git a/docs/cookbook/procgen/11_bsp_traversal.py b/docs/cookbook/procgen/11_bsp_traversal.py new file mode 100644 index 0000000..82dc845 --- /dev/null +++ b/docs/cookbook/procgen/11_bsp_traversal.py @@ -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") diff --git a/docs/cookbook/procgen/12_bsp_adjacency.py b/docs/cookbook/procgen/12_bsp_adjacency.py new file mode 100644 index 0000000..85810d6 --- /dev/null +++ b/docs/cookbook/procgen/12_bsp_adjacency.py @@ -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") diff --git a/docs/cookbook/procgen/13_bsp_shrink.py b/docs/cookbook/procgen/13_bsp_shrink.py new file mode 100644 index 0000000..5bf0dcf --- /dev/null +++ b/docs/cookbook/procgen/13_bsp_shrink.py @@ -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") diff --git a/docs/cookbook/procgen/14_bsp_manual_split.py b/docs/cookbook/procgen/14_bsp_manual_split.py new file mode 100644 index 0000000..91998aa --- /dev/null +++ b/docs/cookbook/procgen/14_bsp_manual_split.py @@ -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") diff --git a/docs/cookbook/procgen/20_noise_algorithms.py b/docs/cookbook/procgen/20_noise_algorithms.py new file mode 100644 index 0000000..0ac32ef --- /dev/null +++ b/docs/cookbook/procgen/20_noise_algorithms.py @@ -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") diff --git a/docs/cookbook/procgen/21_noise_parameters.py b/docs/cookbook/procgen/21_noise_parameters.py new file mode 100644 index 0000000..2c78c1c --- /dev/null +++ b/docs/cookbook/procgen/21_noise_parameters.py @@ -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") diff --git a/docs/cookbook/procgen/30_advanced_cave_dungeon.py b/docs/cookbook/procgen/30_advanced_cave_dungeon.py new file mode 100644 index 0000000..8d3a03e --- /dev/null +++ b/docs/cookbook/procgen/30_advanced_cave_dungeon.py @@ -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") diff --git a/docs/cookbook/procgen/31_advanced_island.py b/docs/cookbook/procgen/31_advanced_island.py new file mode 100644 index 0000000..1b844a4 --- /dev/null +++ b/docs/cookbook/procgen/31_advanced_island.py @@ -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") diff --git a/docs/cookbook/procgen/32_advanced_city.py b/docs/cookbook/procgen/32_advanced_city.py new file mode 100644 index 0000000..e385909 --- /dev/null +++ b/docs/cookbook/procgen/32_advanced_city.py @@ -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") diff --git a/docs/cookbook/procgen/33_advanced_caves.py b/docs/cookbook/procgen/33_advanced_caves.py new file mode 100644 index 0000000..db3862d --- /dev/null +++ b/docs/cookbook/procgen/33_advanced_caves.py @@ -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") diff --git a/docs/cookbook/procgen/34_advanced_volcanic.py b/docs/cookbook/procgen/34_advanced_volcanic.py new file mode 100644 index 0000000..b4cd0e3 --- /dev/null +++ b/docs/cookbook/procgen/34_advanced_volcanic.py @@ -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") diff --git a/docs/cookbook/procgen/procgen_01_heightmap_hills.png b/docs/cookbook/procgen/procgen_01_heightmap_hills.png new file mode 100644 index 0000000..3b167d8 Binary files /dev/null and b/docs/cookbook/procgen/procgen_01_heightmap_hills.png differ diff --git a/docs/cookbook/procgen/procgen_02_heightmap_noise.png b/docs/cookbook/procgen/procgen_02_heightmap_noise.png new file mode 100644 index 0000000..95604f8 Binary files /dev/null and b/docs/cookbook/procgen/procgen_02_heightmap_noise.png differ diff --git a/docs/cookbook/procgen/procgen_03_heightmap_operations.png b/docs/cookbook/procgen/procgen_03_heightmap_operations.png new file mode 100644 index 0000000..731b45d Binary files /dev/null and b/docs/cookbook/procgen/procgen_03_heightmap_operations.png differ diff --git a/docs/cookbook/procgen/procgen_04_heightmap_transforms.png b/docs/cookbook/procgen/procgen_04_heightmap_transforms.png new file mode 100644 index 0000000..4c82b7f Binary files /dev/null and b/docs/cookbook/procgen/procgen_04_heightmap_transforms.png differ diff --git a/docs/cookbook/procgen/procgen_05_heightmap_erosion.png b/docs/cookbook/procgen/procgen_05_heightmap_erosion.png new file mode 100644 index 0000000..09f2256 Binary files /dev/null and b/docs/cookbook/procgen/procgen_05_heightmap_erosion.png differ diff --git a/docs/cookbook/procgen/procgen_06_heightmap_voronoi.png b/docs/cookbook/procgen/procgen_06_heightmap_voronoi.png new file mode 100644 index 0000000..f2a025d Binary files /dev/null and b/docs/cookbook/procgen/procgen_06_heightmap_voronoi.png differ diff --git a/docs/cookbook/procgen/procgen_07_heightmap_bezier.png b/docs/cookbook/procgen/procgen_07_heightmap_bezier.png new file mode 100644 index 0000000..f6a808a Binary files /dev/null and b/docs/cookbook/procgen/procgen_07_heightmap_bezier.png differ diff --git a/docs/cookbook/procgen/procgen_08_heightmap_thresholds.png b/docs/cookbook/procgen/procgen_08_heightmap_thresholds.png new file mode 100644 index 0000000..2cc413c Binary files /dev/null and b/docs/cookbook/procgen/procgen_08_heightmap_thresholds.png differ diff --git a/docs/cookbook/procgen/procgen_10_bsp_dungeon.png b/docs/cookbook/procgen/procgen_10_bsp_dungeon.png new file mode 100644 index 0000000..72f146a Binary files /dev/null and b/docs/cookbook/procgen/procgen_10_bsp_dungeon.png differ diff --git a/docs/cookbook/procgen/procgen_11_bsp_traversal.png b/docs/cookbook/procgen/procgen_11_bsp_traversal.png new file mode 100644 index 0000000..d8fcb6d Binary files /dev/null and b/docs/cookbook/procgen/procgen_11_bsp_traversal.png differ diff --git a/docs/cookbook/procgen/procgen_12_bsp_adjacency.png b/docs/cookbook/procgen/procgen_12_bsp_adjacency.png new file mode 100644 index 0000000..c174f3c Binary files /dev/null and b/docs/cookbook/procgen/procgen_12_bsp_adjacency.png differ diff --git a/docs/cookbook/procgen/procgen_13_bsp_shrink.png b/docs/cookbook/procgen/procgen_13_bsp_shrink.png new file mode 100644 index 0000000..66c0362 Binary files /dev/null and b/docs/cookbook/procgen/procgen_13_bsp_shrink.png differ diff --git a/docs/cookbook/procgen/procgen_14_bsp_manual_split.png b/docs/cookbook/procgen/procgen_14_bsp_manual_split.png new file mode 100644 index 0000000..ab843fa Binary files /dev/null and b/docs/cookbook/procgen/procgen_14_bsp_manual_split.png differ diff --git a/docs/cookbook/procgen/procgen_20_noise_algorithms.png b/docs/cookbook/procgen/procgen_20_noise_algorithms.png new file mode 100644 index 0000000..a715010 Binary files /dev/null and b/docs/cookbook/procgen/procgen_20_noise_algorithms.png differ diff --git a/docs/cookbook/procgen/procgen_21_noise_parameters.png b/docs/cookbook/procgen/procgen_21_noise_parameters.png new file mode 100644 index 0000000..6f62544 Binary files /dev/null and b/docs/cookbook/procgen/procgen_21_noise_parameters.png differ diff --git a/docs/cookbook/procgen/procgen_30_advanced_cave_dungeon.png b/docs/cookbook/procgen/procgen_30_advanced_cave_dungeon.png new file mode 100644 index 0000000..bc616d2 Binary files /dev/null and b/docs/cookbook/procgen/procgen_30_advanced_cave_dungeon.png differ diff --git a/docs/cookbook/procgen/procgen_31_advanced_island.png b/docs/cookbook/procgen/procgen_31_advanced_island.png new file mode 100644 index 0000000..502050d Binary files /dev/null and b/docs/cookbook/procgen/procgen_31_advanced_island.png differ diff --git a/docs/cookbook/procgen/procgen_32_advanced_city.png b/docs/cookbook/procgen/procgen_32_advanced_city.png new file mode 100644 index 0000000..987916e Binary files /dev/null and b/docs/cookbook/procgen/procgen_32_advanced_city.png differ diff --git a/docs/cookbook/procgen/procgen_33_advanced_caves.png b/docs/cookbook/procgen/procgen_33_advanced_caves.png new file mode 100644 index 0000000..09d9db9 Binary files /dev/null and b/docs/cookbook/procgen/procgen_33_advanced_caves.png differ diff --git a/docs/cookbook/procgen/procgen_34_advanced_volcanic.png b/docs/cookbook/procgen/procgen_34_advanced_volcanic.png new file mode 100644 index 0000000..d794b44 Binary files /dev/null and b/docs/cookbook/procgen/procgen_34_advanced_volcanic.png differ diff --git a/docs/cookbook/procgen/run_all_demos.py b/docs/cookbook/procgen/run_all_demos.py new file mode 100644 index 0000000..e627b5f --- /dev/null +++ b/docs/cookbook/procgen/run_all_demos.py @@ -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()) diff --git a/docs/cookbook/tools/sprite_labeler.py b/docs/cookbook/tools/sprite_labeler.py new file mode 100644 index 0000000..60fe412 --- /dev/null +++ b/docs/cookbook/tools/sprite_labeler.py @@ -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")