Test suite modernization
This commit is contained in:
parent
0969f7c2f6
commit
52fdfd0347
141 changed files with 9947 additions and 4665 deletions
282
tests/procgen_cave2_visualization.py
Normal file
282
tests/procgen_cave2_visualization.py
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import mcrfpy
|
||||
|
||||
class ProcgenDemo:
|
||||
"""Multi-step procedural generation: terrain with embedded caves."""
|
||||
|
||||
MAP_SIZE = (64, 48)
|
||||
CELL_SIZE = 14
|
||||
|
||||
# Terrain colors (outside caves)
|
||||
TERRAIN_RANGES = [
|
||||
((0.0, 0.15), ((30, 50, 120), (50, 80, 150))), # Water
|
||||
((0.51, 0.25), ((50, 80, 150), (180, 170, 130))), # Beach
|
||||
((0.25, 0.55), ((80, 140, 60), (50, 110, 40))), # Grass
|
||||
((0.55, 0.75), ((50, 110, 40), (120, 100, 80))), # Rock
|
||||
((0.75, 1.0), ((120, 100, 80), (200, 195, 190))), # Mountain
|
||||
]
|
||||
|
||||
# Cave interior colors
|
||||
CAVE_RANGES = [
|
||||
((0.0, 0.15), (35, 30, 28)), # Wall (dark)
|
||||
((0.15, 0.5), ((50, 45, 42), (100, 90, 80))), # Floor gradient
|
||||
((0.5, 1.0), ((100, 90, 80), (140, 125, 105))), # Lighter floor
|
||||
]
|
||||
|
||||
# Mask visualization
|
||||
MASK_RANGES = [
|
||||
((0.0, 0.01), (20, 20, 25)),
|
||||
((0.01, 1.0), (220, 215, 200)),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
# HeightMaps
|
||||
self.terrain = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||||
self.cave_selection = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||||
self.cave_interior = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||||
self.scratchpad = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||||
|
||||
self.bsp = None
|
||||
self.terrain_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=44)
|
||||
self.cave_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=900)
|
||||
|
||||
# Scene setup
|
||||
scene = mcrfpy.Scene("procgen_demo")
|
||||
|
||||
self.grid = mcrfpy.Grid(
|
||||
grid_size=self.MAP_SIZE,
|
||||
pos=(0,0),
|
||||
size=(1024, 768),
|
||||
layers={"viz": "color"}
|
||||
)
|
||||
scene.children.append(self.grid)
|
||||
|
||||
self.title = mcrfpy.Caption(text="Terrain + Cave Procgen",
|
||||
pos=(20, 15), font_size=24)
|
||||
self.label = mcrfpy.Caption(text="", pos=(20, 45), font_size=16)
|
||||
scene.children.append(self.title)
|
||||
scene.children.append(self.label)
|
||||
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
# Steps with longer pauses for complex operations
|
||||
self.steps = [
|
||||
(500, self.step_01_terrain, "1: Generate terrain elevation"),
|
||||
(4000, self.step_02_bsp_all, "2: BSP partition (all leaves)"),
|
||||
(5000, self.step_03_bsp_subset, "3: Select cave-worthy BSP nodes"),
|
||||
(7000, self.step_04_terrain_mask, "4: Exclude low terrain (water/canyon)"),
|
||||
(9000, self.step_05_valid_caves, "5: Valid cave regions (BSP && high terrain)"),
|
||||
(11000, self.step_06_cave_noise, "6: Organic cave walls (noise threshold)"),
|
||||
(13000, self.step_07_apply_to_selection,"7: Walls within selection only"),
|
||||
(15000, self.step_08_invert_floors, "8: Invert -> cave floors"),
|
||||
(17000, self.step_09_floor_heights, "9: Add floor height variation"),
|
||||
(19000, self.step_10_smooth, "10: Smooth floor gradients"),
|
||||
(21000, self.step_11_composite, "11: Composite: terrain + caves"),
|
||||
(23000, 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):
|
||||
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):
|
||||
for (lo, hi), color_spec in ranges:
|
||||
if lo <= val <= hi:
|
||||
if isinstance(color_spec[0], tuple):
|
||||
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:
|
||||
return color_spec
|
||||
return (128, 128, 128)
|
||||
|
||||
# =========================================================
|
||||
# STEP 1: BASE TERRAIN
|
||||
# =========================================================
|
||||
|
||||
def step_01_terrain(self):
|
||||
"""Generate the base terrain with elevation."""
|
||||
self.terrain.fill(0.0)
|
||||
self.terrain.add_noise(self.terrain_noise,
|
||||
world_size=(10, 10),
|
||||
mode='fbm', octaves=5)
|
||||
self.terrain.normalize(0.0, 1.0)
|
||||
self.apply_colors(self.terrain, self.TERRAIN_RANGES)
|
||||
|
||||
# =========================================================
|
||||
# STEPS 2-5: CAVE SELECTION (where caves can exist)
|
||||
# =========================================================
|
||||
|
||||
def step_02_bsp_all(self):
|
||||
"""Show all BSP leaves (potential cave locations)."""
|
||||
self.bsp = mcrfpy.BSP(pos=(2, 2), size=(60, 44))
|
||||
self.bsp.split_recursive(depth=4, min_size=(8, 6), seed=66)
|
||||
|
||||
all_rooms = self.bsp.to_heightmap(self.MAP_SIZE, 'leaves', shrink=1)
|
||||
self.apply_colors(all_rooms, self.MASK_RANGES)
|
||||
|
||||
def step_03_bsp_subset(self):
|
||||
"""Select only SOME BSP leaves for caves."""
|
||||
self.cave_selection.fill(0.0)
|
||||
|
||||
# Selection criteria: only leaves whose center is in
|
||||
# higher terrain AND not too close to edges
|
||||
w, h = self.MAP_SIZE
|
||||
for leaf in self.bsp.leaves():
|
||||
cx, cy = leaf.center()
|
||||
|
||||
# Skip if center is out of bounds
|
||||
if not (0 <= cx < w and 0 <= cy < h):
|
||||
continue
|
||||
|
||||
terrain_height = self.terrain[cx, cy]
|
||||
|
||||
# Criteria:
|
||||
# - Terrain height > 0.4 (above water/beach)
|
||||
# - Not too close to map edges
|
||||
# - Some randomness based on position
|
||||
edge_margin = 8
|
||||
in_center = (edge_margin < cx < w - edge_margin and
|
||||
edge_margin < cy < h - edge_margin)
|
||||
|
||||
# Pseudo-random selection based on leaf position
|
||||
pseudo_rand = ((cx * 7 + cy * 13) % 10) / 10.0
|
||||
|
||||
if terrain_height > 0.45 and in_center and pseudo_rand > 0.3:
|
||||
# Fill this leaf into selection
|
||||
lx, ly = leaf.pos
|
||||
lw, lh = leaf.size
|
||||
for y in range(ly, ly + lh):
|
||||
for x in range(lx, lx + lw):
|
||||
if 0 <= x < w and 0 <= y < h:
|
||||
self.cave_selection[x, y] = 1.0
|
||||
|
||||
self.apply_colors(self.cave_selection, self.MASK_RANGES)
|
||||
|
||||
def step_04_terrain_mask(self):
|
||||
"""Create mask of terrain high enough for caves."""
|
||||
# Threshold: only where terrain > 0.35 (above water/beach)
|
||||
high_terrain = self.terrain.threshold_binary((0.35, 1.0), value=1.0)
|
||||
self.scratchpad.copy_from(high_terrain)
|
||||
self.apply_colors(self.scratchpad, self.MASK_RANGES)
|
||||
|
||||
def step_05_valid_caves(self):
|
||||
"""AND: selected BSP nodes × high terrain = valid cave regions."""
|
||||
# cave_selection has our chosen BSP leaves
|
||||
# scratchpad has the "high enough" terrain mask
|
||||
self.cave_selection.multiply(self.scratchpad)
|
||||
self.apply_colors(self.cave_selection, self.MASK_RANGES)
|
||||
|
||||
# =========================================================
|
||||
# STEPS 6-10: CAVE INTERIOR (detail within selection)
|
||||
# =========================================================
|
||||
|
||||
def step_06_cave_noise(self):
|
||||
"""Generate organic noise for cave wall shapes."""
|
||||
self.cave_interior.fill(0.0)
|
||||
self.cave_interior.add_noise(self.cave_noise,
|
||||
world_size=(15, 15),
|
||||
mode='fbm', octaves=4)
|
||||
self.cave_interior.normalize(0.0, 1.0)
|
||||
|
||||
# Threshold to binary: 1 = solid (wall), 0 = open
|
||||
walls = self.cave_interior.threshold_binary((0.42, 1.0), value=1.0)
|
||||
self.cave_interior.copy_from(walls)
|
||||
self.apply_colors(self.cave_interior, self.MASK_RANGES)
|
||||
|
||||
def step_07_apply_to_selection(self):
|
||||
"""Walls only within the valid cave selection."""
|
||||
# cave_interior has organic wall pattern
|
||||
# cave_selection has valid cave regions
|
||||
# AND them: walls only where both are 1
|
||||
self.cave_interior.multiply(self.cave_selection)
|
||||
self.apply_colors(self.cave_interior, self.MASK_RANGES)
|
||||
|
||||
def step_08_invert_floors(self):
|
||||
"""Invert to get floor regions within caves."""
|
||||
# cave_interior: 1 = wall, 0 = not-wall
|
||||
# We want floors where selection=1 AND wall=0
|
||||
# floors = selection AND (NOT walls)
|
||||
|
||||
walls_inverted = self.cave_interior.inverse()
|
||||
walls_inverted.clamp(0.0, 1.0)
|
||||
|
||||
# AND with selection to get floors only in cave areas
|
||||
floors = mcrfpy.HeightMap(self.MAP_SIZE)
|
||||
floors.copy_from(self.cave_selection)
|
||||
floors.multiply(walls_inverted)
|
||||
|
||||
self.cave_interior.copy_from(floors)
|
||||
self.apply_colors(self.cave_interior, self.MASK_RANGES)
|
||||
|
||||
def step_09_floor_heights(self):
|
||||
"""Add height variation to cave floors."""
|
||||
floor_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=456)
|
||||
|
||||
heights = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||||
heights.add_noise(floor_noise, world_size=(25, 25),
|
||||
mode='fbm', octaves=3, scale=0.5)
|
||||
heights.add_constant(0.5)
|
||||
heights.clamp(0.2, 1.0) # Keep floors visible (not too dark)
|
||||
|
||||
# Mask to floor regions
|
||||
heights.multiply(self.cave_interior)
|
||||
self.cave_interior.copy_from(heights)
|
||||
self.apply_colors(self.cave_interior, self.CAVE_RANGES)
|
||||
|
||||
def step_10_smooth(self):
|
||||
"""Smooth the floor heights for gradients."""
|
||||
self.cave_interior.smooth(iterations=1)
|
||||
self.apply_colors(self.cave_interior, self.CAVE_RANGES)
|
||||
|
||||
# =========================================================
|
||||
# STEP 11: COMPOSITE
|
||||
# =========================================================
|
||||
|
||||
def step_11_composite(self):
|
||||
"""Composite: terrain outside caves + cave interior inside."""
|
||||
w, h = self.MAP_SIZE
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
cave_val = self.cave_interior[x, y]
|
||||
terrain_val = self.terrain[x, y]
|
||||
|
||||
if cave_val > 0.01:
|
||||
# Inside cave: use cave colors
|
||||
color = self._value_to_color(cave_val, self.CAVE_RANGES)
|
||||
else:
|
||||
# Outside cave: use terrain colors
|
||||
color = self._value_to_color(terrain_val, self.TERRAIN_RANGES)
|
||||
|
||||
self.grid[x, y].viz = color
|
||||
|
||||
def step_done(self):
|
||||
self.label.text = "Mixed procgen terrain"
|
||||
|
||||
# Launch
|
||||
demo = ProcgenDemo()
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue