McRogueFace/tests/procgen_cave_visualization.py

199 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import mcrfpy
import sys
class ProcgenDemo:
"""Multi-step procedural generation visualization.
Demonstrates the workflow from the libtcod discussion:
1. BSP defines room structure
2. Noise adds organic variation
3. Boolean mask composition (AND/multiply)
4. Inversion for floor selection
5. Smoothing for gradient effects
"""
MAP_SIZE = (64, 48)
CELL_SIZE = 14
# Color palettes
MASK_RANGES = [
((0.0, 0.01), (20, 20, 25)), # Empty: near-black
((0.01, 1.0), (220, 215, 200)), # Filled: off-white
]
TERRAIN_RANGES = [
((0.0, 0.25), ((30, 50, 120), (50, 80, 150))), # Deep water → water
((0.25, 0.35), ((50, 80, 150), (180, 170, 130))), # Water → sand
((0.35, 0.55), ((80, 140, 60), (50, 110, 40))), # Light grass → dark grass
((0.55, 0.75), ((50, 110, 40), (120, 100, 80))), # Grass → rock
((0.75, 1.0), ((120, 100, 80), (230, 230, 235))), # Rock → snow
]
DUNGEON_RANGES = [
((0.0, 0.1), (40, 35, 30)), # Wall: dark stone
((0.1, 0.5), ((60, 55, 50), (140, 130, 110))), # Gradient floor
((0.5, 1.0), ((140, 130, 110), (180, 170, 140))), # Lighter floor
]
def __init__(self):
# HeightMaps
self.terrain = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
self.scratchpad = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
self.bsp = None
self.noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
# Scene
scene = mcrfpy.Scene("procgen_demo")
# Grid with color layer
self.grid = mcrfpy.Grid(
grid_size=self.MAP_SIZE,
pos=(20, 60),
size=(self.MAP_SIZE[0] * self.CELL_SIZE,
self.MAP_SIZE[1] * self.CELL_SIZE),
layers={"viz": "color"}
)
scene.children.append(self.grid)
# UI
self.title = mcrfpy.Caption(text="Procedural Generation Demo",
pos=(20, 15), font_size=24)
self.label = mcrfpy.Caption(text="Initializing...",
pos=(20, 40), font_size=16)
scene.children.append(self.title)
scene.children.append(self.label)
mcrfpy.current_scene = scene
# Step schedule
self.steps = [
(500*2, self.step_01_bsp_rooms, "Step 1: BSP Room Partitioning"),
(2500*2, self.step_02_noise, "Step 2: Generate Noise Field"),
(4500*2, self.step_03_threshold, "Step 3: Threshold to Organic Shapes"),
(6500*2, self.step_04_combine, "Step 4: BSP AND Noise to Cave Walls"),
(8500*2, self.step_05_invert, "Step 5: Invert walls (Floor Regions)"),
(10500*2, self.step_06_floor_heights, "Step 6: Add Floor Height Variation"),
(12500*2, self.step_07_smooth, "Step 7: Smooth for Gradient Floors"),
(14500*2, self.step_done, "Complete!"),
]
self.current_step = 0
self.start_time = None
self.timer = mcrfpy.Timer("procgen", self.tick, 50)
def tick(self, timer, runtime):
if self.start_time is None:
self.start_time = runtime
elapsed = runtime - self.start_time
while (self.current_step < len(self.steps) and
elapsed >= self.steps[self.current_step][0]):
_, step_fn, step_label = self.steps[self.current_step]
self.label.text = step_label
step_fn()
self.current_step += 1
if self.current_step >= len(self.steps):
timer.stop()
def apply_colors(self, hmap, ranges):
"""Apply color ranges to grid via GridPoint access."""
# Since we can't get layer directly, iterate cells
w, h = self.MAP_SIZE
for y in range(h):
for x in range(w):
val = hmap[x, y]
color = self._value_to_color(val, ranges)
self.grid[x, y].viz = color
def _value_to_color(self, val, ranges):
"""Find color for value in ranges list."""
for (lo, hi), color_spec in ranges:
if lo <= val <= hi:
if isinstance(color_spec[0], tuple):
# Gradient: interpolate
c1, c2 = color_spec
t = (val - lo) / (hi - lo) if hi > lo else 0
return tuple(int(c1[i] + t * (c2[i] - c1[i])) for i in range(3))
else:
# Fixed color
return color_spec
return (128, 128, 128) # Fallback gray
# =========================================================
# GENERATION STEPS
# =========================================================
def step_01_bsp_rooms(self):
"""Create BSP partition and visualize rooms."""
self.bsp = mcrfpy.BSP(pos=(1, 1), size=(62, 46))
self.bsp.split_recursive(depth=4, min_size=(8, 6), seed=42)
rooms = self.bsp.to_heightmap(self.MAP_SIZE, 'leaves', shrink=1)
self.scratchpad.copy_from(rooms)
self.apply_colors(self.scratchpad, self.MASK_RANGES)
def step_02_noise(self):
"""Generate FBM noise and visualize."""
self.terrain.fill(0.0)
self.terrain.add_noise(self.noise, world_size=(12, 12),
mode='fbm', octaves=5)
self.terrain.normalize(0.0, 1.0)
self.apply_colors(self.terrain, self.TERRAIN_RANGES)
def step_03_threshold(self):
"""Threshold noise to create organic cave boundaries."""
cave_mask = self.terrain.threshold_binary((0.45, 1.0), value=1.0)
self.terrain.copy_from(cave_mask)
self.apply_colors(self.terrain, self.MASK_RANGES)
def step_04_combine(self):
"""AND operation: BSP rooms × noise threshold = cave walls."""
# scratchpad has BSP rooms (1 = inside room)
# terrain has noise threshold (1 = "solid" area)
# multiply gives: 1 where both are 1
combined = mcrfpy.HeightMap(self.MAP_SIZE)
combined.copy_from(self.scratchpad)
combined.multiply(self.terrain)
self.scratchpad.copy_from(combined)
self.apply_colors(self.scratchpad, self.MASK_RANGES)
def step_05_invert(self):
"""Invert to get floor regions (0 becomes floor)."""
# After AND: 1 = wall (inside room AND solid noise)
# Invert: 0 → 1 (floor), 1 → 0 (wall)
# But inverse does 1 - x, so 1 becomes 0, 0 becomes 1
floors = self.scratchpad.inverse()
# Clamp because inverse can give negative values if > 1
floors.clamp(0.0, 1.0)
self.terrain.copy_from(floors)
self.apply_colors(self.terrain, self.DUNGEON_RANGES)
def step_06_floor_heights(self):
"""Add height variation to floors using noise."""
# Create new noise for floor heights
floor_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=789)
height_var = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
height_var.add_noise(floor_noise, world_size=(20, 20),
mode='fbm', octaves=3, scale=0.4)
height_var.add_constant(0.5)
height_var.clamp(0.0, 1.0)
# Mask to floor regions only (terrain has floor mask from step 5)
height_var.multiply(self.terrain)
self.terrain.copy_from(height_var)
self.apply_colors(self.terrain, self.DUNGEON_RANGES)
def step_07_smooth(self):
"""Apply smoothing for gradient floor effect."""
self.terrain.smooth(iterations=1)
self.apply_colors(self.terrain, self.DUNGEON_RANGES)
def step_done(self):
"""Final step - display completion message."""
self.label.text = "Complete!"
# Launch
demo = ProcgenDemo()