diff --git a/tests/demo/screens/pathfinding_demo.py b/tests/demo/screens/pathfinding_demo.py new file mode 100644 index 0000000..adfce1a --- /dev/null +++ b/tests/demo/screens/pathfinding_demo.py @@ -0,0 +1,290 @@ +"""pathfinding_demo.py - Visual demo of the #315 pathfinding primitives. + +Three panels side-by-side on one scene: + [Panel 1] A* with selectable heuristic. Keys 1-5 cycle EUCLIDEAN, MANHATTAN, + CHEBYSHEV, DIAGONAL, ZERO. Q/W bump the weight by 0.25. + [Panel 2] Dijkstra flood from a cursor-controlled root. Arrow keys move the + cursor; the distance field re-renders as a blue gradient. + [Panel 3] Multi-root FLEE: three "guard" entities flee from a shared set of + threats, using an inverted multi-root Dijkstra map. Animated one + step per frame tick. Press T to drop a new threat on the panel. + +Also exercises: DijkstraMap.invert(), DijkstraMap.descent_step(), +mcrfpy.Heuristic, Grid.find_path(heuristic=, weight=), Grid.get_dijkstra_map( +roots=...). +""" +import mcrfpy +import sys + + +GRID_W, GRID_H = 20, 20 +CELL_PX = 14 +PANEL_W_PX = GRID_W * CELL_PX +GAP = 20 + +SCREEN_W = 3 * PANEL_W_PX + 4 * GAP +SCREEN_H = GRID_H * CELL_PX + 140 + + +scene = mcrfpy.Scene("pathfinding_demo") + +bg = mcrfpy.Frame(pos=(0, 0), size=(SCREEN_W, SCREEN_H), + fill_color=mcrfpy.Color(18, 18, 26)) +scene.children.append(bg) + +title = mcrfpy.Caption(text="Pathfinding Next-Gen Demo (#315)", pos=(GAP, 10)) +title.fill_color = mcrfpy.Color(240, 240, 255) +scene.children.append(title) + + +def make_open_grid(x_off): + g = mcrfpy.Grid(grid_size=(GRID_W, GRID_H), + pos=(x_off, 50), + size=(PANEL_W_PX, GRID_H * CELL_PX)) + for y in range(GRID_H): + for x in range(GRID_W): + c = g.at(x, y) + wall = (x in (0, GRID_W - 1)) or (y in (0, GRID_H - 1)) + c.walkable = not wall + c.transparent = not wall + # A small wedge of walls in the middle for visual interest. + for y in range(4, 12): + g.at(GRID_W // 2, y).walkable = False + g.at(GRID_W // 2, y).transparent = False + scene.children.append(g) + return g + + +# ============================================================================= +# Panel 1: A* with heuristic switching +# ============================================================================= +g1 = make_open_grid(GAP) +astar_layer = mcrfpy.ColorLayer(z_index=1, name="astar_path") +g1.add_layer(astar_layer) + +HEURISTICS = [ + (mcrfpy.Heuristic.EUCLIDEAN, "EUCLIDEAN"), + (mcrfpy.Heuristic.MANHATTAN, "MANHATTAN"), + (mcrfpy.Heuristic.CHEBYSHEV, "CHEBYSHEV"), + (mcrfpy.Heuristic.DIAGONAL, "DIAGONAL"), + (mcrfpy.Heuristic.ZERO, "ZERO"), +] +state_astar = {"hidx": 0, "weight": 1.0, + "start": (2, 10), "end": (GRID_W - 3, 10)} + +cap_astar = mcrfpy.Caption(text="A* heuristic: EUCLIDEAN weight=1.00", + pos=(GAP, 50 + GRID_H * CELL_PX + 6)) +cap_astar.fill_color = mcrfpy.Color(180, 220, 255) +scene.children.append(cap_astar) + + +def redraw_astar(): + h, hname = HEURISTICS[state_astar["hidx"]] + astar_layer.fill(mcrfpy.Color(0, 0, 0, 0)) + p = g1.find_path(state_astar["start"], state_astar["end"], + heuristic=h, weight=state_astar["weight"]) + n_steps = 0 + if p is not None: + for step in p: + astar_layer.set((int(step.x), int(step.y)), + mcrfpy.Color(255, 220, 80, 220)) + n_steps += 1 + # Start/end markers. + astar_layer.set(state_astar["start"], mcrfpy.Color(80, 255, 120, 255)) + astar_layer.set(state_astar["end"], mcrfpy.Color(255, 90, 90, 255)) + cap_astar.text = (f"A* heuristic: {hname} " + f"weight={state_astar['weight']:.2f} steps={n_steps}") + + +# ============================================================================= +# Panel 2: Dijkstra flood +# ============================================================================= +g2 = make_open_grid(GAP * 2 + PANEL_W_PX) +dij_layer = mcrfpy.ColorLayer(z_index=1, name="dij_flood") +g2.add_layer(dij_layer) + +state_dij = {"root": (GRID_W // 2 - 3, GRID_H // 2)} + +cap_dij = mcrfpy.Caption(text="Dijkstra flood (arrows move root)", + pos=(GAP * 2 + PANEL_W_PX, 50 + GRID_H * CELL_PX + 6)) +cap_dij.fill_color = mcrfpy.Color(180, 255, 220) +scene.children.append(cap_dij) + + +def redraw_dijkstra(): + dij_layer.fill(mcrfpy.Color(0, 0, 0, 0)) + root = state_dij["root"] + if not g2.at(root[0], root[1]).walkable: + cap_dij.text = "Dijkstra root on wall - move with arrows" + return + dmap = g2.get_dijkstra_map(root) + # Sample distances to find the maximum for normalization. + max_dist = 0.0 + for y in range(1, GRID_H - 1): + for x in range(1, GRID_W - 1): + d = dmap.distance((x, y)) + if d is not None and d > max_dist: + max_dist = d + if max_dist <= 0: + return + for y in range(1, GRID_H - 1): + for x in range(1, GRID_W - 1): + d = dmap.distance((x, y)) + if d is None: + continue + t = min(1.0, d / max_dist) + # Cool gradient: near-root = bright, far = dark. + r = int(40 * t + 80 * (1 - t)) + gc = int(160 * (1 - t) + 40 * t) + bc = int(240 * (1 - t) + 80 * t) + dij_layer.set((x, y), mcrfpy.Color(r, gc, bc, 180)) + dij_layer.set(root, mcrfpy.Color(255, 255, 90, 255)) + cap_dij.text = f"Dijkstra flood max={max_dist:.1f} root={root}" + + +# ============================================================================= +# Panel 3: Multi-root FLEE +# ============================================================================= +g3 = make_open_grid(GAP * 3 + 2 * PANEL_W_PX) +flee_layer = mcrfpy.ColorLayer(z_index=1, name="flee_layer") +g3.add_layer(flee_layer) + +state_flee = { + "threats": [(3, 3), (GRID_W - 4, GRID_H - 4)], + "guards": [(10, 6), (10, 10), (10, 14)], + "safety": None, + "threat_map": None, +} + +cap_flee = mcrfpy.Caption(text="Multi-root FLEE (T adds threat, R resets)", + pos=(GAP * 3 + 2 * PANEL_W_PX, + 50 + GRID_H * CELL_PX + 6)) +cap_flee.fill_color = mcrfpy.Color(255, 190, 190) +scene.children.append(cap_flee) + + +def recompute_flee(): + state_flee["threat_map"] = g3.get_dijkstra_map(roots=state_flee["threats"]) + state_flee["safety"] = state_flee["threat_map"].invert() + + +def redraw_flee(): + flee_layer.fill(mcrfpy.Color(0, 0, 0, 0)) + # Threats: red. + for t in state_flee["threats"]: + flee_layer.set(t, mcrfpy.Color(255, 60, 60, 255)) + # Guards: green. + for gd in state_flee["guards"]: + flee_layer.set(gd, mcrfpy.Color(80, 255, 120, 255)) + cap_flee.text = (f"FLEE: {len(state_flee['threats'])} threats " + f"{len(state_flee['guards'])} guards") + + +def step_guards(): + if state_flee["safety"] is None: + return + new_guards = [] + for gd in state_flee["guards"]: + nxt = state_flee["safety"].descent_step(gd) + if nxt is None: + new_guards.append(gd) + else: + candidate = (int(nxt.x), int(nxt.y)) + # Avoid landing on another guard or a threat. + if candidate in new_guards or candidate in state_flee["threats"]: + new_guards.append(gd) + else: + new_guards.append(candidate) + state_flee["guards"] = new_guards + redraw_flee() + + +# ============================================================================= +# Key handling +# ============================================================================= +instructions = [ + "Panel 1 (A*): [1-5] heuristic [Q/W] weight -/+", + "Panel 2 (Dij): [Arrow keys] move root", + "Panel 3 (FLEE): [T] add threat [R] reset (guards auto-step)", + "[ESC] quit", +] +for i, text in enumerate(instructions): + c = mcrfpy.Caption(text=text, pos=(GAP, SCREEN_H - 90 + i * 22)) + c.fill_color = mcrfpy.Color(180, 180, 200) + scene.children.append(c) + + +def on_key(key, state): + if state != mcrfpy.InputState.PRESSED: + return + + # Heuristic switching + for i, digit in enumerate([mcrfpy.Key.Num1, mcrfpy.Key.Num2, mcrfpy.Key.Num3, + mcrfpy.Key.Num4, mcrfpy.Key.Num5]): + if key == digit: + state_astar["hidx"] = i + redraw_astar() + return + + if key == mcrfpy.Key.Q: + state_astar["weight"] = max(0.25, state_astar["weight"] - 0.25) + redraw_astar() + return + if key == mcrfpy.Key.W: + state_astar["weight"] = min(5.0, state_astar["weight"] + 0.25) + redraw_astar() + return + + # Dijkstra root movement + rx, ry = state_dij["root"] + moved = False + if key == mcrfpy.Key.LEFT: + rx = max(1, rx - 1); moved = True + elif key == mcrfpy.Key.RIGHT: + rx = min(GRID_W - 2, rx + 1); moved = True + elif key == mcrfpy.Key.UP: + ry = max(1, ry - 1); moved = True + elif key == mcrfpy.Key.DOWN: + ry = min(GRID_H - 2, ry + 1); moved = True + if moved: + state_dij["root"] = (rx, ry) + redraw_dijkstra() + + # FLEE panel: drop threat at a random walkable cell. + if key == mcrfpy.Key.T: + import random + for _ in range(20): + p = (random.randint(1, GRID_W - 2), random.randint(1, GRID_H - 2)) + if g3.at(p[0], p[1]).walkable and p not in state_flee["threats"]: + state_flee["threats"].append(p) + break + recompute_flee() + redraw_flee() + + if key == mcrfpy.Key.R: + state_flee["threats"] = [(3, 3), (GRID_W - 4, GRID_H - 4)] + state_flee["guards"] = [(10, 6), (10, 10), (10, 14)] + recompute_flee() + redraw_flee() + + if key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + + +scene.on_key = on_key + +# Step FLEE guards on a timer (6 ticks/sec is slow enough to watch). +def tick(timer, runtime): + step_guards() + + +# Initial render +redraw_astar() +redraw_dijkstra() +recompute_flee() +redraw_flee() + +mcrfpy.Timer("flee_tick", tick, 160) +mcrfpy.current_scene = scene + +print("pathfinding_demo loaded - see on-screen instructions.")