Add "Procedural-Generation"

John McCardle 2025-10-25 22:46:15 +00:00
parent 612cd2c101
commit dc09e36937
1 changed files with 631 additions and 0 deletions

631
Procedural-Generation.-.md Normal file

@ -0,0 +1,631 @@
# Procedural Generation
## Overview
McRogueFace supports procedural content generation through its Grid and Entity systems. The engine provides the spatial containers and rendering, while Python scripts implement generation algorithms like Binary Space Partitioning, Wave Function Collapse, cellular automata, and noise-based terrain.
**Related Pages:**
- [[Grid-System]] - Spatial containers for generated content
- [[Entity-Management]] - Populating grids with entities
- [[AI-and-Pathfinding]] - Walkability and navigation for generated maps
**Key Files:**
- `src/scripts/cos_level.py` - BSP dungeon generation
- `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)
### Dungeon Layout Generation
BSP recursively divides space to create room layouts:
```python
import random
import mcrfpy
class BinaryRoomNode:
"""Node in BSP tree representing a room"""
def __init__(self, x, y, w, h):
self.x = x
self.y = y
self.w = w
self.h = h
self.left = None
self.right = None
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:
direction = "horizontal"
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:
split_x = int(self.w * split_ratio)
self.left = BinaryRoomNode(self.x, self.y, split_x, self.h)
self.right = BinaryRoomNode(self.x + split_x, self.y,
self.w - split_x, self.h)
return True
elif direction == "horizontal" and self.h >= min_size * 2:
split_y = int(self.h * split_ratio)
self.left = BinaryRoomNode(self.x, self.y, self.w, split_y)
self.right = BinaryRoomNode(self.x, self.y + split_y,
self.w, self.h - split_y)
return True
return False # Can't split further
def get_leaves(self):
"""Get all leaf rooms (no children)"""
if self.left is None and self.right is None:
return [self]
leaves = []
if self.left:
leaves.extend(self.left.get_leaves())
if self.right:
leaves.extend(self.right.get_leaves())
return leaves
# Generate dungeon layout
def generate_bsp_dungeon(grid, num_splits=4):
w, h = grid.grid_size
root = BinaryRoomNode(0, 0, w, h)
# Split recursively
nodes = [root]
for _ in range(num_splits):
new_nodes = []
for node in nodes:
if node.split():
new_nodes.extend([node.left, node.right])
else:
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
rh = room.h - 4
for x in range(rx, rx + rw):
for y in range(ry, ry + rh):
cell = grid.at((x, y))
cell.walkable = True
cell.tilesprite = 0 # Floor tile
cell.color = (128, 128, 128, 255)
return rooms
```
See `src/scripts/cos_level.py` for the complete implementation used in "Crypt of Sokoban."
### Connecting Rooms
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.walkable = True
cell.tilesprite = 0
# Then vertical
for y in range(min(y1, y2), max(y1, y2) + 1):
cell = grid.at((x2, y))
cell.walkable = 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)
```
---
## Wave Function Collapse (WFC)
### Tile-Based Constraint Solving
WFC generates tilemap patterns that satisfy local constraints:
```python
class TileRule:
"""Constraint: which tiles can appear adjacent"""
def __init__(self, tile_id, north=None, south=None, east=None, west=None):
self.tile_id = tile_id
self.neighbors = {
"north": north or [],
"south": south or [],
"east": east or [],
"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
# 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
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)]
```
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
---
## Cellular Automata
### Cave Generation
Classic cellular automata for organic caves:
```python
import random
def generate_cave(grid, fill_probability=0.45, iterations=5):
"""Generate cave using cellular automata"""
w, h = grid.grid_size
# Initialize with random noise
for x in range(w):
for y in range(h):
if random.random() < fill_probability:
grid.at((x, y)).walkable = False
grid.at((x, y)).tilesprite = 1 # Wall
else:
grid.at((x, y)).walkable = True
grid.at((x, y)).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]:
if dx == 0 and dy == 0:
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
# 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
```
### Smoothing and Refinement
```python
def remove_small_regions(grid, min_size=10):
"""Flood-fill to remove disconnected small regions"""
w, h = grid.grid_size
visited = set()
def flood_fill(x, y):
"""Returns size of connected region"""
stack = [(x, y)]
region = []
while stack:
cx, cy = stack.pop()
if (cx, cy) in visited:
continue
if cx < 0 or cx >= w or cy < 0 or cy >= h:
continue
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:
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:
for x, y in region:
grid.at((x, y)).walkable = False
grid.at((x, y)).tilesprite = 1
```
---
## Noise-Based Terrain
### Perlin/Simplex Noise
Generate heightmaps for terrain:
```python
# Requires noise library: pip install noise
import noise
def generate_terrain(grid, scale=0.1, octaves=4):
"""Generate terrain using Perlin noise"""
w, h = grid.grid_size
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)
# 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
cell = grid.at((x, y))
cell.tilesprite = tile
cell.walkable = walkable
```
---
## Populating Generated Worlds
### Entity Placement
Place entities in generated content:
```python
def place_entities(grid, rooms, entity_density=0.05):
"""Place enemies in rooms"""
for room in rooms:
room_area = room.w * room.h
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)
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
room = random.choice(rooms)
x = random.randint(room.x, room.x + room.w - 1)
y = random.randint(room.y, room.y + room.h - 1)
if grid.at((x, y)).walkable:
item = mcrfpy.Entity(
grid_pos=(x, y),
sprite_index=random.randint(20, 25)
)
grid.entities.append(item)
placed += 1
attempts += 1
```
---
## Large World Generation
### Chunk-Based Generation
For worlds larger than memory allows:
```python
class ChunkManager:
"""Generate chunks on-demand"""
def __init__(self, chunk_size=256):
self.chunk_size = chunk_size
self.loaded_chunks = {} # (cx, cy) -> Grid
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)
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))
for key in to_unload:
del self.loaded_chunks[key]
```
**Issue #123** and **Issue #67** track official subgrid and chunk stitching support.
---
## Performance Considerations
### Batch Grid Updates
Set many cells efficiently:
```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)
# FASTER: Minimize Python/C++ crossings
tiles = []
for x in range(100):
for y in range(100):
tiles.append(compute_tile(x, y))
# 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]
```
### Generation Timing
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
```
---
## Examples and Demos
### Complete Dungeon Generator
```python
import mcrfpy
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
rooms = generate_bsp_dungeon(grid, num_splits=4)
# 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
first_room = rooms[0]
player = mcrfpy.Entity(
grid_pos=(first_room.x + first_room.w // 2,
first_room.y + first_room.h // 2),
sprite_index=1
)
grid.entities.append(player)
# 6. Place enemies
place_entities(grid, rooms[1:], entity_density=0.05)
# 7. Place exit in last room
last_room = rooms[-1]
exit_entity = mcrfpy.Entity(
grid_pos=(last_room.x + last_room.w // 2,
last_room.y + last_room.h // 2),
sprite_index=99
)
grid.entities.append(exit_entity)
return grid, player
# Use it
mcrfpy.createScene("game")
mcrfpy.setScene("game")
grid, player = create_procedural_dungeon()
ui = mcrfpy.sceneUI("game")
ui.append(grid)
```
See `src/scripts/cos_level.py` and `src/scripts/cos_tiles.py` for production examples from "Crypt of Sokoban."
---
## API Reference
See [`docs/api_reference_dynamic.html`](../../src/branch/master/docs/api_reference_dynamic.html) for Grid and Entity APIs.
**Key Grid Methods:**
- `grid.at((x, y))` - Get cell for modification
- `grid.grid_size` - Get dimensions
- `grid.entities` - Entity collection
**Key Cell Properties:**
- `cell.tilesprite` - Tile index
- `cell.walkable` - Pathfinding flag
- `cell.color` - RGBA tint
---
**Navigation:**
- [[Home]] - Documentation hub
- [[Grid-System]] - Spatial container details
- [[Entity-Management]] - Populating worlds with entities
- [[AI-and-Pathfinding]] - Navigation in generated worlds