"""Validate default spawn candidates per 3x3 room of LevelTotal. Reads tiles[] from src/Apps/LightGame/src/levels/LevelTotalData.h, then for each candidate (col,row,tile_x,tile_y) checks: - tile at (tx,ty) and (tx,ty+1)/(tx,ty-1) per collider (8,0)->(24,32) is non-solid - tile at (tx, ty+1) (the tile directly under feet at world_y=tile_y*32+32) is solid Player position used by spawn check (LightGameApp.cpp:181): pos = (tile_x*32, tile_y*32) -- collider min=(8,0), max=(24,32) world_box = pos + collider = (tx*32+8, ty*32) -> (tx*32+24, ty*32+32) is_grounded checks tiles below feet; collider rows tile_top..tile_bottom-1 must be non-solid. Solid ids: 5,6,7,8,9,10,11. Deadly: 12, 22. """ import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "src/Apps/LightGame/src/levels/LevelTotalData.h" text = SRC.read_text(encoding="utf-8") # Parse tiles array m = re.search(r"static const uint16_t tiles\[\]\s*=\s*\{([^}]*)\};", text, re.DOTALL) nums = [int(x) for x in re.findall(r"-?\d+", m.group(1))] W, H = 96, 54 assert len(nums) == W * H, len(nums) tiles = [nums[y * W:(y + 1) * W] for y in range(H)] SOLID = {5, 6, 7, 8, 9, 10, 11} DEADLY = {12, 22} def at(tx, ty): if tx < 0 or tx >= W or ty < 0 or ty >= H: return -1 return tiles[ty][tx] def collider_overlaps_solid(tx, ty): # world_box = (tx*32+8, ty*32) -> (tx*32+24, ty*32+32) # tile rows: ty .. (ty*32+32-1)//32 = ty (since 32+32-1=63, //32=1, so rows ty..ty+1-1 = ty) # Actually max.y - 1 = ty*32 + 31, //32 = ty. So only row ty. # tile cols: (tx*32+8)//32 = tx, (tx*32+24-1)//32 = tx. Only col tx. return at(tx, ty) in SOLID or at(tx, ty) in DEADLY def has_solid_below(tx, ty): # Player feet at world_y = ty*32 + 32. is_grounded probes 1px below = ty*32+33. # Tile row = (ty*32+32)//32 = ty+1. return at(tx, ty + 1) in SOLID def safe_above_head(tx, ty): # don't have ceiling spike just touching return at(tx, ty - 1) not in DEADLY def candidate_score(tx, ty): if collider_overlaps_solid(tx, ty): return None, "overlap solid/deadly at body row" if not has_solid_below(tx, ty): return None, "not grounded" if not safe_above_head(tx, ty): return None, "deadly above head" return True, "ok" # Room grid: cols 0/32/64, rows 0/18/36; size 32x18 def find_safe_in_room(rcol, rrow, prefer=None): tx0, ty0 = rcol * 32, rrow * 18 tx1, ty1 = tx0 + 32, ty0 + 18 candidates = [] if prefer: ptx, pty = prefer ok, why = candidate_score(ptx, pty) if ok: return (ptx, pty, "preferred") else: print(f" preferred {(ptx,pty)} rejected: {why}") # Scan center outward cx, cy = (tx0 + tx1) // 2, (ty0 + ty1) // 2 for ty in range(ty0, ty1): for tx in range(tx0, tx1): ok, why = candidate_score(tx, ty) if ok: d = (tx - cx) ** 2 + (ty - cy) ** 2 candidates.append((d, tx, ty)) if not candidates: return None candidates.sort() _, tx, ty = candidates[0] return (tx, ty, "auto") # Center room must reuse current spawn (1184, 928) = tile (37, 29) preferences = { (1, 1): (37, 29), } print(f"{'Room':<8} {'tile':<10} {'pixel':<14} note") print("-" * 50) for rrow in range(3): for rcol in range(3): prefer = preferences.get((rcol, rrow)) result = find_safe_in_room(rcol, rrow, prefer=prefer) if result is None: print(f"({rcol},{rrow}) NO SAFE SPAWN FOUND") continue tx, ty, kind = result # Spawn pos in pixels = (tx*32, ty*32). Verify under feet: below = at(tx, ty + 1) print(f"({rcol},{rrow}) ({tx:3d},{ty:3d}) ({tx*32:5d},{ty*32:4d}) below={below} ({kind})")