Advanced Novel PathFinder

Paste ID: fed124c8

Created at: 2026-06-21 06:22:20

gaming math maze path finding

Content

"""
Deep Observation Maze Solver – with GIF Export
Records the entire solving sequence and exports it as an animated GIF.
"""

import numpy as np
import tkinter as tk
from tkinter import ttk, messagebox
import heapq
import random
import time
from PIL import Image, ImageGrab
import imageio.v2 as imageio
import os

# ------------------------------------------------------------------------------
# Maze Generator
# ------------------------------------------------------------------------------

class MazeGenerator:
    def __init__(self, width=21, height=21):
        self.width = width if width % 2 == 1 else width + 1
        self.height = height if height % 2 == 1 else height + 1
        self.grid = None
        self.start = (1, 1)
        self.goal = (self.width - 2, self.height - 2)

    def generate(self):
        self.grid = np.zeros((self.height, self.width), dtype=int)
        stack = [(1, 1)]
        self.grid[1, 1] = 1
        while stack:
            x, y = stack[-1]
            neighbors = []
            for dx, dy in [(2,0), (-2,0), (0,2), (0,-2)]:
                nx, ny = x+dx, y+dy
                if 0 <= nx < self.width and 0 <= ny < self.height and self.grid[ny, nx] == 0:
                    neighbors.append((nx, ny, dx, dy))
            if neighbors:
                nx, ny, dx, dy = random.choice(neighbors)
                self.grid[y + dy//2, x + dx//2] = 1
                self.grid[ny, nx] = 1
                stack.append((nx, ny))
            else:
                stack.pop()
        self.grid[self.start[1], self.start[0]] = 1
        self.grid[self.goal[1], self.goal[0]] = 1

    def is_free(self, x, y):
        if x < 0 or y < 0 or x >= self.width or y >= self.height:
            return False
        return self.grid[y, x] == 1

    def get_free_neighbors(self, x, y):
        neighbors = []
        for dx, dy in [(1,0),(-1,0),(0,1),(0,-1)]:
            nx, ny = x+dx, y+dy
            if self.is_free(nx, ny):
                neighbors.append((nx, ny))
        return neighbors

# ------------------------------------------------------------------------------
# HierarchicalPathfinder
# ------------------------------------------------------------------------------

class HierarchicalPathfinder:
    def __init__(self, maze, agent_pos=None, num_layers=3):
        self.maze = maze
        if agent_pos is None:
            agent_pos = maze.start
        self.agent_pos = agent_pos
        self.num_layers = num_layers
        self.radii = [8, 4, 2]
        self.noise_vars = [0.2, 0.1, 0.05]
        self.belief_maps = [np.ones((maze.height, maze.width)) * 0.5 for _ in range(num_layers)]
        self.true_occupancy = maze.grid.copy()
        self.visited = set()
        self.visited.add(agent_pos)

    def observe(self):
        x, y = self.agent_pos
        observations = []
        for layer_idx in range(self.num_layers):
            radius = self.radii[layer_idx]
            noise_var = self.noise_vars[layer_idx]
            obs_map = np.ones((self.maze.height, self.maze.width)) * 0.5
            for dy in range(-radius, radius+1):
                for dx in range(-radius, radius+1):
                    nx, ny = x+dx, y+dy
                    if 0 <= nx < self.maze.width and 0 <= ny < self.maze.height:
                        true_occ = self.true_occupancy[ny, nx]
                        true_prob = 1.0 if true_occ == 1 else 0.0
                        obs = true_prob + np.random.normal(0, np.sqrt(noise_var))
                        obs = np.clip(obs, 0.0, 1.0)
                        obs_map[ny, nx] = obs
            observations.append(obs_map)
        return observations

    def upward_pass(self, observations):
        for i in range(self.num_layers):
            if i == 0:
                prior_var = self.noise_vars[i] * 0.5
                obs_var = self.noise_vars[i]
                posterior = (prior_var * observations[i] + obs_var * self.belief_maps[i]) / (prior_var + obs_var)
                self.belief_maps[i] = posterior
            else:
                prior = self.belief_maps[i-1]
                obs = observations[i]
                prior_var = self.noise_vars[i-1] * 0.5
                obs_var = self.noise_vars[i]
                posterior = (prior_var * obs + obs_var * prior) / (prior_var + obs_var)
                self.belief_maps[i] = posterior

    def downward_pass(self):
        for i in range(self.num_layers-2, -1, -1):
            prior = self.belief_maps[i+1]
            current = self.belief_maps[i]
            alpha = 0.3
            self.belief_maps[i] = alpha * prior + (1 - alpha) * current

    def step(self, iterations=1):
        obs = self.observe()
        for _ in range(iterations):
            self.upward_pass(obs)
            self.downward_pass()

    def get_fused_map(self):
        return self.belief_maps[0]

    def get_uncertainty_map(self):
        fused = self.get_fused_map()
        return fused * (1 - fused)

    def plan_path(self, start=None, goal=None, threshold=0.45):
        if start is None:
            start = self.agent_pos
        if goal is None:
            goal = self.maze.goal
        fused = self.get_fused_map()
        obstacle_map = (fused < threshold).astype(int)
        def heuristic(a, b):
            return abs(a[0]-b[0]) + abs(a[1]-b[1])
        open_set = []
        heapq.heappush(open_set, (0, start))
        came_from = {}
        g_score = {start: 0}
        f_score = {start: heuristic(start, goal)}
        visited_set = set()
        while open_set:
            current = heapq.heappop(open_set)[1]
            if current == goal:
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                path.append(start)
                path.reverse()
                return path
            if current in visited_set:
                continue
            visited_set.add(current)
            x, y = current
            for dx, dy in [(1,0),(-1,0),(0,1),(0,-1)]:
                nx, ny = x+dx, y+dy
                if 0 <= nx < self.maze.width and 0 <= ny < self.maze.height:
                    if obstacle_map[ny, nx] == 1:
                        continue
                    neighbor = (nx, ny)
                    tentative_g = g_score[current] + 1
                    if neighbor not in g_score or tentative_g < g_score[neighbor]:
                        came_from[neighbor] = current
                        g_score[neighbor] = tentative_g
                        f_score[neighbor] = tentative_g + heuristic(neighbor, goal)
                        heapq.heappush(open_set, (f_score[neighbor], neighbor))
        return None

    def move_agent(self, new_pos):
        self.agent_pos = new_pos
        self.visited.add(new_pos)

    def explore(self):
        x, y = self.agent_pos
        neighbors = self.maze.get_free_neighbors(x, y)
        if not neighbors:
            return False
        uncertainty = self.get_uncertainty_map()
        unvisited = [n for n in neighbors if n not in self.visited]
        if unvisited:
            candidates = unvisited
        else:
            candidates = neighbors
        best = max(candidates, key=lambda p: uncertainty[p[1], p[0]] + random.uniform(-0.01, 0.01))
        self.agent_pos = best
        self.visited.add(best)
        return True

# ------------------------------------------------------------------------------
# GUI Application
# ------------------------------------------------------------------------------

class MazeSolverGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Deep Observation Maze Solver – with GIF Export")
        self.maze = MazeGenerator(21, 21)
        self.maze.generate()
        self.agent_pos = self.maze.start
        self.goal = self.maze.goal
        self.pathfinder = HierarchicalPathfinder(self.maze, self.agent_pos, num_layers=3)
        self.cell_size = 20
        self.canvas_width = self.maze.width * self.cell_size
        self.canvas_height = self.maze.height * self.cell_size
        self.steps = 0
        self.create_widgets()
        self.update_display()

    def create_widgets(self):
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.grid(row=0, column=0, sticky="nsew")
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

        canvas_frame = ttk.LabelFrame(main_frame, text="Maze & Belief", padding=5)
        canvas_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
        self.canvas = tk.Canvas(canvas_frame, width=self.canvas_width, height=self.canvas_height, bg='#1e2a3a')
        self.canvas.pack(fill=tk.BOTH, expand=True)

        control_frame = ttk.LabelFrame(main_frame, text="Controls", padding=5)
        control_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5)

        ttk.Label(control_frame, text="Move Agent:").grid(row=0, column=0, columnspan=2)
        move_buttons = ttk.Frame(control_frame)
        move_buttons.grid(row=1, column=0, columnspan=2, pady=5)
        ttk.Button(move_buttons, text="?", command=lambda: self.move(0,-1)).grid(row=0, column=1)
        ttk.Button(move_buttons, text="?", command=lambda: self.move(-1,0)).grid(row=1, column=0)
        ttk.Button(move_buttons, text="?", command=lambda: self.move(1,0)).grid(row=1, column=2)
        ttk.Button(move_buttons, text="?", command=lambda: self.move(0,1)).grid(row=2, column=1)

        ttk.Button(control_frame, text="Observe & Update", command=self.observe_step).grid(row=2, column=0, columnspan=2, pady=5)
        ttk.Button(control_frame, text="Plan Path (A*)", command=self.plan_path).grid(row=3, column=0, columnspan=2, pady=5)
        ttk.Button(control_frame, text="Step Solve", command=self.step_solve).grid(row=4, column=0, columnspan=2, pady=5)
        ttk.Button(control_frame, text="Auto Solve", command=self.auto_solve).grid(row=5, column=0, columnspan=2, pady=5)
        ttk.Button(control_frame, text="Export GIF", command=self.export_gif).grid(row=6, column=0, columnspan=2, pady=5)
        ttk.Button(control_frame, text="Reset", command=self.reset).grid(row=7, column=0, columnspan=2, pady=5)

        info_frame = ttk.LabelFrame(control_frame, text="Status", padding=5)
        info_frame.grid(row=8, column=0, columnspan=2, sticky="nsew", pady=10)
        self.info_text = tk.Text(info_frame, height=12, width=30, bg='#0a0f1a', fg='#b0c6ff', font=('Courier', 9))
        self.info_text.pack(fill=tk.BOTH, expand=True)

        main_frame.columnconfigure(0, weight=2)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(0, weight=1)

    def move(self, dx, dy):
        x, y = self.agent_pos
        nx, ny = x+dx, y+dy
        if self.maze.is_free(nx, ny):
            self.agent_pos = (nx, ny)
            self.pathfinder.move_agent(self.agent_pos)
            self.update_display()

    def observe_step(self):
        self.pathfinder.step(iterations=1)
        self.update_display()

    def plan_path(self):
        path = self.pathfinder.plan_path(start=self.agent_pos, goal=self.goal, threshold=0.45)
        if path is None:
            messagebox.showinfo("Path", "No path found with current belief!")
        else:
            self.draw_path(path, color='white')
            self.info_text.insert(tk.END, f"\nA* path length: {len(path)} steps.")
            self.info_text.see(tk.END)

    def step_solve(self):
        path = self.pathfinder.plan_path(start=self.agent_pos, goal=self.goal, threshold=0.45)
        if path is not None and len(path) >= 2:
            next_pos = path[1]
            self.agent_pos = next_pos
            self.pathfinder.move_agent(next_pos)
            self.pathfinder.step(iterations=1)
            self.steps += 1
        else:
            moved = self.pathfinder.explore()
            if moved:
                self.agent_pos = self.pathfinder.agent_pos
                self.pathfinder.step(iterations=1)
                self.steps += 1
                self.info_text.insert(tk.END, f"Explored to {self.agent_pos}\n")
            else:
                self.info_text.insert(tk.END, "Stuck: no free neighbor!\n")
        self.update_display()
        if self.agent_pos == self.goal:
            self.info_text.insert(tk.END, f"? Goal reached in {self.steps} steps!\n")
        else:
            self.info_text.insert(tk.END, f"Step {self.steps}: at {self.agent_pos}\n")
        self.info_text.see(tk.END)

    def auto_solve(self):
        self.info_text.insert(tk.END, "\nAuto-solving with exploration...\n")
        self.info_text.see(tk.END)
        while self.agent_pos != self.goal:
            path = self.pathfinder.plan_path(start=self.agent_pos, goal=self.goal, threshold=0.45)
            if path is not None and len(path) >= 2:
                next_pos = path[1]
                self.agent_pos = next_pos
                self.pathfinder.move_agent(next_pos)
                self.pathfinder.step(iterations=1)
                self.steps += 1
            else:
                moved = self.pathfinder.explore()
                if moved:
                    self.agent_pos = self.pathfinder.agent_pos
                    self.pathfinder.step(iterations=1)
                    self.steps += 1
                else:
                    self.info_text.insert(tk.END, "Stuck! Cannot move.\n")
                    break
            self.update_display()
            self.root.update()
            time.sleep(0.05)
        if self.agent_pos == self.goal:
            self.info_text.insert(tk.END, f"? Goal reached in {self.steps} steps!\n")
        else:
            self.info_text.insert(tk.END, "Failed to reach goal.\n")
        self.info_text.see(tk.END)

    def export_gif(self):
        """Run auto_solve and record each frame to a GIF."""
        # Ensure we start from a fresh maze
        self.reset()
        # Record frames
        frames = []
        # We'll simulate stepping and capture canvas after each move
        while self.agent_pos != self.goal:
            path = self.pathfinder.plan_path(start=self.agent_pos, goal=self.goal, threshold=0.45)
            if path is not None and len(path) >= 2:
                next_pos = path[1]
                self.agent_pos = next_pos
                self.pathfinder.move_agent(next_pos)
                self.pathfinder.step(iterations=1)
                self.steps += 1
            else:
                moved = self.pathfinder.explore()
                if moved:
                    self.agent_pos = self.pathfinder.agent_pos
                    self.pathfinder.step(iterations=1)
                    self.steps += 1
                else:
                    break
            # Update canvas
            self.update_display()
            self.root.update()
            # Capture canvas as image
            x = self.canvas.winfo_rootx()
            y = self.canvas.winfo_rooty()
            w = self.canvas.winfo_width()
            h = self.canvas.winfo_height()
            # Grab the canvas area
            img = ImageGrab.grab(bbox=(x, y, x+w, y+h))
            frames.append(img)
            # Small delay for visibility
            time.sleep(0.05)
        # Save GIF
        if frames:
            gif_path = "maze_solve.gif"
            imageio.mimsave(gif_path, frames, format='GIF', duration=0.2)
            self.info_text.insert(tk.END, f"\nGIF saved as {gif_path}\n")
        else:
            self.info_text.insert(tk.END, "\nNo frames captured.\n")
        self.info_text.see(tk.END)

    def reset(self):
        self.maze = MazeGenerator(21, 21)
        self.maze.generate()
        self.agent_pos = self.maze.start
        self.goal = self.maze.goal
        self.pathfinder = HierarchicalPathfinder(self.maze, self.agent_pos, num_layers=3)
        self.steps = 0
        self.update_display()
        self.info_text.delete(1.0, tk.END)
        self.info_text.insert(tk.END, "Reset. New maze.\n")

    def draw_grid(self, map_data, offset=(0,0), cell_size=None):
        if cell_size is None:
            cell_size = self.cell_size
        ox, oy = offset
        h, w = map_data.shape
        for y in range(h):
            for x in range(w):
                val = map_data[y, x]
                if val < 0.4:
                    color = '#cc3333'
                elif val > 0.6:
                    color = '#33cc33'
                else:
                    color = '#666688'
                self.canvas.create_rectangle(ox + x*cell_size, oy + y*cell_size,
                                             ox + (x+1)*cell_size, oy + (y+1)*cell_size,
                                             fill=color, outline='')

    def draw_agent(self):
        x, y = self.agent_pos
        cx = x*self.cell_size + self.cell_size//2
        cy = y*self.cell_size + self.cell_size//2
        r = self.cell_size//3
        self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='yellow', outline='white', width=2)

    def draw_goal(self):
        x, y = self.goal
        cx = x*self.cell_size + self.cell_size//2
        cy = y*self.cell_size + self.cell_size//2
        r = self.cell_size//3
        self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='cyan', outline='white', width=2)

    def draw_path(self, path, color='white'):
        for px, py in path:
            cx = px*self.cell_size + self.cell_size//2
            cy = py*self.cell_size + self.cell_size//2
            self.canvas.create_oval(cx-2, cy-2, cx+2, cy+2, fill=color, outline=color)

    def update_display(self):
        self.canvas.delete("all")
        fused = self.pathfinder.get_fused_map()
        self.draw_grid(fused, (0,0), self.cell_size)
        self.draw_agent()
        self.draw_goal()
        self.info_text.delete(1.0, tk.END)
        self.info_text.insert(tk.END, f"Agent at: {self.agent_pos}\nGoal at: {self.goal}\n")
        self.info_text.insert(tk.END, f"Steps: {self.steps}\n")
        self.info_text.see(tk.END)

# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------

def main():
    root = tk.Tk()
    app = MazeSolverGUI(root)
    root.mainloop()

if __name__ == "__main__":
    main()

Share this Paste