Add edge-type Wang path overlay as 3rd layer in tiled demo

Generates a network of bezier-curved dirt paths connecting POIs placed
via grid-distributed random selection. Uses minimum spanning tree with
extra fork edges, noise-offset control points for organic curves, and
the "pathways" edge-type Wang set for directional path tiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-04-01 11:27:33 -04:00
commit b1902a3d8b

View file

@ -1,5 +1,5 @@
# tiled_demo.py - Visual demo of Tiled integration
# Shows premade maps, Wang auto-tiling, and procgen terrain
# Shows premade maps, Wang auto-tiling, procgen terrain, and edge-type path overlay
#
# Usage:
# Headless: cd build && ./mcrogueface --headless --exec ../tests/demo/screens/tiled_demo.py
@ -8,6 +8,8 @@
import mcrfpy
from mcrfpy import automation
import sys
import math
import random
# -- Asset Paths -------------------------------------------------------
PUNY_BASE = "/home/john/Development/7DRL2026_Liber_Noster_jmccardle/assets_sources/PUNY_WORLD_v1/PUNY_WORLD_v1"
@ -202,15 +204,15 @@ scene1.children.append(nav1)
# ======================================================================
# SCREEN 2: Procedural Wang Auto-Tile (2-layer approach)
# SCREEN 2: Procedural Wang Auto-Tile (3-layer: terrain + trees + paths)
# ======================================================================
print("\nSetting up Screen 2: Procgen Wang Terrain (2-layer)...")
print("\nSetting up Screen 2: Procgen Wang Terrain (3-layer)...")
scene2 = mcrfpy.Scene("tiled_procgen")
bg2 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
scene2.children.append(bg2)
title2 = mcrfpy.Caption(text="Procgen Wang Auto-Tile (60x60, 2 layers)", pos=(20, 10))
title2 = mcrfpy.Caption(text="Procgen Wang Auto-Tile (60x60, 3 layers)", pos=(20, 10))
title2.fill_color = mcrfpy.Color(255, 255, 255)
scene2.children.append(title2)
@ -277,15 +279,121 @@ terrain_counts["TREES(overlay)"] = tree_count
print(f" Terrain distribution: {terrain_counts}")
# Create grid with 2 layers and apply Wang auto-tiling
# -- Path overlay: bezier curves with noise-offset control points --
# Uses the "pathways" EDGE-type Wang set for thin directional path tiles
print(" Generating path network...")
pathways_ws = tileset.wang_set("pathways")
PathT = pathways_ws.terrain_enum()
# 1. Find land cells for POI placement
land_terrains = {int(T.GRASS), int(T.SAND), int(T.CLIFF)}
land_cells = []
for y in range(H):
for x in range(W):
if base_dm.get(x, y) in land_terrains:
land_cells.append((x, y))
# 2. Place POIs across the map using a grid to ensure good distribution
random.seed(42)
poi_grid_n = 4
zone_w, zone_h = W // poi_grid_n, H // poi_grid_n
pois = []
for gx in range(poi_grid_n):
for gy in range(poi_grid_n):
zone_cells = [(x, y) for x, y in land_cells
if gx * zone_w <= x < (gx + 1) * zone_w
and gy * zone_h <= y < (gy + 1) * zone_h]
if zone_cells:
pois.append(random.choice(zone_cells))
print(f" POIs placed: {len(pois)}")
# 3. Build a minimum spanning tree to connect all POIs, then add extra edges for forks
edges = []
if len(pois) > 1:
connected = {0}
unconnected = set(range(1, len(pois)))
while unconnected:
best_dist = float('inf')
best_pair = None
for c in connected:
for u in unconnected:
dx = pois[c][0] - pois[u][0]
dy = pois[c][1] - pois[u][1]
d = math.hypot(dx, dy)
if d < best_dist:
best_dist = d
best_pair = (c, u)
if best_pair:
edges.append(best_pair)
connected.add(best_pair[1])
unconnected.discard(best_pair[1])
# Add extra edges for interesting forks (non-MST shortcuts)
edge_set = set(edges) | {(b, a) for a, b in edges}
for _ in range(len(pois) // 2):
i = random.randint(0, len(pois) - 1)
j = random.randint(0, len(pois) - 1)
if i != j and (i, j) not in edge_set:
edges.append((i, j))
edge_set.add((i, j))
edge_set.add((j, i))
print(f" Path edges: {len(edges)} ({len(edges) - len(pois) + 1} extra forks)")
# 4. Dig bezier paths with noise-offset control points for organic curves
path_hm = mcrfpy.HeightMap((W, H))
path_hm.fill(1.0)
path_ctrl_noise = mcrfpy.NoiseSource(dimensions=1, seed=7071)
for idx, (i, j) in enumerate(edges):
p0 = pois[i]
p3 = pois[j]
mx = (p0[0] + p3[0]) / 2.0
my = (p0[1] + p3[1]) / 2.0
dx = p3[0] - p0[0]
dy = p3[1] - p0[1]
length = max(math.hypot(dx, dy), 1.0)
# Perpendicular direction for curve offset
nx, ny = -dy / length, dx / length
# Noise-driven offset makes each path curve uniquely
offset1 = path_ctrl_noise.get((float(idx * 2),)) * length * 0.4
offset2 = path_ctrl_noise.get((float(idx * 2 + 1),)) * length * 0.4
# Two control points offset along the perpendicular
# dig_bezier requires integer coordinates (libtcod uses int arrays)
cx1 = int(round(max(0, min(W - 1, (p0[0] + mx) / 2 + nx * offset1))))
cy1 = int(round(max(0, min(H - 1, (p0[1] + my) / 2 + ny * offset1))))
cx2 = int(round(max(0, min(W - 1, (mx + p3[0]) / 2 + nx * offset2))))
cy2 = int(round(max(0, min(H - 1, (my + p3[1]) / 2 + ny * offset2))))
path_hm.dig_bezier(
points=(p0, (cx1, cy1), (cx2, cy2), p3),
start_radius=0.5, end_radius=0.5,
start_height=0.0, end_height=0.0
)
# 5. Convert heightmap to DiscreteMap: low cells = DIRT_PATHS, only on land
path_dm = mcrfpy.DiscreteMap((W, H))
for y in range(H):
for x in range(W):
if path_hm.get(x, y) < 0.5 and base_dm.get(x, y) in land_terrains:
path_dm.set(x, y, int(PathT.DIRT_PATHS))
path_cell_count = path_dm.count(int(PathT.DIRT_PATHS))
print(f" Path cells: {path_cell_count}")
# Create grid with 3 layers and apply Wang auto-tiling
grid2 = mcrfpy.Grid(grid_size=(W, H), pos=(20, 50), size=(520, 520), layers=[])
grid2.fill_color = mcrfpy.Color(30, 30, 50)
base_layer2 = mcrfpy.TileLayer(name="base", z_index=-2, texture=texture)
base_layer2 = mcrfpy.TileLayer(name="base", z_index=-3, texture=texture)
grid2.add_layer(base_layer2)
overworld_ws.apply(base_dm, base_layer2)
overlay_layer2 = mcrfpy.TileLayer(name="trees", z_index=-1, texture=texture)
overlay_layer2 = mcrfpy.TileLayer(name="trees", z_index=-2, texture=texture)
grid2.add_layer(overlay_layer2)
overworld_ws.apply(overlay_dm, overlay_layer2)
@ -295,6 +403,18 @@ for y in range(H):
if overlay_dm.get(x, y) == int(T.AIR):
overlay_layer2.set((x, y), -1)
# 6. Apply pathways edge-type Wang set to path layer
path_layer2 = mcrfpy.TileLayer(name="paths", z_index=-1, texture=texture)
grid2.add_layer(path_layer2)
pathways_ws.apply(path_dm, path_layer2)
# Post-process: only keep tiles on actual path cells (remove neighbor spillover)
# and clear non-path cells to transparent
for y in range(H):
for x in range(W):
if path_dm.get(x, y) != int(PathT.DIRT_PATHS):
path_layer2.set((x, y), -1)
grid2.center = (W * tileset.tile_width // 2, H * tileset.tile_height // 2)
scene2.children.append(grid2)
@ -302,7 +422,7 @@ scene2.children.append(grid2)
info_lines = [
"Iterative terrain expansion",
f"Seed: 42 (base), 999 (trees)",
f"Grid: {W}x{H}, 2 layers",
f"Grid: {W}x{H}, 3 layers",
"",
"Base (3 passes):",
]
@ -314,6 +434,11 @@ info_lines.append("")
info_lines.append("Tree Overlay:")
info_lines.append(f" TREES: {tree_count}")
info_lines.append(f" reverted: {overlay_reverted}")
info_lines.append("")
info_lines.append("Path Overlay (edge Wang):")
info_lines.append(f" POIs: {len(pois)}")
info_lines.append(f" Edges: {len(edges)}")
info_lines.append(f" Path cells: {path_cell_count}")
make_info_panel(scene2, info_lines)