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()