McRogueFace/tests/procgen_interactive/demos/cave_demo.py

362 lines
12 KiB
Python
Raw Permalink Normal View History

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