McRogueFace/tests/demo/screens/tiled_demo.py

504 lines
19 KiB
Python
Raw Normal View History

2026-02-06 21:43:03 -05:00
# tiled_demo.py - Visual demo of Tiled integration
# Shows premade maps, Wang auto-tiling, and procgen terrain
#
# Usage:
# Headless: cd build && ./mcrogueface --headless --exec ../tests/demo/screens/tiled_demo.py
# Interactive: cd build && ./mcrogueface --exec ../tests/demo/screens/tiled_demo.py
import mcrfpy
from mcrfpy import automation
import sys
# -- Asset Paths -------------------------------------------------------
PUNY_BASE = "/home/john/Development/7DRL2026_Liber_Noster_jmccardle/assets_sources/PUNY_WORLD_v1/PUNY_WORLD_v1"
TSX_PATH = PUNY_BASE + "/Tiled/punyworld-overworld-tiles.tsx"
# -- Load Shared Assets ------------------------------------------------
print("Loading Puny World tileset...")
tileset = mcrfpy.TileSetFile(TSX_PATH)
texture = tileset.to_texture()
overworld_ws = tileset.wang_set("overworld")
Terrain = overworld_ws.terrain_enum()
print(f" Tileset: {tileset.name}")
print(f" Tiles: {tileset.tile_count} ({tileset.columns} cols, {tileset.tile_width}x{tileset.tile_height}px)")
print(f" Wang set: {overworld_ws.name} ({overworld_ws.type}, {overworld_ws.color_count} colors)")
print(f" Terrain enum members: {[t.name for t in Terrain]}")
# -- Helper: Iterative terrain expansion ----------------------------------
def iterative_terrain(hm, wang_set, width, height, passes):
"""Build a DiscreteMap by iteratively splitting terrains outward from
a valid binary map. Each pass splits one terrain on each end of the
chain, validates with wang_set.resolve(), and reverts invalid cells
to their previous value.
hm: HeightMap (normalized 0-1)
passes: list of (threshold, lo_old, lo_new, hi_old, hi_new) tuples.
Each pass says: cells currently == lo_old with height < threshold
become lo_new; cells currently == hi_old with height >= threshold
become hi_new.
Returns (DiscreteMap, stats_dict).
"""
dm = mcrfpy.DiscreteMap((width, height))
# Pass 0: binary split - everything is one of two terrains
p0 = passes[0]
thresh, lo_terrain, hi_terrain = p0
for y in range(height):
for x in range(width):
if hm.get(x, y) < thresh:
dm.set(x, y, int(lo_terrain))
else:
dm.set(x, y, int(hi_terrain))
# Validate pass 0 and fix any invalid cells (rare edge cases like
# checkerboard patterns at the binary boundary)
results = wang_set.resolve(dm)
inv = sum(1 for r in results if r == -1)
if inv > 0:
# Fix by flipping invalid cells to the other terrain
for y in range(height):
for x in range(width):
if results[y * width + x] == -1:
val = dm.get(x, y)
if val == int(lo_terrain):
dm.set(x, y, int(hi_terrain))
else:
dm.set(x, y, int(lo_terrain))
results2 = wang_set.resolve(dm)
inv = sum(1 for r in results2 if r == -1)
stats = {"pass0_invalid": inv}
# Subsequent passes: split outward
for pi, (thresh, lo_old, lo_new, hi_old, hi_new) in enumerate(passes[1:], 1):
# Save current state so we can revert invalid cells
prev = [dm.get(x, y) for y in range(height) for x in range(width)]
# Track which cells were changed this pass
changed = set()
for y in range(height):
for x in range(width):
val = dm.get(x, y)
h = hm.get(x, y)
if val == int(lo_old) and h < thresh:
dm.set(x, y, int(lo_new))
changed.add((x, y))
elif val == int(hi_old) and h >= thresh:
dm.set(x, y, int(hi_new))
changed.add((x, y))
# Iteratively revert changed cells that cause invalid tiles.
# A changed cell should be reverted if:
# - It is itself invalid, OR
# - It is a neighbor of an invalid UN-changed cell (it broke
# a pre-existing valid cell by being placed next to it)
dirs8 = [(-1,-1),(0,-1),(1,-1),(-1,0),(1,0),(-1,1),(0,1),(1,1)]
total_reverted = 0
for revert_round in range(30):
results = wang_set.resolve(dm)
to_revert = set()
for y in range(height):
for x in range(width):
if results[y * width + x] != -1:
continue
if (x, y) in changed:
# This changed cell is invalid - revert it
to_revert.add((x, y))
else:
# Pre-existing cell is now invalid - revert its
# changed neighbors to restore it
for dx, dy in dirs8:
nx, ny = x+dx, y+dy
if (nx, ny) in changed:
to_revert.add((nx, ny))
if not to_revert:
break
for (x, y) in to_revert:
dm.set(x, y, prev[y * width + x])
changed.discard((x, y))
total_reverted += 1
results_final = wang_set.resolve(dm)
remaining = sum(1 for r in results_final if r == -1)
stats[f"pass{pi}_kept"] = len(changed)
stats[f"pass{pi}_reverted"] = total_reverted
stats[f"pass{pi}_remaining"] = remaining
return dm, stats
# -- Helper: Info Panel -------------------------------------------------
def make_info_panel(scene, lines, x=560, y=60, w=220, h=None):
"""Create a semi-transparent info panel with text lines."""
if h is None:
h = len(lines) * 22 + 20
panel = mcrfpy.Frame(pos=(x, y), size=(w, h),
fill_color=mcrfpy.Color(20, 20, 30, 220),
outline_color=mcrfpy.Color(80, 80, 120),
outline=1.5)
scene.children.append(panel)
for i, text in enumerate(lines):
cap = mcrfpy.Caption(text=text, pos=(10, 10 + i * 22))
cap.fill_color = mcrfpy.Color(200, 200, 220)
panel.children.append(cap)
return panel
# ======================================================================
# SCREEN 1: Premade Tiled Map
# ======================================================================
print("\nSetting up Screen 1: Premade Map...")
scene1 = mcrfpy.Scene("tiled_premade")
bg1 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
scene1.children.append(bg1)
title1 = mcrfpy.Caption(text="Premade Tiled Map (50x50, 3 layers)", pos=(20, 10))
title1.fill_color = mcrfpy.Color(255, 255, 255)
scene1.children.append(title1)
# Load samplemap1
tm1 = mcrfpy.TileMapFile(PUNY_BASE + "/Tiled/samplemap1.tmj")
print(f" Map: {tm1.width}x{tm1.height}, layers: {tm1.tile_layer_names}")
grid1 = mcrfpy.Grid(grid_size=(tm1.width, tm1.height),
pos=(20, 50), size=(520, 520), layers=[])
grid1.fill_color = mcrfpy.Color(30, 30, 50)
# Add a tile layer for each map layer, bottom-up z ordering
layer_names_1 = tm1.tile_layer_names
for i, name in enumerate(layer_names_1):
z = -(len(layer_names_1) - i)
layer = mcrfpy.TileLayer(name=name, z_index=z, texture=texture)
grid1.add_layer(layer)
tm1.apply_to_tile_layer(layer, name, tileset_index=0)
print(f" Applied layer '{name}' (z_index={z})")
# Center camera on map center (pixels = tiles * tile_size)
grid1.center = (tm1.width * tileset.tile_width // 2,
tm1.height * tileset.tile_height // 2)
scene1.children.append(grid1)
make_info_panel(scene1, [
f"Tileset: {tileset.name}",
f"Tile size: {tileset.tile_width}x{tileset.tile_height}",
f"Tile count: {tileset.tile_count}",
f"Map size: {tm1.width}x{tm1.height}",
"",
"Layers:",
] + [f" {name}" for name in layer_names_1] + [
"",
"Wang sets:",
f" {overworld_ws.name} ({overworld_ws.type})",
f" pathways (edge)",
])
nav1 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
nav1.fill_color = mcrfpy.Color(120, 120, 150)
scene1.children.append(nav1)
# ======================================================================
# SCREEN 2: Procedural Wang Auto-Tile (2-layer approach)
# ======================================================================
print("\nSetting up Screen 2: Procgen Wang Terrain (2-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.fill_color = mcrfpy.Color(255, 255, 255)
scene2.children.append(title2)
W, H = 60, 60
T = Terrain # shorthand
# Generate terrain heightmap using NoiseSource
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
hm = noise.sample(size=(W, H), mode="fbm", octaves=4, world_size=(4.0, 4.0))
hm.normalize(0.0, 1.0)
# -- Base terrain: iterative expansion from binary map --
# Pass 0: binary split at median -> SEAWATER_LIGHT / SAND
# Pass 1: split outward -> SEAWATER_MEDIUM from LIGHT, GRASS from SAND
# Pass 2: split outward -> SEAWATER_DEEP from MEDIUM, CLIFF from GRASS
base_passes = [
# Pass 0: (threshold, lo_terrain, hi_terrain)
(0.45, T.SEAWATER_LIGHT, T.SAND),
# Pass 1+: (threshold, lo_old, lo_new, hi_old, hi_new)
(0.30, T.SEAWATER_LIGHT, T.SEAWATER_MEDIUM, T.SAND, T.GRASS),
(0.20, T.SEAWATER_MEDIUM, T.SEAWATER_DEEP, T.GRASS, T.CLIFF),
]
base_dm, base_stats = iterative_terrain(hm, overworld_ws, W, H, base_passes)
base_dm.enum_type = T
print(f" Base terrain stats: {base_stats}")
# -- Tree overlay: separate noise, binary TREES/AIR --
tree_noise = mcrfpy.NoiseSource(dimensions=2, seed=999)
tree_hm = tree_noise.sample(size=(W, H), mode="fbm", octaves=3, world_size=(6.0, 6.0))
tree_hm.normalize(0.0, 1.0)
overlay_dm = mcrfpy.DiscreteMap((W, H))
overlay_dm.enum_type = T
for y in range(H):
for x in range(W):
base_val = base_dm.get(x, y)
tree_h = tree_hm.get(x, y)
# Trees only on GRASS, driven by separate noise
if base_val == int(T.GRASS) and tree_h > 0.45:
overlay_dm.set(x, y, int(T.TREES))
else:
overlay_dm.set(x, y, int(T.AIR))
# Validate overlay and revert invalid to AIR
overlay_results = overworld_ws.resolve(overlay_dm)
overlay_reverted = 0
for y in range(H):
for x in range(W):
if overlay_results[y * W + x] == -1:
overlay_dm.set(x, y, int(T.AIR))
overlay_reverted += 1
print(f" Overlay: {overlay_reverted} tree cells reverted to AIR")
# Count terrain distribution
terrain_counts = {}
for t in T:
if t == T.NONE:
continue
c = base_dm.count(int(t))
if c > 0:
terrain_counts[t.name] = c
tree_count = overlay_dm.count(int(T.TREES))
terrain_counts["TREES(overlay)"] = tree_count
print(f" Terrain distribution: {terrain_counts}")
# Create grid with 2 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)
grid2.add_layer(base_layer2)
overworld_ws.apply(base_dm, base_layer2)
overlay_layer2 = mcrfpy.TileLayer(name="trees", z_index=-1, texture=texture)
grid2.add_layer(overlay_layer2)
overworld_ws.apply(overlay_dm, overlay_layer2)
# Post-process overlay: AIR resolves to an opaque tile, set to -1 (transparent)
for y in range(H):
for x in range(W):
if overlay_dm.get(x, y) == int(T.AIR):
overlay_layer2.set((x, y), -1)
grid2.center = (W * tileset.tile_width // 2, H * tileset.tile_height // 2)
scene2.children.append(grid2)
# Info panel
info_lines = [
"Iterative terrain expansion",
f"Seed: 42 (base), 999 (trees)",
f"Grid: {W}x{H}, 2 layers",
"",
"Base (3 passes):",
]
for name in ["SEAWATER_DEEP", "SEAWATER_MEDIUM", "SEAWATER_LIGHT",
"SAND", "GRASS", "CLIFF"]:
count = terrain_counts.get(name, 0)
info_lines.append(f" {name}: {count}")
info_lines.append("")
info_lines.append("Tree Overlay:")
info_lines.append(f" TREES: {tree_count}")
info_lines.append(f" reverted: {overlay_reverted}")
make_info_panel(scene2, info_lines)
nav2 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
nav2.fill_color = mcrfpy.Color(120, 120, 150)
scene2.children.append(nav2)
# ======================================================================
# SCREEN 3: Side-by-Side Comparison
# ======================================================================
print("\nSetting up Screen 3: Side-by-Side...")
scene3 = mcrfpy.Scene("tiled_compare")
bg3 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
scene3.children.append(bg3)
title3 = mcrfpy.Caption(text="Premade vs Procedural", pos=(20, 10))
title3.fill_color = mcrfpy.Color(255, 255, 255)
scene3.children.append(title3)
# Left: Premade map (samplemap2, 30x30)
tm2 = mcrfpy.TileMapFile(PUNY_BASE + "/Tiled/samplemap2.tmj")
print(f" Map2: {tm2.width}x{tm2.height}, layers: {tm2.tile_layer_names}")
left_label = mcrfpy.Caption(text="Premade (samplemap2)", pos=(20, 38))
left_label.fill_color = mcrfpy.Color(180, 220, 255)
scene3.children.append(left_label)
grid_left = mcrfpy.Grid(grid_size=(tm2.width, tm2.height),
pos=(20, 60), size=(380, 380), layers=[])
grid_left.fill_color = mcrfpy.Color(30, 30, 50)
for i, name in enumerate(tm2.tile_layer_names):
z = -(len(tm2.tile_layer_names) - i)
layer = mcrfpy.TileLayer(name=name, z_index=z, texture=texture)
grid_left.add_layer(layer)
tm2.apply_to_tile_layer(layer, name, tileset_index=0)
grid_left.center = (tm2.width * tileset.tile_width // 2,
tm2.height * tileset.tile_height // 2)
scene3.children.append(grid_left)
# Right: Procgen island
right_label = mcrfpy.Caption(text="Procgen Island (2-layer Wang)", pos=(420, 38))
right_label.fill_color = mcrfpy.Color(180, 255, 220)
scene3.children.append(right_label)
IW, IH = 30, 30
island_noise = mcrfpy.NoiseSource(dimensions=2, seed=7777)
island_hm = island_noise.sample(size=(IW, IH), mode="fbm", octaves=3, world_size=(3.0, 3.0))
island_hm.normalize(0.0, 1.0)
# Create island shape: attenuate edges with radial gradient
for y in range(IH):
for x in range(IW):
dx = (x - IW / 2.0) / (IW / 2.0)
dy = (y - IH / 2.0) / (IH / 2.0)
dist = (dx * dx + dy * dy) ** 0.5
falloff = max(0.0, 1.0 - dist * 1.2)
h = island_hm.get(x, y) * falloff
island_hm[x, y] = h
island_hm.normalize(0.0, 1.0)
# Iterative base terrain expansion (same technique as Screen 2)
island_passes_def = [
(0.40, T.SEAWATER_LIGHT, T.SAND),
(0.25, T.SEAWATER_LIGHT, T.SEAWATER_MEDIUM, T.SAND, T.GRASS),
(0.15, T.SEAWATER_MEDIUM, T.SEAWATER_DEEP, T.GRASS, T.CLIFF),
]
island_base_dm, island_stats = iterative_terrain(
island_hm, overworld_ws, IW, IH, island_passes_def)
island_base_dm.enum_type = T
print(f" Island base stats: {island_stats}")
# Tree overlay with separate noise
island_tree_noise = mcrfpy.NoiseSource(dimensions=2, seed=8888)
island_tree_hm = island_tree_noise.sample(
size=(IW, IH), mode="fbm", octaves=3, world_size=(4.0, 4.0))
island_tree_hm.normalize(0.0, 1.0)
island_overlay_dm = mcrfpy.DiscreteMap((IW, IH))
island_overlay_dm.enum_type = T
for y in range(IH):
for x in range(IW):
base_val = island_base_dm.get(x, y)
tree_h = island_tree_hm.get(x, y)
if base_val == int(T.GRASS) and tree_h > 0.50:
island_overlay_dm.set(x, y, int(T.TREES))
else:
island_overlay_dm.set(x, y, int(T.AIR))
# Validate overlay
island_ov_results = overworld_ws.resolve(island_overlay_dm)
for y in range(IH):
for x in range(IW):
if island_ov_results[y * IW + x] == -1:
island_overlay_dm.set(x, y, int(T.AIR))
grid_right = mcrfpy.Grid(grid_size=(IW, IH),
pos=(420, 60), size=(380, 380), layers=[])
grid_right.fill_color = mcrfpy.Color(30, 30, 50)
island_base_layer = mcrfpy.TileLayer(name="island_base", z_index=-2, texture=texture)
grid_right.add_layer(island_base_layer)
overworld_ws.apply(island_base_dm, island_base_layer)
island_overlay_layer = mcrfpy.TileLayer(name="island_trees", z_index=-1, texture=texture)
grid_right.add_layer(island_overlay_layer)
overworld_ws.apply(island_overlay_dm, island_overlay_layer)
# Post-process: make AIR cells transparent
for y in range(IH):
for x in range(IW):
if island_overlay_dm.get(x, y) == int(T.AIR):
island_overlay_layer.set((x, y), -1)
grid_right.center = (IW * tileset.tile_width // 2, IH * tileset.tile_height // 2)
scene3.children.append(grid_right)
# Info for both
make_info_panel(scene3, [
"Left: Premade Map",
f" samplemap2.tmj",
f" {tm2.width}x{tm2.height}, {len(tm2.tile_layer_names)} layers",
"",
"Right: Procgen Island",
f" {IW}x{IH}, seed=7777",
" Iterative terrain expansion",
" 2-layer Wang auto-tile",
"",
"Same tileset, same engine",
"Different workflows",
], x=200, y=460, w=400, h=None)
nav3 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
nav3.fill_color = mcrfpy.Color(120, 120, 150)
scene3.children.append(nav3)
# ======================================================================
# Navigation & Screenshots
# ======================================================================
scenes = [scene1, scene2, scene3]
scene_names = ["premade", "procgen", "compare"]
# Keyboard navigation (all scenes share the same handler)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.NUM_1:
mcrfpy.current_scene = scene1
elif key == mcrfpy.Key.NUM_2:
mcrfpy.current_scene = scene2
elif key == mcrfpy.Key.NUM_3:
mcrfpy.current_scene = scene3
elif key == mcrfpy.Key.ESCAPE:
mcrfpy.exit()
for s in scenes:
s.on_key = on_key
# Detect headless mode and take screenshots synchronously
is_headless = False
try:
win = mcrfpy.Window.get()
is_headless = "headless" in str(win).lower()
except:
is_headless = True
if is_headless:
# Headless: use step() to advance simulation and take screenshots directly
for i, (sc, name) in enumerate(zip(scenes, scene_names)):
mcrfpy.current_scene = sc
# Step a few frames to let the scene render
for _ in range(3):
mcrfpy.step(0.016)
fname = f"tiled_demo_{name}.png"
automation.screenshot(fname)
print(f" Screenshot: {fname}")
print("\nAll screenshots captured. Done!")
sys.exit(0)
else:
# Interactive: start on screen 1
mcrfpy.current_scene = scene1
print("\nTiled Demo ready!")
print("Press [1] [2] [3] to switch screens, [ESC] to quit")