Update Procedural-Generation wiki: modernize API usage (Scene/Timer/Entity/Grid constructors, remove cell.color, use positional grid.at, add visual layers section)

John McCardle 2026-02-07 22:32:05 +00:00
commit 991b6d95dd

@ -14,11 +14,6 @@ McRogueFace supports procedural content generation through its Grid and Entity s
- `src/scripts/cos_tiles.py` - Wave Function Collapse tile placement
- `src/UIGrid.cpp` - Grid manipulation API
**Related Issues:**
- [#123](../issues/123) - Subgrid system for large worlds
- [#67](../issues/67) - Grid stitching for seamless chunks
- [#55](../issues/55) - Agent simulation demo
---
## Binary Space Partitioning (BSP)
@ -43,7 +38,6 @@ class BinaryRoomNode:
def split(self, min_size=8):
"""Split this room into two sub-rooms"""
# Choose split direction based on aspect ratio
if self.w > self.h:
direction = "vertical"
elif self.h > self.w:
@ -51,7 +45,6 @@ class BinaryRoomNode:
else:
direction = random.choice(["vertical", "horizontal"])
# Calculate split position (30-70%)
split_ratio = random.uniform(0.3, 0.7)
if direction == "vertical" and self.w >= min_size * 2:
@ -67,7 +60,7 @@ class BinaryRoomNode:
self.w, self.h - split_y)
return True
return False # Can't split further
return False
def get_leaves(self):
"""Get all leaf rooms (no children)"""
@ -80,12 +73,12 @@ class BinaryRoomNode:
leaves.extend(self.right.get_leaves())
return leaves
# Generate dungeon layout
def generate_bsp_dungeon(grid, num_splits=4):
"""Generate dungeon layout using BSP"""
w, h = grid.grid_size
root = BinaryRoomNode(0, 0, w, h)
# Split recursively
nodes = [root]
for _ in range(num_splits):
new_nodes = []
@ -96,12 +89,10 @@ def generate_bsp_dungeon(grid, num_splits=4):
new_nodes.append(node)
nodes = new_nodes
# Get leaf rooms
rooms = root.get_leaves()
# Carve rooms into grid
for room in rooms:
# Add margin
rx = room.x + 2
ry = room.y + 2
rw = room.w - 4
@ -109,10 +100,10 @@ def generate_bsp_dungeon(grid, num_splits=4):
for x in range(rx, rx + rw):
for y in range(ry, ry + rh):
cell = grid.at((x, y))
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.tilesprite = 0 # Floor tile
cell.color = (128, 128, 128, 255)
return rooms
```
@ -126,30 +117,28 @@ After generating rooms, create corridors:
```python
def carve_corridor(grid, x1, y1, x2, y2):
"""L-shaped corridor between two points"""
# Horizontal first
for x in range(min(x1, x2), max(x1, x2) + 1):
cell = grid.at((x, y1))
cell = grid.at(x, y1)
cell.walkable = True
cell.transparent = True
cell.tilesprite = 0
# Then vertical
for y in range(min(y1, y2), max(y1, y2) + 1):
cell = grid.at((x2, y))
cell = grid.at(x2, y)
cell.walkable = True
cell.transparent = True
cell.tilesprite = 0
def connect_rooms(grid, rooms):
"""Connect all rooms with corridors"""
for i in range(len(rooms) - 1):
r1 = rooms[i]
r2 = rooms[i + 1]
# Room centers
x1 = r1.x + r1.w // 2
y1 = r1.y + r1.h // 2
x2 = r2.x + r2.w // 2
y2 = r2.y + r2.h // 2
carve_corridor(grid, x1, y1, x2, y2)
```
@ -173,69 +162,35 @@ class TileRule:
"west": west or []
}
def can_place(self, grid, x, y):
"""Check if tile can be placed at (x, y)"""
# Check each neighbor
if y > 0: # North
north_tile = grid.at((x, y - 1)).tilesprite
if north_tile not in self.neighbors["north"]:
return False
if y < grid.grid_size[1] - 1: # South
south_tile = grid.at((x, y + 1)).tilesprite
if south_tile not in self.neighbors["south"]:
return False
# ... check east/west similarly
return True
# Define tile rules (example: simple dungeon)
FLOOR = 0
WALL = 1
RULES = [
TileRule(FLOOR, north=[FLOOR, WALL], south=[FLOOR, WALL],
east=[FLOOR, WALL], west=[FLOOR, WALL]),
TileRule(WALL, north=[WALL], south=[WALL],
east=[WALL], west=[WALL])
]
def wfc_generate(grid):
"""Generate tiles using Wave Function Collapse"""
w, h = grid.grid_size
# Track cells with multiple possibilities
possibilities = {}
for x in range(w):
for y in range(h):
possibilities[(x, y)] = [0, 1] # Can be floor or wall
possibilities[(x, y)] = [0, 1]
# Iteratively collapse
while possibilities:
# Find cell with minimum entropy (fewest possibilities)
min_cell = min(possibilities.keys(),
key=lambda c: len(possibilities[c]))
x, y = min_cell
# Choose random tile from possibilities
tile = random.choice(possibilities[min_cell])
grid.at((x, y)).tilesprite = tile
grid.at(x, y).tilesprite = tile
del possibilities[min_cell]
# Update neighbor possibilities based on constraints
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = x + dx, y + dy
if (nx, ny) in possibilities:
# Filter based on placed tile's constraints
# (simplified - real WFC is more complex)
possibilities[(nx, ny)] = [t for t in possibilities[(nx, ny)]
if can_coexist(tile, t)]
possibilities[(nx, ny)] = [
t for t in possibilities[(nx, ny)]
if can_coexist(tile, t)
]
```
See `src/scripts/cos_tiles.py` for the complete WFC implementation with:
- 9-cell constraint matching
- Special cardinal direction rules
- Weighted tile selection
- Multi-pass convergence
See `src/scripts/cos_tiles.py` for the complete WFC implementation with 9-cell constraint matching and weighted tile selection.
---
@ -255,19 +210,21 @@ def generate_cave(grid, fill_probability=0.45, iterations=5):
# Initialize with random noise
for x in range(w):
for y in range(h):
cell = grid.at(x, y)
if random.random() < fill_probability:
grid.at((x, y)).walkable = False
grid.at((x, y)).tilesprite = 1 # Wall
cell.walkable = False
cell.transparent = False
cell.tilesprite = 1 # Wall
else:
grid.at((x, y)).walkable = True
grid.at((x, y)).tilesprite = 0 # Floor
cell.walkable = True
cell.transparent = True
cell.tilesprite = 0 # Floor
# Iterate cellular automata rules
for _ in range(iterations):
next_state = []
for x in range(w):
for y in range(h):
# Count wall neighbors
wall_count = 0
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
@ -275,21 +232,21 @@ def generate_cave(grid, fill_probability=0.45, iterations=5):
continue
nx, ny = x + dx, y + dy
if nx < 0 or nx >= w or ny < 0 or ny >= h:
wall_count += 1 # Out of bounds = wall
elif not grid.at((nx, ny)).walkable:
wall_count += 1
elif not grid.at(nx, ny).walkable:
wall_count += 1
# Apply rule: become wall if 5+ neighbors are walls
is_wall = wall_count >= 5
next_state.append((x, y, is_wall))
# Apply next state
for x, y, is_wall in next_state:
grid.at((x, y)).walkable = not is_wall
grid.at((x, y)).tilesprite = 1 if is_wall else 0
cell = grid.at(x, y)
cell.walkable = not is_wall
cell.transparent = not is_wall
cell.tilesprite = 1 if is_wall else 0
```
### Smoothing and Refinement
### Region Cleanup
```python
def remove_small_regions(grid, min_size=10):
@ -298,7 +255,6 @@ def remove_small_regions(grid, min_size=10):
visited = set()
def flood_fill(x, y):
"""Returns size of connected region"""
stack = [(x, y)]
region = []
while stack:
@ -307,79 +263,60 @@ def remove_small_regions(grid, min_size=10):
continue
if cx < 0 or cx >= w or cy < 0 or cy >= h:
continue
if not grid.at((cx, cy)).walkable:
if not grid.at(cx, cy).walkable:
continue
visited.add((cx, cy))
region.append((cx, cy))
# Add neighbors
stack.extend([(cx-1, cy), (cx+1, cy), (cx, cy-1), (cx, cy+1)])
return region
# Find all regions
regions = []
for x in range(w):
for y in range(h):
if (x, y) not in visited and grid.at((x, y)).walkable:
if (x, y) not in visited and grid.at(x, y).walkable:
region = flood_fill(x, y)
regions.append(region)
# Keep only largest region, fill others
if regions:
largest = max(regions, key=len)
for region in regions:
if len(region) < min_size:
if region is not largest and len(region) < min_size:
for x, y in region:
grid.at((x, y)).walkable = False
grid.at((x, y)).tilesprite = 1
cell = grid.at(x, y)
cell.walkable = False
cell.transparent = False
cell.tilesprite = 1
```
---
## Noise-Based Terrain
## Visual Layers for Generated Content
### Perlin/Simplex Noise
Generate heightmaps for terrain:
GridPoint properties (`walkable`, `transparent`, `tilesprite`) handle world state. For visual styling, use layers:
```python
# Requires noise library: pip install noise
import noise
# Create grid with explicit layers
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[])
def generate_terrain(grid, scale=0.1, octaves=4):
"""Generate terrain using Perlin noise"""
w, h = grid.grid_size
# Background colors layer
bg = mcrfpy.ColorLayer(name="background", z_index=-2)
grid.add_layer(bg)
bg.fill(mcrfpy.Color(20, 20, 30, 255)) # Dark blue
for x in range(w):
for y in range(h):
# Sample noise
value = noise.pnoise2(x * scale, y * scale,
octaves=octaves,
persistence=0.5,
lacunarity=2.0,
repeatx=w,
repeaty=h,
base=0)
# Tile sprites layer (requires a texture)
# tiles = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=tileset)
# grid.add_layer(tiles)
# Map to tile types (-1 to 1 -> 0 to N)
if value < -0.3:
tile = 2 # Water
walkable = False
elif value < 0.0:
tile = 0 # Grass
walkable = True
elif value < 0.3:
tile = 1 # Forest
walkable = True
else:
tile = 3 # Mountain
walkable = False
# Fog overlay (above entities)
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid.add_layer(fog)
fog.fill(mcrfpy.Color(0, 0, 0, 255)) # Start fully hidden
cell = grid.at((x, y))
cell.tilesprite = tile
cell.walkable = walkable
# After generating rooms, color the background
for room in rooms:
for x in range(room.x + 2, room.x + room.w - 2):
for y in range(room.y + 2, room.y + room.h - 2):
bg.set((x, y), mcrfpy.Color(128, 128, 128, 255))
```
---
@ -398,30 +335,28 @@ def place_entities(grid, rooms, entity_density=0.05):
num_entities = int(room_area * entity_density)
for _ in range(num_entities):
# Random walkable position in room
x = random.randint(room.x, room.x + room.w - 1)
y = random.randint(room.y, room.y + room.h - 1)
x = random.randint(room.x + 2, room.x + room.w - 3)
y = random.randint(room.y + 2, room.y + room.h - 3)
if grid.at((x, y)).walkable:
if grid.at(x, y).walkable:
enemy = mcrfpy.Entity(
grid_pos=(x, y),
sprite_index=random.randint(10, 15)
)
grid.entities.append(enemy)
def place_items(grid, rooms, num_items=10):
"""Place items in random locations"""
placed = 0
attempts = 0
max_attempts = num_items * 10
while placed < num_items and attempts < max_attempts:
# Random room
while placed < num_items and attempts < num_items * 10:
room = random.choice(rooms)
x = random.randint(room.x, room.x + room.w - 1)
y = random.randint(room.y, room.y + room.h - 1)
x = random.randint(room.x + 2, room.x + room.w - 3)
y = random.randint(room.y + 2, room.y + room.h - 3)
if grid.at((x, y)).walkable:
if grid.at(x, y).walkable:
item = mcrfpy.Entity(
grid_pos=(x, y),
sprite_index=random.randint(20, 25)
@ -443,114 +378,76 @@ For worlds larger than memory allows:
```python
class ChunkManager:
"""Generate chunks on-demand"""
def __init__(self, chunk_size=256):
def __init__(self, chunk_size=64):
self.chunk_size = chunk_size
self.loaded_chunks = {} # (cx, cy) -> Grid
self.loaded_chunks = {}
def get_chunk(self, chunk_x, chunk_y):
"""Load or generate chunk"""
key = (chunk_x, chunk_y)
if key not in self.loaded_chunks:
self.loaded_chunks[key] = self._generate_chunk(chunk_x, chunk_y)
return self.loaded_chunks[key]
def _generate_chunk(self, cx, cy):
"""Generate a single chunk"""
grid = mcrfpy.Grid(
grid_size=(self.chunk_size, self.chunk_size),
pos=(cx * self.chunk_size * 16, cy * self.chunk_size * 16),
size=(self.chunk_size * 16, self.chunk_size * 16)
)
# Use chunk coordinates as seed for deterministic generation
seed = hash((cx, cy))
random.seed(seed)
# Generate terrain for this chunk
generate_terrain(grid, scale=0.1)
generate_cave(grid)
return grid
def unload_distant_chunks(self, player_chunk, radius=2):
"""Unload chunks far from player"""
px, py = player_chunk
to_unload = []
for (cx, cy), chunk in self.loaded_chunks.items():
if abs(cx - px) > radius or abs(cy - py) > radius:
to_unload.append((cx, cy))
to_unload = [
key for key in self.loaded_chunks
if abs(key[0] - px) > radius or abs(key[1] - py) > radius
]
for key in to_unload:
del self.loaded_chunks[key]
```
**Issue #123** and **Issue #67** track official subgrid and chunk stitching support.
---
## Performance Considerations
## Incremental Generation with Timers
### Batch Grid Updates
Set many cells efficiently:
Generate during scene transitions to avoid frame drops:
```python
# SLOW: Individual cell updates
for x in range(100):
for y in range(100):
cell = grid.at((x, y))
cell.tilesprite = compute_tile(x, y)
generation_step = [0]
rooms = [None]
# FASTER: Minimize Python/C++ crossings
tiles = []
for x in range(100):
for y in range(100):
tiles.append(compute_tile(x, y))
def start_generation(grid):
"""Begin incremental procedural generation"""
scene = mcrfpy.Scene("loading")
mcrfpy.current_scene = scene
# Then set in batch (when batch API available - see [[Performance-and-Profiling]])
for i, (x, y) in enumerate(coords):
grid.at((x, y)).tilesprite = tiles[i]
```
def generate_world(timer, runtime):
if generation_step[0] == 0:
rooms[0] = generate_bsp_dungeon(grid)
elif generation_step[0] == 1:
connect_rooms(grid, rooms[0])
elif generation_step[0] == 2:
place_entities(grid, rooms[0])
else:
timer.stop()
game_scene = mcrfpy.Scene("game")
game_scene.children.append(grid)
mcrfpy.current_scene = game_scene
return
### Generation Timing
generation_step[0] += 1
Generate during scene transitions or in background:
```python
def start_generation():
"""Begin procedural generation"""
mcrfpy.createScene("loading")
mcrfpy.setScene("loading")
# Start generation timer
mcrfpy.setTimer("generate", generate_world, 10)
def generate_world(ms):
"""Generate incrementally"""
global generation_step
if generation_step == 0:
rooms = generate_bsp_dungeon(grid)
elif generation_step == 1:
connect_rooms(grid, rooms)
elif generation_step == 2:
wfc_generate_tiles(grid)
elif generation_step == 3:
place_entities(grid)
else:
# Done!
mcrfpy.setScene("game")
mcrfpy.delTimer("generate")
return
generation_step += 1
mcrfpy.Timer("gen_step", generate_world, 10)
```
---
## Examples and Demos
### Complete Dungeon Generator
## Complete Dungeon Generator
```python
import mcrfpy
@ -558,7 +455,6 @@ import random
def create_procedural_dungeon():
"""Complete dungeon generation pipeline"""
# Create grid
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(0, 0), size=(1024, 768))
# 1. Generate layout with BSP
@ -567,13 +463,7 @@ def create_procedural_dungeon():
# 2. Connect rooms
connect_rooms(grid, rooms)
# 3. Tile placement with WFC
wfc_generate_tiles(grid)
# 4. Smooth with cellular automata
smooth_edges(grid)
# 5. Place player in first room
# 3. Place player in first room
first_room = rooms[0]
player = mcrfpy.Entity(
grid_pos=(first_room.x + first_room.w // 2,
@ -582,10 +472,10 @@ def create_procedural_dungeon():
)
grid.entities.append(player)
# 6. Place enemies
# 4. Place enemies
place_entities(grid, rooms[1:], entity_density=0.05)
# 7. Place exit in last room
# 5. Place exit in last room
last_room = rooms[-1]
exit_entity = mcrfpy.Entity(
grid_pos=(last_room.x + last_room.w // 2,
@ -596,31 +486,38 @@ def create_procedural_dungeon():
return grid, player
# Use it
mcrfpy.createScene("game")
mcrfpy.setScene("game")
game_scene = mcrfpy.Scene("game")
grid, player = create_procedural_dungeon()
ui = mcrfpy.sceneUI("game")
ui.append(grid)
game_scene.children.append(grid)
mcrfpy.current_scene = game_scene
```
See `src/scripts/cos_level.py` and `src/scripts/cos_tiles.py` for production examples from "Crypt of Sokoban."
See `src/scripts/cos_level.py` and `src/scripts/cos_tiles.py` for production examples.
---
## API Reference
See [`docs/api_reference_dynamic.html`](../src/branch/master/docs/api_reference_dynamic.html) for Grid and Entity APIs.
**Grid Construction:**
- `mcrfpy.Grid(grid_size=(w, h), pos=(x, y), size=(pw, ph))` - Create grid
**Key Grid Methods:**
- `grid.at((x, y))` - Get cell for modification
- `grid.grid_size` - Get dimensions
- `grid.entities` - Entity collection
**Cell Properties (GridPoint):**
- `cell.walkable` - Pathfinding flag (bool)
- `cell.transparent` - FOV flag (bool)
- `cell.tilesprite` - Tile sprite index (int, legacy)
**Key Cell Properties:**
- `cell.tilesprite` - Tile index
- `cell.walkable` - Pathfinding flag
- `cell.color` - RGBA tint
**Visual Layers:**
- `mcrfpy.ColorLayer(name="...", z_index=N)` - Color overlay layer
- `mcrfpy.TileLayer(name="...", z_index=N, texture=tex)` - Tile sprite layer
- `grid.add_layer(layer)` - Attach layer to grid
- `layer.set((x, y), value)` - Set cell value
- `layer.fill(value)` - Fill all cells
**Entity Placement:**
- `mcrfpy.Entity(grid_pos=(x, y), sprite_index=N)` - Create entity
- `grid.entities.append(entity)` - Add to grid
---