McRogueFace/src/scripts/cos_level.py

181 lines
7.1 KiB
Python
Raw Normal View History

import random
import mcrfpy
import cos_tiles as ct
t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
2025-03-08 10:42:17 -05:00
class Level:
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = mcrfpy.Grid(grid_size=(width, height), texture=t,
pos=(10, 5), size=(1014, 700))
self.bsp = None
self.leaves = []
self.room_map = [[None] * height for _ in range(width)]
def reset(self):
"""Initialize all cells as walls (unwalkable, opaque)."""
for x in range(self.width):
for y in range(self.height):
self.grid.at((x, y)).walkable = False
self.grid.at((x, y)).transparent = False
self.grid.at((x, y)).tilesprite = 0
self.room_map = [[None] * self.height for _ in range(self.width)]
def leaf_center(self, leaf):
"""Get center coordinates of a BSP leaf."""
x, y = int(leaf.pos[0]), int(leaf.pos[1])
w, h = int(leaf.size[0]), int(leaf.size[1])
return (x + w // 2, y + h // 2)
2025-03-08 10:42:17 -05:00
def room_coord(self, leaf, margin=0):
"""Get a random walkable coordinate inside a leaf's carved room area."""
x, y = int(leaf.pos[0]), int(leaf.pos[1])
w, h = int(leaf.size[0]), int(leaf.size[1])
# Room interior starts at (x+1, y+1) due to wall carving margin
inner_x = x + 1 + margin
inner_y = y + 1 + margin
inner_max_x = x + w - 2 - margin
inner_max_y = y + h - 2 - margin
if inner_max_x <= inner_x:
inner_x = inner_max_x = x + w // 2
if inner_max_y <= inner_y:
inner_y = inner_max_y = y + h // 2
return (random.randint(inner_x, inner_max_x),
random.randint(inner_y, inner_max_y))
def dig_path(self, start, end):
"""Dig an L-shaped corridor between two points."""
x1, x2 = min(start[0], end[0]), max(start[0], end[0])
y1, y2 = min(start[1], end[1]), max(start[1], end[1])
# Random L-shape corner
2025-03-08 10:42:17 -05:00
tx, ty = (x1, y1) if random.random() >= 0.5 else (x2, y2)
for x in range(x1, x2 + 1):
if 0 <= x < self.width and 0 <= ty < self.height:
self.grid.at((x, ty)).walkable = True
self.grid.at((x, ty)).transparent = True
for y in range(y1, y2 + 1):
if 0 <= tx < self.width and 0 <= y < self.height:
self.grid.at((tx, y)).walkable = True
self.grid.at((tx, y)).transparent = True
def generate(self, level_plan):
"""Generate a level using BSP room placement and corridor digging.
Args:
level_plan: list of tuples, each tuple is the features for one room.
Can also be a set of alternative plans.
Returns:
List of (feature_name, (x, y)) tuples for entity placement.
"""
2025-03-08 10:42:17 -05:00
if type(level_plan) is set:
level_plan = list(random.choice(list(level_plan)))
target_rooms = len(level_plan)
for attempt in range(10):
self.reset()
# 1. Create and split BSP
self.bsp = mcrfpy.BSP(pos=(1, 1),
size=(self.width - 2, self.height - 2))
depth = max(2, target_rooms.bit_length() + 1)
self.bsp.split_recursive(
depth=depth,
min_size=(4, 4),
max_ratio=1.5
)
# 2. Get leaves - retry if not enough
self.leaves = list(self.bsp.leaves())
if len(self.leaves) < target_rooms:
continue
# 3. Carve rooms (1-cell wall margin from leaf edges)
for leaf in self.leaves:
lx, ly = int(leaf.pos[0]), int(leaf.pos[1])
lw, lh = int(leaf.size[0]), int(leaf.size[1])
for cx in range(lx + 1, lx + lw - 1):
for cy in range(ly + 1, ly + lh - 1):
if 0 <= cx < self.width and 0 <= cy < self.height:
self.grid.at((cx, cy)).walkable = True
self.grid.at((cx, cy)).transparent = True
# 4. Build room map (cell -> leaf index)
for i, leaf in enumerate(self.leaves):
lx, ly = int(leaf.pos[0]), int(leaf.pos[1])
lw, lh = int(leaf.size[0]), int(leaf.size[1])
for cx in range(lx, lx + lw):
for cy in range(ly, ly + lh):
if 0 <= cx < self.width and 0 <= cy < self.height:
self.room_map[cx][cy] = i
# 5. Carve corridors using BSP adjacency
adj = self.bsp.adjacency
connected = set()
for i in range(len(adj)):
for j in adj[i]:
edge = (min(i, j), max(i, j))
if edge in connected:
continue
connected.add(edge)
ci = self.leaf_center(self.leaves[i])
cj = self.leaf_center(self.leaves[j])
self.dig_path(ci, cj)
# 6. Place features using level_plan
feature_coords = []
for room_num in range(min(target_rooms, len(self.leaves))):
leaf = self.leaves[room_num]
room_plan = level_plan[room_num]
if type(room_plan) == str:
room_plan = [room_plan]
for f in room_plan:
fcoord = None
for _ in range(100):
fc = self.room_coord(leaf)
if not self.grid.at(fc).walkable:
continue
if fc in [c[1] for c in feature_coords]:
continue
fcoord = fc
break
if fcoord is None:
fcoord = self.leaf_center(leaf)
feature_coords.append((f, fcoord))
# 7. Solvability check
spawn_pos = None
boulder_positions = []
button_positions = []
exit_pos = None
for f, pos in feature_coords:
if f == "spawn":
spawn_pos = pos
elif f == "boulder":
boulder_positions.append(pos)
elif f == "button":
button_positions.append(pos)
elif f == "exit":
exit_pos = pos
if spawn_pos and boulder_positions and button_positions and exit_pos:
from cos_solver import is_solvable
if is_solvable(self.grid, spawn_pos, boulder_positions,
button_positions, exit_pos):
break
print(f"Level attempt {attempt + 1}: unsolvable, retrying...")
else:
break # No puzzle elements to verify
# 8. Tile painting (WFC)
possibilities = None
while possibilities or possibilities is None:
possibilities = ct.wfc_pass(self.grid, possibilities)
return feature_coords