2025-03-05 20:21:24 -05:00
|
|
|
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
|
|
|
|
2025-03-05 20:21:24 -05:00
|
|
|
class Level:
|
|
|
|
|
def __init__(self, width, height):
|
|
|
|
|
self.width = width
|
|
|
|
|
self.height = height
|
2026-02-14 20:34:14 -05:00
|
|
|
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)]
|
2025-03-05 20:21:24 -05:00
|
|
|
|
|
|
|
|
def reset(self):
|
2026-02-14 20:34:14 -05:00
|
|
|
"""Initialize all cells as walls (unwalkable, opaque)."""
|
2025-03-05 20:21:24 -05:00
|
|
|
for x in range(self.width):
|
|
|
|
|
for y in range(self.height):
|
2026-02-14 20:34:14 -05:00
|
|
|
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
|
|
|
|
2026-02-14 20:34:14 -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)
|
|
|
|
|
|
2026-02-14 20:34:14 -05:00
|
|
|
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:
|
2026-02-14 20:34:14 -05:00
|
|
|
level_plan = list(random.choice(list(level_plan)))
|
|
|
|
|
target_rooms = len(level_plan)
|
2025-03-05 20:21:24 -05:00
|
|
|
|
2026-02-14 20:34:14 -05:00
|
|
|
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
|
2025-03-05 20:21:24 -05:00
|
|
|
|
2026-02-14 20:34:14 -05:00
|
|
|
# 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)
|
2025-03-05 20:21:24 -05:00
|
|
|
possibilities = None
|
|
|
|
|
while possibilities or possibilities is None:
|
|
|
|
|
possibilities = ct.wfc_pass(self.grid, possibilities)
|
|
|
|
|
|
|
|
|
|
return feature_coords
|