├── requirements.txt ├── .vscode └── launch.json ├── .gitignore ├── README.md └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | ffmpeg 2 | matplotlib 3 | noise 4 | numba 5 | numpy 6 | Pillow 7 | requests 8 | ruff 9 | scikit-image 10 | scipy 11 | tqdm -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: Visualize aStar in Mazes", 6 | "type": "debugpy", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/main.py", 9 | "console": "integratedTerminal", 10 | "justMyCode": false, 11 | "args": [], 12 | "env": {}, 13 | "cwd": "${workspaceFolder}", 14 | "stopOnEntry": false 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | 22 | # Installer logs 23 | pip-log.txt 24 | pip-delete-this-directory.txt 25 | 26 | # Unit test / coverage reports 27 | .tox/ 28 | .coverage 29 | .cache 30 | nosetests.xml 31 | coverage.xml 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Mr Developer 37 | .mr.developer.cfg 38 | .project 39 | .pydevproject 40 | 41 | # Rope 42 | .ropeproject 43 | 44 | # Django stuff: 45 | *.log 46 | *.pot 47 | 48 | # Sphinx documentation 49 | docs/_build/ 50 | 51 | #JE stuff: 52 | db/ 53 | venv/ 54 | *_log 55 | old_logs/ 56 | .python-version 57 | pathfinding_visualization.mp4 58 | pathfinding_visualization.gif 59 | maze_animations 60 | fonts -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visual A\* Pathfinding and Maze Generation in Python 2 | 3 | This project provides a high-performance implementation of the A\* ("A-Star") pathfinding algorithm (based on [this](https://gitlab.com/lockie/cl-astar) Lisp implementation by Andrew Kravchuck) along with various maze generation techniques to showcase how this algorithm works, as well as an advanced animated visualization of pathfinding in these mazes. The mazes are generated using many diverse approaches, each providing a different visual look and feel and also potential challenges for a pathfinding algorithm. The A\* algorithm is designed to efficiently find the shortest path in these mazes, taking into consideration various heuristic functions and neighbor enumerators. 4 | 5 | ## Features 6 | 7 | - **Optimized A\* Pathfinding**: Includes custom priority queue and efficient state handling for both integer and float coordinates. 8 | - **Diverse Maze Generation**: Multiple algorithms for creating complex and varied mazes, including Diffusion-Limited Aggregation (DLA), Game of Life, One-Dimensional Automata, Langton's Ant, Voronoi Diagrams, Fractal Division, Wave Function Collapse, Growing Tree, Terrain-Based, Musicalized, Quantum-Inspired, Artistic, Cellular Automaton, Fourier-Based, and Reaction-Diffusion. 9 | - **Advanced Visualization**: Detailed visual representation of maze generation and pathfinding, including animation of exploration and path discovery. 10 | 11 |
12 | 13 | Demo of it in Action 14 | 15 |
16 | Demo of it in Action (click thumbnail for YouTube video!) 17 |
18 | 19 | ## Pathfinding Implementation 20 | 21 | ### Design Philosophy and Performance 22 | 23 | The A\* algorithm implementation focuses on efficiency and scalability. Key aspects include: 24 | 25 | 1. **Custom Priority Queue**: The priority queue is a fundamental component of the A\* algorithm, used to manage the open set (frontier) of nodes to be explored. In this implementation, the priority queue is optimized for fast insertion and extraction of elements based on their priority values, which represent the estimated total cost (distance traveled + heuristic) to reach the goal. This allows the algorithm to quickly focus on the most promising nodes. 26 | 27 | 2. **Coordinate Encoding**: The system supports both integer and float coordinates, which are encoded efficiently to optimize memory usage and computation. This encoding process involves converting floating-point coordinates into a unique integer representation, ensuring precise and quick decoding. The encoding scheme supports a wide range of values, accommodating both fine-grained precision and large-scale maps. 28 | 29 | 3. **Heuristic Functions**: A variety of heuristic functions are available, including Manhattan, Octile, and Euclidean distance heuristics. Each heuristic offers a different way to estimate the cost to reach the goal from a given node, balancing accuracy with computational efficiency. The choice of heuristic can significantly affect the performance of the A\* algorithm, with more accurate heuristics generally leading to faster pathfinding at the cost of additional computation. 30 | 31 | 4. **Neighbor Enumeration**: The algorithm provides customizable neighbor enumerators that define how neighboring nodes are considered during the pathfinding process. Options include 4-directional, 8-directional, and more complex movement patterns. This flexibility allows the algorithm to handle various types of terrain and movement costs, such as diagonal movement being more expensive than orthogonal movement. 32 | 33 | ### Exact and Heuristic Cost Functions 34 | 35 | - **Exact Cost**: This function calculates the actual cost of moving from one node to another. It can account for various factors, such as the distance between nodes and any penalties associated with certain types of terrain or movement. For instance, moving diagonally may have a higher cost than moving vertically or horizontally. 36 | - **Heuristic Cost**: The heuristic cost is an estimate of the cost to reach the goal from a given node. It serves as a guide to the A\* algorithm, helping it prioritize nodes that are likely closer to the goal. The accuracy and computational cost of the heuristic can vary; a more accurate heuristic may provide better guidance but require more computation. 37 | 38 | ### Maze Generation Methods 39 | 40 | This project includes a rich variety of maze generation algorithms, each creating unique patterns and challenges. Below is a detailed explanation of each method: 41 | 42 | 1. **Diffusion-Limited Aggregation (DLA)**: 43 | - **Description**: DLA is a process that simulates the random motion of particles in a medium until they stick to a surface or to each other, forming aggregates. In this algorithm, particles start from random positions and move randomly until they either stick to an existing structure or fall off the boundary of the defined space. 44 | - **Mechanism**: The algorithm initializes with a few seed particles on the grid. New particles are introduced at random locations and follow a random walk. When a particle encounters an occupied cell (another particle), it sticks to it, thereby growing the aggregate structure. This process results in intricate, tree-like patterns, which can resemble natural formations like snowflakes or mineral deposits. 45 | 46 | 2. **Game of Life**: 47 | - **Description**: Based on Conway's Game of Life, this method uses cellular automata rules to evolve a grid of cells, where each cell can be either alive or dead. The next state of each cell is determined by its current state and the number of alive neighbors it has. 48 | - **Mechanism**: The grid is initialized with a random configuration of alive (1) and dead (0) cells. The state of each cell in the next generation is determined by counting its alive neighbors. Cells with exactly three alive neighbors become alive, while cells with fewer than two or more than three alive neighbors die. This evolution creates dynamic and unpredictable patterns, often resulting in maze-like structures with complex corridors and dead-ends. 49 | 50 | 3. **One-Dimensional Automata**: 51 | - **Description**: This method involves the use of simple rules applied to a single row of cells (1D) which then evolves over time to form a 2D maze pattern. The rule set, often represented as a binary number, dictates the state of a cell based on the states of its neighbors. 52 | - **Mechanism**: A row of cells is initialized randomly. Each cell's state in the next row is determined by its current state and the states of its immediate neighbors, according to a specific rule set (e.g., Rule 30, Rule 110). This process iteratively generates new rows, creating complex patterns that range from simple to highly chaotic, depending on the rule used. 53 | 54 | 4. **Langton's Ant**: 55 | - **Description**: A simple Turing machine that moves on a grid of black and white cells, with its movement rules determined by the color of the cell it encounters. 56 | - **Mechanism**: The ant follows a set of rules: if it encounters a white cell, it turns right, flips the color of the cell to black, and moves forward; if it encounters a black cell, it turns left, flips the color to white, and moves forward. Despite the simplicity, the system exhibits complex behavior, leading to the formation of highways and chaotic regions. Over time, the ant's path can generate intricate and unpredictable patterns. 57 | 58 | 5. **Voronoi Diagram**: 59 | - **Description**: This method divides space into regions based on the distance to a set of seed points, where each region contains all points closer to one seed point than to any other. 60 | - **Mechanism**: Random points are placed on the grid, and the Voronoi diagram is computed by determining the nearest seed point for each grid cell. The edges between different regions are treated as walls, resulting in a maze with polygonal cells. The boundaries between the cells are then refined to form passages, often creating a natural, organic feel to the maze structure. 61 | 62 | 6. **Fractal Division**: 63 | - **Description**: A recursive subdivision method that divides the grid into smaller regions by introducing walls along the division lines. 64 | - **Mechanism**: The algorithm begins by splitting the grid with a wall either horizontally or vertically and then adds an opening in the wall. The process repeats recursively on the resulting subregions. This method, also known as the recursive division algorithm, can produce highly symmetrical and self-similar patterns, where the layout at smaller scales resembles the overall structure. 65 | 66 | 7. **Wave Function Collapse**: 67 | - **Description**: Inspired by the concept of quantum mechanics, this method uses a constraint-based approach to determine the state of each cell based on its neighbors. 68 | - **Mechanism**: The algorithm starts with an undecided grid where each cell can potentially take on multiple states. It then collapses each cell's possibilities based on constraints from neighboring cells, ensuring that the pattern remains consistent and non-contradictory. This method produces highly detailed and aesthetically pleasing mazes, where the structure is consistent with the predefined rules and patterns. 69 | 70 | 8. **Growing Tree**: 71 | - **Description**: A procedural method for creating mazes by expanding paths from a starting point, using a selection strategy to decide which frontier cell to grow from. 72 | - **Mechanism**: The algorithm begins with a single cell and iteratively adds neighboring cells to the maze. The selection strategy can vary (e.g., random, last-in-first-out, first-in-first-out), affecting the overall structure. The growing tree method is flexible and can generate mazes with a variety of appearances, from long corridors to densely packed networks. 73 | 74 | 9. **Terrain-Based**: 75 | - **Description**: This approach uses Perlin noise to generate a terrain-like heightmap, which is then converted into a maze by thresholding. 76 | - **Mechanism**: Perlin noise, a type of gradient noise, is used to create a smooth and continuous terrain heightmap. The grid is then divided into passable and impassable regions based on a threshold value. This method produces mazes that resemble natural landscapes with hills and valleys, offering a different challenge with natural-looking obstacles. 77 | 78 | 10. **Musicalized**: 79 | - **Description**: Inspired by musical compositions, this method generates mazes by interpreting harmonic functions and waves. 80 | - **Mechanism**: The algorithm generates a grid where the value at each cell is determined by the sum of multiple sine waves with different frequencies and amplitudes. The resulting wave patterns are then thresholded to create walls and paths, resembling rhythmic and wave-like structures. This method provides a unique aesthetic, mirroring the periodic nature of music. 81 | 82 | 11. **Quantum-Inspired**: 83 | - **Description**: Mimics quantum interference patterns by superimposing wave functions, creating complex interference patterns. 84 | - **Mechanism**: The algorithm uses a combination of wave functions to create a probability density field. By thresholding this field, the maze walls are determined. The resulting patterns are intricate and delicate, often resembling the complex interference patterns seen in quantum physics experiments. This method offers visually stunning mazes with a high degree of symmetry and complexity. 85 | 86 | 12. **Artistic**: 87 | - **Description**: Utilizes artistic techniques such as brush strokes and splatter effects to create abstract maze patterns. 88 | - **Mechanism**: The algorithm randomly places brush strokes and splatters on a canvas, with each stroke affecting multiple cells on the grid. The placement and orientation of strokes are randomized, creating unique and abstract patterns. This artistic approach results in mazes that mimic various art styles, offering a visually distinct experience. 89 | 90 | 13. **Cellular Automaton**: 91 | - **Description**: Uses custom rules to evolve a grid of cells, with each cell's state influenced by its neighbors. 92 | - **Mechanism**: The grid is initialized with random states. A set of rules determines the next state of each cell based on the states of its neighbors. This process is iterated multiple times, with the specific rules and number of iterations influencing the final pattern. The method can generate a wide range of structures, from highly ordered to chaotic, depending on the chosen ruleset. 93 | 94 | 14. **Fourier-Based**: 95 | - **Description**: Applies the Fourier transform to a noise field, selectively filtering frequencies to create smooth patterns. 96 | - **Mechanism**: The algorithm begins with a random noise field and transforms it into the frequency domain using the Fourier transform. Certain frequency components are then filtered out, and the inverse transform is applied to obtain the spatial domain pattern. The result is a maze with smooth, flowing structures, influenced by the selected frequencies and their combinations. 97 | 98 | 15. **Reaction-Diffusion**: 99 | - **Description**: Simulates chemical reaction and diffusion processes to create organic and biomorphic patterns. 100 | - **Mechanism**: The algorithm models the interaction between two chemical substances that spread out and react with each other. The concentration of these substances evolves over time according to reaction-diffusion equations. The resulting patterns are thresholded to form the maze structure. This method creates mazes with natural, fluid-like structures, similar to those seen in biological organisms and chemical reactions. 101 | 102 | Each method in this collection offers a distinct visual and structural style, making it possible to explore a wide range of maze characteristics and challenges. These mazes are suitable for testing various pathfinding algorithms and for generating visually compelling visualizations. 103 | 104 | ### Maze Validation and Adjustment Techniques 105 | 106 | In maze generation, ensuring that the resulting structures are not only visually appealing but also functionally navigable is critical. Various techniques and methods are employed to validate the generated mazes and modify them if they don't meet specific criteria, such as solvability, complexity, or connectivity. This section details the philosophy, theory, and practical implementations behind these techniques, with a focus on ensuring high-quality maze structures. 107 | 108 | #### Overview 109 | 110 | The approach to maze validation and adjustment involves a multi-step process: 111 | 112 | 1. **Validation**: After generating a maze, we assess it against predefined criteria such as connectivity, solvability, and structural diversity. 113 | 2. **Modification**: If the maze fails to meet these criteria, specific functions are employed to adjust the structure, such as adding or removing walls, creating pathways, or ensuring connectivity between regions. 114 | 3. **Final Verification**: The modified maze is re-evaluated to confirm that it now meets all the desired criteria. 115 | 116 | This process ensures that each maze not only provides a challenging and engaging environment but also maintains a balance between complexity and solvability. 117 | 118 | #### Detailed Function Explanations 119 | 120 | 1. **smart_hole_puncher** 121 | - **Purpose**: To ensure that a generated maze has a path from the start to the goal by strategically removing walls. 122 | - **Mechanism**: The function iteratively selects wall cells and removes them, prioritizing areas where the path might be blocked. It stops once a viable path is found, minimizing changes to the maze's overall structure. This method is particularly useful for complex mazes that may have isolated regions. 123 | 124 | 2. **ensure_connectivity** 125 | - **Purpose**: To guarantee that all open regions in a maze are connected, preventing isolated areas. 126 | - **Mechanism**: This function uses pathfinding algorithms to verify that a continuous path exists between important points (e.g., start and goal). If disconnected regions are found, the function identifies the shortest path between these regions and creates openings to link them, ensuring the maze is fully navigable. 127 | 128 | 3. **add_walls** 129 | - **Purpose**: To increase the complexity of a maze by adding walls, which can create new challenges and alter the maze's navigability. 130 | - **Mechanism**: Additional walls are placed in the maze in a controlled manner to achieve a target wall density. This function randomly selects open cells to convert into walls, balancing between adding challenge and maintaining solvability. 131 | 132 | 4. **remove_walls** 133 | - **Purpose**: To simplify a maze by removing walls, making it less dense and more navigable. 134 | - **Mechanism**: The function selects walls for removal based on the need to decrease wall density to a target percentage. It ensures that the removals do not oversimplify the maze, maintaining a level of challenge and complexity. 135 | 136 | 5. **add_room_separators** 137 | - **Purpose**: To divide large open spaces into smaller, distinct areas, thereby adding structure and complexity. 138 | - **Mechanism**: The function introduces separators or walls within large open areas of the maze. These separators create distinct rooms or sections, which can then be connected or further modified. This technique prevents overly large open areas that can make the maze less challenging. 139 | 140 | 6. **break_up_large_room** 141 | - **Purpose**: To prevent excessively large open spaces that could simplify navigation and reduce the challenge. 142 | - **Mechanism**: The function identifies large rooms in the maze and introduces additional walls to break them into smaller sections. This process involves a careful analysis of the room sizes and a controlled introduction of walls to maintain the balance between openness and complexity. 143 | 144 | 7. **break_up_large_areas** 145 | - **Purpose**: Similar to breaking up large rooms, this function targets large contiguous open areas in the maze, ensuring they are partitioned for increased complexity. 146 | - **Mechanism**: The function identifies large connected areas and introduces walls to create smaller, manageable sections. This helps in preventing navigational ease due to large uninterrupted spaces and ensures a more challenging experience. 147 | 148 | 8. **create_simple_maze** 149 | - **Purpose**: To generate a basic structure or fill in small areas within a larger maze. 150 | - **Mechanism**: This function uses simple algorithms, such as recursive division or random path generation, to create a basic maze structure. It is often used in conjunction with other techniques to fill specific areas or as a foundation that can be modified further. 151 | 152 | 9. **connect_areas** 153 | - **Purpose**: To ensure that all regions of a maze are accessible and interconnected. 154 | - **Mechanism**: The function uses a combination of pathfinding and wall removal to connect distinct regions or areas within a maze. This ensures that no area is isolated, facilitating complete navigation from any starting point. 155 | 156 | 10. **connect_disconnected_areas** 157 | - **Purpose**: Specifically focuses on connecting areas that are entirely isolated from the rest of the maze. 158 | - **Mechanism**: This function identifies completely disconnected regions and creates paths to integrate them into the main maze. It uses algorithms like breadth-first search (BFS) to find the shortest paths for connection, ensuring efficiency and minimal structural change. 159 | 160 | 11. **bresenham_line** 161 | - **Purpose**: To draw straight lines on a grid, typically used for creating direct connections or walls. 162 | - **Mechanism**: The Bresenham's line algorithm is employed to draw straight lines between two points on a grid, ensuring the line is as continuous and close to a true line as possible. This is useful for creating corridors or walls that follow a straight path. 163 | 164 | 12. **validate_and_adjust_maze** 165 | - **Purpose**: To perform a comprehensive check of the maze's structural integrity and navigability, followed by necessary adjustments. 166 | - **Mechanism**: This function validates the maze against criteria such as solvability, wall density, and connectivity. Based on the assessment, it applies various adjustments (like wall addition/removal, area connection) to ensure the maze meets all necessary conditions. It serves as the final quality check before the maze is considered complete. 167 | 168 | 13. **generate_and_validate_maze** 169 | - **Purpose**: To generate a maze using one of the specified algorithms and ensure it meets all criteria for quality and functionality. 170 | - **Mechanism**: This function integrates the entire process of maze generation, validation, and adjustment. It starts with generating a maze, runs validation checks, and applies modifications as needed. If the maze does not meet the criteria, the function can regenerate or further adjust it until all requirements are satisfied. 171 | 172 | ## Visualization 173 | 174 | The visualization component in this project is designed to provide a comprehensive and interactive display of both the maze generation process and the pathfinding algorithms at work. This component uses the `matplotlib` library to create detailed visual representations that highlight the complexities and intricacies of maze structures and pathfinding strategies. Key elements of the visualization include: 175 | 176 | ### Maze Structure 177 | 178 | - **Walls and Floors**: The visualization distinctly represents walls and floors using a two-color scheme. Walls are typically rendered in a dark color (e.g., deep blue or gray), while floors are displayed in a contrasting light color (e.g., white or light gray). This clear differentiation helps users easily identify passable and impassable areas within the maze. 179 | 180 | - **Color Mapping**: The code allows for the customization of wall and floor colors. This is particularly useful for creating visual themes or adjusting the visualization for different viewing conditions (e.g., color blindness). The `LinearSegmentedColormap` from `matplotlib` can be used to define custom gradients for different maze elements. 181 | 182 | ### Pathfinding Progress 183 | 184 | - **Exploration Order**: During the pathfinding process, the visualization dynamically displays the exploration order of the algorithm. This is achieved by coloring explored cells using a gradient that represents the progression of exploration. Lighter shades indicate earlier exploration, while darker shades denote later exploration stages. The use of an exploration colormap helps visualize the pathfinding algorithm's exploration strategy and efficiency. 185 | 186 | - **Path Discovery**: As the algorithm discovers the path from the start to the goal, the visualization highlights the path using a distinct color (e.g., blue or green). The path is typically represented as a continuous line, indicating the sequence of cells that constitute the solution. The visualization updates in real-time, allowing viewers to see how the path evolves as the algorithm progresses. 187 | 188 | - **Markers for Start and Goal Points**: The start and goal points are clearly marked with distinct symbols (e.g., circles or stars) and colors (e.g., green for the start, red for the goal). These markers remain visible throughout the visualization, providing consistent reference points for the viewer. 189 | 190 | ### Customizable Colors 191 | 192 | - **Customization Options**: The visualization component offers extensive customization options for colors, allowing users to adjust the appearance of walls, floors, paths, exploration stages, and start/goal markers. This customization is facilitated through parameters passed to the visualization functions, enabling users to tailor the display to their preferences or specific use cases. 193 | 194 | - **Colormap Selection**: For the exploration and path colors, users can select from predefined colormaps or create custom ones using `LinearSegmentedColormap`. This flexibility ensures that the visualization can be adapted to various aesthetic preferences or accessibility needs. 195 | 196 | - **Transparency and Layering**: The visualization supports transparency and layering effects, particularly for the exploration map. By adjusting the alpha value, users can overlay the exploration progress on top of the maze structure without obscuring the underlying details. This feature is useful for simultaneously visualizing the explored area and the structural layout of the maze. 197 | 198 | ### Animation and Export 199 | 200 | - **Frame Generation**: The visualization is animated by generating frames that capture the state of the maze and pathfinding process at each time step. The code uses concurrent processing to efficiently generate these frames, leveraging multiple CPU cores for faster rendering. Each frame is created by plotting the maze, exploration progress, and current path status. 201 | 202 | - **Animation Playback**: The frames can be compiled into an animation using `FuncAnimation` from `matplotlib.animation`. The playback speed can be adjusted by setting the frames per second (FPS), allowing for slower or faster visualization of the pathfinding process. The animation provides a smooth and continuous representation of the algorithm's operation, from initial exploration to final pathfinding. 203 | 204 | - **Output Formats**: The frames can be saved individually as images or compiled into a video. Each frame is saved as an individual image in the specified `frame_format` (e.g., PNG, JPG). This option is useful for creating high-quality image sequences or for detailed post-processing of individual frames. Alternatively, if `save_as_frames_only` is set to `False`, the frames are compiled into an animation in formats such as MP4. For MP4 exports, the `FFMpegWriter` is used, allowing for fine-tuned control over encoding parameters, such as bitrate and codec settings. This ensures high-quality video output suitable for presentations or further analysis. 205 | 206 | - **Resource Management**: To manage disk space and avoid clutter, the code includes functionality to delete small or temporary files after the animation or frame sequence is saved. This helps maintain a clean working directory and ensures that only the most relevant files are retained. This feature is particularly useful when saving individual frames, as it can help prevent the accumulation of numerous image files. 207 | 208 | ### Assembling Frames into an MP4 File Using FFmpeg 209 | 210 | If you have saved the frames as individual image files and wish to manually assemble them into an MP4 video, you can use FFmpeg. You can download it from the [official FFmpeg website](https://ffmpeg.org/download.html) or install it via a package manager. Alternatively, you can download a pre-compiled binary from the most recent version [here](https://johnvansickle.com/ffmpeg/) (recommended; note that if you do this, you'll have to copy the binary to `/usr/bin/` and do `chmod +x ffmpeg` to make it executable. To check which version of FFmpeg you're actually using, try `which ffmpeg`). Additionally, you may need the `bc` command for calculations, which can be installed using: 211 | 212 | ```bash 213 | sudo apt install bc 214 | ``` 215 | 216 | #### Command Example 217 | 218 | Assuming your frames are named sequentially (e.g., `frame_0001.png`, `frame_0002.png`, etc.) and stored in the current directory, you can use the following command to generate a 30-second video file using x265: 219 | 220 | ```bash 221 | cd /home/ubuntu/visual_astar_python/maze_animations/animation_20240805_114757/ # Change to the directory containing the frames-- this is just an example 222 | ffmpeg -framerate $(echo "($(find . -maxdepth 1 -type f -name 'frame_*.png' | wc -l) + 30 - 1) / 30" | bc) -i frame_%05d.png -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2,scale=3840:2160" -c:v libx265 -preset slow -crf 28 -pix_fmt yuv420p -x265-params "pools=16:bframes=8:ref=4:no-open-gop=1:me=star:rd=4:aq-mode=3:aq-strength=1.0" -movflags +faststart output.mp4 223 | ``` 224 | 225 | For encoding using x264, use: 226 | 227 | ```bash 228 | ffmpeg -framerate $(echo "($(find . -maxdepth 1 -type f -name 'frame_*.png' | wc -l) + 30 - 1) / 30" | bc) -i frame_%05d.png -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" -c:v libx264 -crf 18 -pix_fmt yuv420p -threads 16 -movflags +faststart output_x264.mp4 229 | ``` 230 | 231 | #### Explanation of Options 232 | 233 | - **`-framerate $(...)`**: Calculates the frame rate based on the number of images and desired video duration (30 seconds in this example). This ensures that the video plays for the correct duration regardless of the number of frames. 234 | - **`-i frame_%05d.png`**: Specifies the input file pattern. `%05d` indicates that the input files are sequentially numbered with four digits (e.g., `frame_00001.png`, `frame_00002.png`). 235 | - **`-vf "pad=ceil(iw/2)*2:ceil(ih/2)*2,scale=3840:2160"`**: The `pad` filter ensures the video dimensions are even, which is required for many codecs. The `scale` filter resizes the video to 4K resolution (3840x2160). These filters ensure the output video has compatible dimensions and resolution. 236 | - **`-c:v libx265`**: Specifies the use of the x265 codec for encoding, which provides efficient compression. The x264 variant uses `-c:v libx264` for compatibility and high-quality output. 237 | - **`-preset slow`**: Sets the encoding preset, balancing compression efficiency and encoding time. `slow` is a good compromise for higher compression at a slower speed. 238 | - **`-crf 28`** (for x265) and **`-crf 18`** (for x264): Controls the Constant Rate Factor, affecting the quality and file size. Lower values yield higher quality at the cost of larger file sizes. `crf 28` is suitable for x265, while `crf 18` provides nearly lossless quality for x264. 239 | - **`-pix_fmt yuv420p`**: Sets the pixel format to YUV 4:2:0, ensuring compatibility with most media players and devices. 240 | - **`-x265-params "pools=16:bframes=8:ref=4:no-open-gop=1:me=star:rd=4:aq-mode=3:aq-strength=1.0"`**: Specifies advanced x265 settings to fine-tune the encoding process. These parameters set the number of threads (`pools`), number of B-frames, reference frames, and other encoding settings for optimal quality and compression. 241 | - **`-threads 16`** (for x264): Limits the number of threads used for encoding to 16, balancing performance and resource usage. 242 | - **`-movflags +faststart`**: Enables the `faststart` option, which moves the metadata to the beginning of the file, allowing the video to start playing before it is fully downloaded. This is useful for streaming scenarios. 243 | 244 | These commands and explanations should help you efficiently create high-quality MP4 videos from a sequence of frames using FFmpeg. 245 | 246 | ## Usage 247 | 248 | ### Initial Setup 249 | 250 | Clone the repo and set up a virtual environment with the required packages using (tested on Python 3.12 and Ubuntu 22): 251 | 252 | ```bash 253 | git clone https://github.com/Dicklesworthstone/visual_astar_python.git 254 | cd visual_astar_python 255 | python -m venv venv 256 | source venv/bin/activate 257 | python -m pip install --upgrade pip 258 | python -m pip install wheel 259 | python -m pip install --upgrade setuptools wheel 260 | pip install -r requirements.txt 261 | ``` 262 | 263 | The code is tested with Python 3.12. If you want to use that version without messing with your system Python version, then on Ubuntu you can install and use PyEnv like so: 264 | 265 | ```bash 266 | if ! command -v pyenv &> /dev/null; then 267 | sudo apt-get update 268 | sudo apt-get install -y build-essential libssl-dev zlib1g-dev libbz2-dev \ 269 | libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \ 270 | xz-utils tk-dev libffi-dev liblzma-dev python3-openssl git 271 | 272 | git clone https://github.com/pyenv/pyenv.git ~/.pyenv 273 | echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc 274 | echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc 275 | echo 'eval "$(pyenv init --path)"' >> ~/.zshrc 276 | source ~/.zshrc 277 | fi 278 | cd ~/.pyenv && git pull && cd - 279 | pyenv install 3.12 280 | cd ~ 281 | git clone https://github.com/Dicklesworthstone/visual_astar_python.git 282 | cd visual_astar_python 283 | pyenv local 3.12 284 | python -m venv venv 285 | source venv/bin/activate 286 | python -m pip install --upgrade pip 287 | python -m pip install wheel 288 | python -m pip install --upgrade setuptools wheel 289 | pip install -r requirements.txt 290 | 291 | ``` 292 | 293 | ### Generating and Visualizing Mazes 294 | 295 | To generate and visualize a maze, run the main script with the desired parameters: 296 | 297 | ```bash 298 | python main.py 299 | ``` 300 | 301 | You can see what it looks like running here: 302 | 303 | [![asciicast](https://asciinema.org/a/BwnzShGe1Py5QZJ2IXru7xGMv.svg)](https://asciinema.org/a/BwnzShGe1Py5QZJ2IXru7xGMv) 304 | 305 | ### Customization 306 | 307 | - **Maze Generation Approach**: Specify the desired maze generation approach (e.g., `dla`, `wave_function_collapse`) to customize the type of maze generated. 308 | - **Grid Size**: Set the `GRID_SIZE` parameter to adjust the size of the maze. 309 | - **Visualization Settings**: Modify color schemes and animation settings to suit your preferences. 310 | 311 | ## Parameter Configuration 312 | 313 | This project includes several parameters that users can configure to customize the output of maze generation and pathfinding visualization. Key parameters include: 314 | 315 | - **num_animations**: The number of separate animations to generate, each featuring different mazes and pathfinding scenarios. 316 | - **GRID_SIZE**: The size of the maze grid, determining the number of cells along one dimension. Higher values create more detailed and complex mazes. 317 | - **num_problems**: The number of mazes to display side by side in each animation, allowing for comparison of different generation methods or pathfinding strategies. 318 | - **DPI**: The dots per inch for the animation, affecting image resolution and quality. Higher DPI values yield sharper images. 319 | - **FPS**: Frames per second for animation playback. Higher values create smoother animations but may require more resources. 320 | - **save_as_frames_only**: A boolean parameter indicating whether to save each frame as an individual image. Set `True` to save frames, `False` to compile them into a video. 321 | - **frame_format**: The format for saving frames when `save_as_frames_only` is `True`. Common formats include 'png' and 'jpg'. 322 | - **dark_mode**: Enables a dark theme for visualizations. 323 | - **override_maze_approach**: Forces the use of a specific maze generation approach for consistency across all animations. 324 | 325 | These parameters provide users with extensive control over the behavior and appearance of the generated mazes and visualizations, allowing for fine-tuning according to specific requirements or preferences. 326 | 327 | ## A\* Algorithm: Theoretical Overview and Advanced Implementation 328 | 329 | ### The Big Ideas Behind A\* 330 | 331 | The A\* algorithm is a sophisticated method for finding the shortest path in various environments, whether it's navigating a complex maze or plotting a course through a dynamically changing landscape. Unlike simpler algorithms, A\* intelligently evaluates paths by considering both the journey already taken and the estimated distance to the goal. This dual approach makes A\* not only a pathfinder but a path optimizer, ensuring the selected path is the most efficient. 332 | 333 | Imagine you're in a vast maze; A\* doesn't just explore paths blindly. It uses a strategic approach akin to a seasoned traveler who checks their progress and considers the remaining distance to the destination at every decision point. This ability to foresee and plan makes A\* particularly adept at avoiding dead ends and minimizing travel time. 334 | 335 | ### Key Components of A\* 336 | 337 | 1. **The Journey So Far (g-cost)**: This aspect involves calculating the exact cost from the start point to the current position. It's like keeping track of the miles traveled during a road trip. By accumulating these costs, A\* can compare different paths to the same point, ensuring it chooses the most efficient one. 338 | 339 | 2. **The Journey Ahead (h-cost)**: Known as the heuristic estimate, this component predicts the cost from the current position to the goal. It's a calculated guess based on the nature of the environment. For example, in a grid, this might be the Euclidean distance (straight-line distance), which provides a quick and reasonably accurate estimate of the remaining journey. 340 | 341 | The sum of these two values (g-cost + h-cost) forms the **f-cost**, which A\* uses to prioritize paths. This combination ensures that A\* not only seeks to minimize the total travel cost but also maintains a focus on progressing towards the goal. 342 | 343 | ### Why A\* Excels Compared to Other Algorithms 344 | 345 | A\* stands out for its efficiency and effectiveness, particularly in comparison to simpler algorithms: 346 | 347 | - **Depth-First Search (DFS)** explores as deep as possible along one branch before backtracking. While it may find a path, it often does so inefficiently, sometimes missing the shortest path entirely due to its lack of goal-awareness and tendency to get trapped in deep branches. 348 | 349 | - **Breadth-First Search (BFS)** methodically explores all nodes at the present depth before moving on to the next. While BFS guarantees finding the shortest path in an unweighted graph, it is computationally expensive and memory-intensive, especially in large graphs, as it explores all nodes without any consideration of the goal's location. 350 | 351 | - **Dijkstra's Algorithm** is a precursor to A\* that calculates the shortest path by considering the total cost from the start node to each node. However, it does not incorporate a heuristic, treating all paths equally regardless of their direction relative to the goal. This can lead to unnecessary exploration and inefficiencies, particularly in large graphs with varied edge costs. 352 | 353 | A\* merges the strengths of Dijkstra's thorough cost analysis with an informed heuristic approach, directing its search towards the goal and avoiding unnecessary paths. This combination allows it to find the shortest path efficiently, making it suitable for a wide range of practical applications. 354 | 355 | ### Applications and Advantages 356 | 357 | A\* is widely used in various domains due to its reliability and efficiency: 358 | 359 | - **Video Games**: A\* is the backbone of many AI pathfinding systems, guiding characters through complex virtual worlds with precision. Its ability to navigate around obstacles and efficiently reach objectives makes it ideal for real-time strategy games and role-playing games. 360 | 361 | - **Robotics**: In robotics, A\* helps autonomous robots navigate through environments, such as factory floors or outdoor terrains. The algorithm enables robots to avoid obstacles, plan efficient routes, and respond to dynamic changes in their surroundings. 362 | 363 | - **Navigation Systems**: A\* is used in GPS navigation systems to find the quickest route between two points, considering factors like road distances and traffic conditions. 364 | 365 | - **AI and Machine Learning**: A\* is used in AI for problem-solving, such as solving puzzles or planning tasks. Its ability to incorporate different heuristics allows it to be adapted to various types of problems. 366 | 367 | ### Advanced Implementation Features 368 | 369 | This project's implementation of A\* goes beyond standard features, incorporating advanced techniques to enhance performance and versatility. Most of the credit for these goes to Andrew Kravchuck, who wrote the Lisp implementation this is based on: 370 | 371 | 1. **Smart Organization**: The algorithm uses a priority queue to manage paths, ensuring that the most promising paths are explored first. This efficient data structure reduces the time spent evaluating less optimal paths. 372 | 373 | 2. **Precision Handling**: The implementation supports both integer and floating-point coordinates, making it adaptable to different scenarios, from simple grid maps to detailed real-world environments. 374 | 375 | 3. **Adaptive Heuristics**: It allows for various heuristic functions, such as Manhattan, Euclidean, or Octile distances, which can be tailored to the specifics of the problem space, optimizing the search process. 376 | 377 | 4. **Complex Terrain Navigation**: The implementation can handle diverse movements, including diagonal and custom paths, enhancing its capability to navigate through varied terrains. 378 | 379 | 5. **Efficient Path Reconstruction**: Upon reaching the goal, the implementation efficiently reconstructs the path, ensuring minimal computational overhead in finalizing the route. 380 | 381 | 6. **Robust Error Handling**: The algorithm gracefully manages exceptional situations, such as encountering impassable regions or unsolvable configurations, providing clear feedback to users. 382 | 383 | 7. **Optimized Data Structures**: The use of bit fields for coordinate encoding enhances memory efficiency and processing speed, crucial for handling large-scale environments or high-resolution grids. 384 | 385 | ## Dependencies 386 | 387 | - Python 3.x 388 | - NumPy 389 | - Matplotlib 390 | - SciPy 391 | - Scikit-Image 392 | - Noise 393 | - Pillow 394 | - TQDM 395 | - Numba (for JIT compilation) 396 | - FFmpeg (for video encoding) 397 | - Requests (for downloading custom fonts) 398 | 399 | ## License 400 | 401 | This project is licensed under the MIT License. See the `LICENSE` file for details. 402 | 403 | --- 404 | 405 | Thanks for your interest in my open-source project! I hope you find it useful. You might also find my commercial web apps useful, and I would really appreciate it if you checked them out: 406 | 407 | **[YoutubeTranscriptOptimizer.com](https://youtubetranscriptoptimizer.com)** makes it really quick and easy to paste in a YouTube video URL and have it automatically generate not just a really accurate direct transcription, but also a super polished and beautifully formatted written document that can be used independently of the video. 408 | 409 | The document basically sticks to the same material as discussed in the video, but it sounds much more like a real piece of writing and not just a transcript. It also lets you optionally generate quizzes based on the contents of the document, which can be either multiple choice or short-answer quizzes, and the multiple choice quizzes get turned into interactive HTML files that can be hosted and easily shared, where you can actually take the quiz and it will grade your answers and score the quiz for you. 410 | 411 | **[FixMyDocuments.com](https://fixmydocuments.com/)** lets you submit any kind of document— PDFs (including scanned PDFs that require OCR), MS Word and Powerpoint files, images, audio files (mp3, m4a, etc.) —and turn them into highly optimized versions in nice markdown formatting, from which HTML and PDF versions are automatically generated. Once converted, you can also edit them directly in the site using the built-in markdown editor, where it saves a running revision history and regenerates the PDF/HTML versions. 412 | 413 | In addition to just getting the optimized version of the document, you can also generate many other kinds of "derived documents" from the original: interactive multiple-choice quizzes that you can actually take and get graded on; slick looking presentation slides as PDF or HTML (using LaTeX and Reveal.js), an in-depth summary, a concept mind map (using Mermaid diagrams) and outline, custom lesson plans where you can select your target audience, a readability analysis and grade-level versions of your original document (good for simplifying concepts for students), Anki Flashcards that you can import directly into the Anki app or use on the site in a nice interface, and more. 414 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import time 4 | import gc 5 | import os 6 | import asyncio 7 | import shutil 8 | import warnings 9 | from asyncio import to_thread 10 | from datetime import datetime 11 | import concurrent.futures 12 | from concurrent.futures import ProcessPoolExecutor 13 | from asyncio import Semaphore 14 | from functools import partial 15 | import numpy as np 16 | import numba as nb 17 | from scipy.spatial import Voronoi 18 | from skimage.morphology import skeletonize, binary_erosion 19 | from noise import snoise2 20 | from scipy.ndimage import label, binary_dilation 21 | from scipy.signal import convolve2d 22 | from skimage.morphology import thin, disk 23 | from PIL import Image 24 | import requests 25 | from tqdm import tqdm 26 | import matplotlib.pyplot as plt 27 | from matplotlib import font_manager 28 | from matplotlib.colors import LinearSegmentedColormap 29 | from matplotlib.animation import FuncAnimation, FFMpegWriter 30 | from matplotlib.lines import Line2D 31 | from matplotlib.patheffects import withStroke 32 | from matplotlib.patches import Patch, Circle, Rectangle 33 | from heapq import heappush, heappop 34 | from PIL import Image 35 | warnings.filterwarnings("ignore", message=".*tight_layout.*") 36 | 37 | # Add this line to switch to a non-interactive backend 38 | plt.switch_backend("Agg") 39 | 40 | # Define the URL for the Montserrat font (Regular weight) 41 | font_url = "https://github.com/JulietaUla/Montserrat/raw/master/fonts/ttf/Montserrat-Regular.ttf" 42 | font_filename = "Montserrat-Regular.ttf" 43 | font_path = os.path.join("fonts", font_filename) 44 | 45 | # Create a fonts directory if it doesn't exist 46 | os.makedirs("fonts", exist_ok=True) 47 | 48 | os.nice(22) # Increase the niceness value to lower the priority 49 | 50 | 51 | # Function to download the font 52 | def download_font(url, path): 53 | response = requests.get(url) 54 | response.raise_for_status() # Raise an exception for HTTP errors 55 | with open(path, "wb") as f: 56 | f.write(response.content) 57 | 58 | 59 | # Download the font if it doesn't exist locally or is corrupted 60 | try: 61 | if not os.path.isfile(font_path) or os.path.getsize(font_path) == 0: 62 | print("Downloading Montserrat font...") 63 | download_font(font_url, font_path) 64 | print("Font downloaded.") 65 | except requests.exceptions.RequestException as e: 66 | print(f"Error downloading the font: {e}") 67 | raise 68 | 69 | # Verify that the font is a valid TrueType font 70 | try: 71 | font_manager.fontManager.addfont(font_path) 72 | plt.rcParams["font.family"] = "Montserrat" 73 | print("Font loaded and set.") 74 | except RuntimeError as e: 75 | print(f"Error loading font: {e}") 76 | raise 77 | 78 | # Constants for integer coordinate encoding 79 | BITS_PER_COORDINATE = int(math.floor(math.log2((1 << 63) - 1) / 2)) 80 | 81 | # Constants for float coordinate encoding 82 | BITS_PER_FLOAT_SIGNIFICAND = 24 83 | BITS_PER_FLOAT_EXPONENT = BITS_PER_COORDINATE - 1 + BITS_PER_FLOAT_SIGNIFICAND 84 | MOST_FLOAT_COORDINATE = (1 - (1 / (1 << BITS_PER_FLOAT_SIGNIFICAND))) * ( 85 | 1 << (BITS_PER_FLOAT_EXPONENT - 1) 86 | ) 87 | LEAST_FLOAT_COORDINATE = -MOST_FLOAT_COORDINATE 88 | 89 | 90 | class PriorityQueue: 91 | def __init__(self, items=None, priorities=None): 92 | if items is None: 93 | items = [] 94 | if priorities is None: 95 | priorities = [] 96 | self.items = items 97 | self.priorities = priorities 98 | self.size = len(items) 99 | self.capacity = len(items) 100 | 101 | def is_empty(self): 102 | return self.size == 0 103 | 104 | def pop(self): 105 | if self.is_empty(): 106 | raise EmptyQueueError() 107 | min_item = self.items[0] 108 | self.size -= 1 109 | if self.size > 0: 110 | last_item = self.items[self.size] 111 | self.items[0] = last_item 112 | self.priorities[0] = self.priorities[self.size] 113 | self._heapify(0) 114 | return min_item 115 | 116 | def insert(self, item, priority): 117 | if self.size >= self.capacity: 118 | self._grow() 119 | if self.size < len(self.items): 120 | self.items[self.size] = item 121 | self.priorities[self.size] = priority 122 | else: 123 | self.items.append(item) 124 | self.priorities.append(priority) 125 | self._improve_key(self.size) 126 | self.size += 1 127 | 128 | def _grow(self): 129 | new_size = self.new_capacity(self.capacity) 130 | self.capacity = new_size 131 | self.items.extend([None] * (new_size - len(self.items))) 132 | self.priorities.extend([float("inf")] * (new_size - len(self.priorities))) 133 | 134 | @staticmethod 135 | def new_capacity(current_capacity): 136 | return current_capacity + (current_capacity >> 1) 137 | 138 | def _heapify(self, i): 139 | smallest = i 140 | l = 2 * i + 1 # noqa: E741 141 | r = 2 * i + 2 142 | 143 | if l < self.size and self.priorities[l] < self.priorities[smallest]: 144 | smallest = l 145 | if r < self.size and self.priorities[r] < self.priorities[smallest]: 146 | smallest = r 147 | 148 | if smallest != i: 149 | self.items[i], self.items[smallest] = self.items[smallest], self.items[i] 150 | self.priorities[i], self.priorities[smallest] = ( 151 | self.priorities[smallest], 152 | self.priorities[i], 153 | ) 154 | self._heapify(smallest) 155 | 156 | def _improve_key(self, i): 157 | while i > 0 and self.priorities[(i - 1) // 2] > self.priorities[i]: 158 | parent = (i - 1) // 2 159 | self.items[i], self.items[parent] = self.items[parent], self.items[i] 160 | self.priorities[i], self.priorities[parent] = ( 161 | self.priorities[parent], 162 | self.priorities[i], 163 | ) 164 | i = parent 165 | 166 | 167 | class EmptyQueueError(Exception): 168 | """Raised when an operation depends on a non-empty queue.""" 169 | 170 | pass 171 | 172 | 173 | @nb.jit 174 | def encode_integer_coordinates(x, y): 175 | return (x & ((1 << BITS_PER_COORDINATE) - 1)) | (y << BITS_PER_COORDINATE) 176 | 177 | 178 | @nb.jit 179 | def decode_integer_coordinates(value): 180 | mask = (1 << BITS_PER_COORDINATE) - 1 181 | x = value & mask 182 | y = value >> BITS_PER_COORDINATE 183 | return x, y 184 | 185 | 186 | @nb.jit 187 | def float_to_int(f): 188 | significand, exponent = math.frexp(f) 189 | significand = int(significand * (1 << BITS_PER_FLOAT_SIGNIFICAND)) 190 | exponent = exponent + BITS_PER_FLOAT_SIGNIFICAND - 1 191 | result = (significand & ((1 << BITS_PER_FLOAT_SIGNIFICAND) - 1)) | ( 192 | exponent << BITS_PER_FLOAT_SIGNIFICAND 193 | ) 194 | return result if f >= 0 else -result 195 | 196 | 197 | @nb.jit 198 | def int_to_float(i): 199 | v = abs(i) 200 | significand = v & ((1 << BITS_PER_FLOAT_SIGNIFICAND) - 1) 201 | exponent = v >> BITS_PER_FLOAT_SIGNIFICAND 202 | return math.ldexp( 203 | significand / (1 << BITS_PER_FLOAT_SIGNIFICAND), 204 | exponent - BITS_PER_FLOAT_SIGNIFICAND + 1, 205 | ) * (1 if i >= 0 else -1) 206 | 207 | 208 | @nb.jit 209 | def encode_float_coordinates(x, y): 210 | x_int = float_to_int(x) 211 | y_int = float_to_int(y) 212 | return encode_integer_coordinates(x_int, y_int) 213 | 214 | 215 | @nb.jit 216 | def decode_float_coordinates(value): 217 | x_int, y_int = decode_integer_coordinates(value) 218 | x = int_to_float(x_int) 219 | y = int_to_float(y_int) 220 | return x, y 221 | 222 | 223 | @nb.jit 224 | def make_row_major_indexer(width, node_width=1, node_height=1): 225 | def indexer(x, y): 226 | return (y // node_height) * width + (x // node_width) 227 | 228 | return indexer 229 | 230 | 231 | @nb.jit 232 | def make_column_major_indexer(height, node_width=1, node_height=1): 233 | def indexer(x, y): 234 | return (x // node_width) * height + (y // node_height) 235 | 236 | return indexer 237 | 238 | 239 | @nb.jit 240 | def make_4_directions_enumerator( 241 | node_width=1, node_height=1, min_x=0, min_y=0, max_x=None, max_y=None 242 | ): 243 | max_x = max_x if max_x is not None else np.inf 244 | max_y = max_y if max_y is not None else np.inf 245 | 246 | def enumerator(x, y, func): 247 | for dx, dy in [ 248 | (0, -node_height), 249 | (0, node_height), 250 | (-node_width, 0), 251 | (node_width, 0), 252 | ]: 253 | next_x = x + dx 254 | next_y = y + dy 255 | if min_x <= next_x < max_x and min_y <= next_y < max_y: 256 | func(next_x, next_y) 257 | 258 | return enumerator 259 | 260 | 261 | @nb.jit 262 | def make_8_directions_enumerator( 263 | node_width=1, node_height=1, min_x=0, min_y=0, max_x=None, max_y=None 264 | ): 265 | max_x = max_x if max_x is not None else np.inf 266 | max_y = max_y if max_y is not None else np.inf 267 | 268 | def enumerator(x, y, func): 269 | for dx, dy in [ 270 | (node_width, 0), 271 | (node_width, -node_height), 272 | (0, -node_height), 273 | (-node_width, -node_height), 274 | (-node_width, 0), 275 | (-node_width, node_height), 276 | (0, node_height), 277 | (node_width, node_height), 278 | ]: 279 | next_x = x + dx 280 | next_y = y + dy 281 | if min_x <= next_x < max_x and min_y <= next_y < max_y: 282 | func(next_x, next_y) 283 | 284 | return enumerator 285 | 286 | 287 | @nb.jit 288 | def make_manhattan_distance_heuristic(scale_factor=1.0): 289 | def heuristic(x1, y1, x2, y2): 290 | return scale_factor * (abs(x1 - x2) + abs(y1 - y2)) 291 | 292 | return heuristic 293 | 294 | 295 | @nb.jit 296 | def make_octile_distance_heuristic(scale_factor=1.0): 297 | sqrt2 = math.sqrt(2) 298 | 299 | def heuristic(x1, y1, x2, y2): 300 | return scale_factor * ( 301 | min(abs(x1 - x2), abs(y1 - y2)) * sqrt2 + abs(abs(x1 - x2) - abs(y1 - y2)) 302 | ) 303 | 304 | return heuristic 305 | 306 | 307 | @nb.jit 308 | def make_euclidean_distance_heuristic(scale_factor=1.0): 309 | def heuristic(x1, y1, x2, y2): 310 | return scale_factor * math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) 311 | 312 | return heuristic 313 | 314 | 315 | def define_path_finder( 316 | name, 317 | world_size, 318 | frontier_size=500, 319 | coordinate_type="float", 320 | coordinate_encoder=encode_float_coordinates, 321 | coordinate_decoder=decode_float_coordinates, 322 | indexer=None, 323 | goal_reached_p=None, 324 | neighbor_enumerator=None, 325 | exact_cost=None, 326 | heuristic_cost=None, 327 | max_movement_cost=float("inf"), 328 | path_initiator=lambda length: None, 329 | path_processor=lambda x, y: None, 330 | path_finalizer=lambda: True, 331 | ): 332 | @nb.jit 333 | def path_finder_core( 334 | start_x, start_y, goal_x, goal_y, cost_so_far, came_from, path 335 | ): 336 | frontier = [] # We'll use a list as a simple priority queue 337 | heappush(frontier, (0.0, coordinate_encoder(start_x, start_y))) 338 | start_index = indexer(start_x, start_y) 339 | goal_index = indexer(goal_x, goal_y) 340 | cost_so_far[start_index] = 0.0 341 | 342 | while frontier: 343 | _, current = heappop(frontier) 344 | current_x, current_y = coordinate_decoder(current) 345 | current_index = indexer(current_x, current_y) 346 | 347 | if goal_reached_p(current_x, current_y, goal_x, goal_y): 348 | break 349 | 350 | def process_neighbor(next_x, next_y): 351 | next_index = indexer(next_x, next_y) 352 | new_cost = cost_so_far[current_index] + exact_cost( 353 | current_x, current_y, next_x, next_y 354 | ) 355 | if new_cost < max_movement_cost and ( 356 | math.isnan(cost_so_far[next_index]) 357 | or new_cost < cost_so_far[next_index] 358 | ): 359 | cost_so_far[next_index] = new_cost 360 | came_from[next_index] = current 361 | priority = new_cost + heuristic_cost(next_x, next_y, goal_x, goal_y) 362 | heappush(frontier, (priority, coordinate_encoder(next_x, next_y))) 363 | 364 | neighbor_enumerator(current_x, current_y, process_neighbor) 365 | 366 | if math.isnan(cost_so_far[goal_index]): 367 | return 0 # Path not found 368 | 369 | length = 0 370 | current = goal_index 371 | while current != start_index: 372 | path[length] = current 373 | length += 1 374 | current_x, current_y = coordinate_decoder(came_from[current]) 375 | current = indexer(current_x, current_y) 376 | path[length] = start_index 377 | length += 1 378 | 379 | return length 380 | 381 | def path_finder(start_x, start_y, goal_x, goal_y, **params): 382 | cost_so_far = np.full(world_size, np.nan) 383 | came_from = np.full(world_size, -1, dtype=np.int64) 384 | path = np.full(world_size, -1, dtype=np.int64) 385 | 386 | length = path_finder_core( 387 | start_x, start_y, goal_x, goal_y, cost_so_far, came_from, path 388 | ) 389 | 390 | if length == 0: 391 | return None 392 | 393 | path_initiator(length) 394 | for i in range(length - 1, -1, -1): 395 | path_x, path_y = coordinate_decoder(path[i]) 396 | path_processor(path_x, path_y) 397 | 398 | return path_finalizer() 399 | 400 | path_finder.__name__ = name 401 | return path_finder 402 | 403 | 404 | @nb.jit 405 | def is_maze_solvable(maze, start, goal, max_iterations=100000): 406 | queue = [(start[0], start[1])] 407 | visited = set([(start[0], start[1])]) 408 | iterations = 0 409 | 410 | while queue and iterations < max_iterations: 411 | iterations += 1 412 | x, y = queue.pop(0) 413 | 414 | if (x, y) == goal: 415 | return True 416 | 417 | for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]: 418 | nx, ny = x + dx, y + dy 419 | if ( 420 | 0 <= nx < maze.shape[1] 421 | and 0 <= ny < maze.shape[0] 422 | and maze[ny, nx] == 0 423 | and (nx, ny) not in visited 424 | ): 425 | queue.append((nx, ny)) 426 | visited.add((nx, ny)) 427 | 428 | return False 429 | 430 | 431 | @nb.jit(nopython=True) 432 | def create_dla_maze(width, height): 433 | maze = np.zeros((height, width), dtype=np.int32) 434 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 435 | 436 | num_seeds = np.random.randint( 437 | max(3, min(width, height) // 20), max(10, min(width, height) // 5) 438 | ) 439 | seed_positions = np.random.randint( 440 | 1, min(width - 1, height - 1), size=(num_seeds, 2) 441 | ) 442 | 443 | # Use a loop instead of advanced indexing 444 | for i in range(num_seeds): 445 | y, x = seed_positions[i] 446 | maze[y, x] = 1 447 | 448 | num_particles = np.random.randint(width * height // 16, width * height // 8) 449 | directions = np.array([(0, 1), (1, 0), (0, -1), (-1, 0)]) 450 | 451 | for _ in range(num_particles): 452 | x, y = np.random.randint(1, width - 1), np.random.randint(1, height - 1) 453 | steps = 0 454 | max_steps = np.random.randint(100, 1000) 455 | while maze[y, x] == 0 and steps < max_steps: 456 | dx, dy = directions[np.random.randint(4)] 457 | nx, ny = x + dx, y + dy 458 | if 0 <= nx < width and 0 <= ny < height: 459 | if maze[ny, nx] == 1: 460 | maze[y, x] = 1 461 | break 462 | x, y = nx, ny 463 | steps += 1 464 | 465 | # Ensure connectivity 466 | for y in range(1, height - 1): 467 | for x in range(1, width - 1): 468 | if maze[y, x] == 1 and np.random.random() < 0.1: 469 | maze[y, x] = 0 470 | 471 | return maze 472 | 473 | 474 | def create_game_of_life_maze(width, height): 475 | p_alive = random.uniform(0.3, 0.7) 476 | maze = np.random.choice([0, 1], size=(height, width), p=[1 - p_alive, p_alive]) 477 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 478 | 479 | generations = random.randint(3, 10) 480 | for _ in range(generations): 481 | new_maze = maze.copy() 482 | for y in range(1, height - 1): 483 | for x in range(1, width - 1): 484 | neighbors = maze[y - 1 : y + 2, x - 1 : x + 2].sum() - maze[y, x] 485 | if maze[y, x] == 1: 486 | if neighbors < 2 or neighbors > 3: 487 | new_maze[y, x] = 0 488 | else: 489 | if neighbors == 3: 490 | new_maze[y, x] = 1 491 | maze = new_maze 492 | 493 | return maze.astype(np.int32) 494 | 495 | 496 | def create_one_dim_automata_maze(width, height): 497 | maze = np.zeros((height, width), dtype=np.int32) 498 | maze[0] = np.random.choice([0, 1], size=width) 499 | maze[0, 0] = maze[0, -1] = 1 500 | 501 | rule_number = random.randint(0, 255) 502 | rule = np.zeros((2, 2, 2), dtype=np.int32) 503 | for i in range(8): 504 | rule[i // 4, (i % 4) // 2, i % 2] = (rule_number >> i) & 1 505 | 506 | for y in range(1, height): 507 | for x in range(width): 508 | left = maze[y - 1, (x - 1) % width] 509 | center = maze[y - 1, x] 510 | right = maze[y - 1, (x + 1) % width] 511 | maze[y, x] = rule[left, center, right] 512 | 513 | maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 514 | return maze 515 | 516 | 517 | @nb.jit 518 | def create_langtons_ant_maze(width, height): 519 | maze = np.zeros((height, width), dtype=np.int32) 520 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 521 | 522 | ant_x, ant_y = random.randint(1, width - 2), random.randint(1, height - 2) 523 | ant_direction = random.randint(0, 3) 524 | 525 | steps = random.randint(width * height // 4, width * height) 526 | for _ in range(steps): 527 | maze[ant_y, ant_x] = 1 - maze[ant_y, ant_x] 528 | if maze[ant_y, ant_x] == 1: 529 | ant_direction = (ant_direction + 1) % 4 530 | else: 531 | ant_direction = (ant_direction - 1) % 4 532 | 533 | if ant_direction == 0: 534 | ant_y = max(1, ant_y - 1) 535 | elif ant_direction == 1: 536 | ant_x = min(width - 2, ant_x + 1) 537 | elif ant_direction == 2: 538 | ant_y = min(height - 2, ant_y + 1) 539 | else: 540 | ant_x = max(1, ant_x - 1) 541 | 542 | return maze 543 | 544 | 545 | def create_voronoi_maze(width, height): 546 | num_points = np.random.randint(max(width, height) // 3, max(width, height) // 2) 547 | points = np.random.rand(num_points, 2) * [width, height] 548 | vor = Voronoi(points) 549 | 550 | maze = np.ones((height, width), dtype=np.int32) 551 | 552 | maze = draw_lines(maze, vor.vertices, vor.ridge_vertices) 553 | 554 | # Apply erosion to create wider passages 555 | maze = binary_erosion(maze, np.ones((3, 3))) 556 | 557 | # Skeletonize to thin the passages 558 | maze = skeletonize(1 - maze).astype(np.int32) 559 | maze = 1 - maze 560 | 561 | # Ensure borders are walls 562 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 563 | 564 | return maze 565 | 566 | 567 | @nb.jit 568 | def recursive_divide(maze, x, y, w, h, min_size=4): 569 | if w <= min_size or h <= min_size: 570 | return 571 | 572 | # Randomly decide whether to divide horizontally or vertically 573 | if w > h: 574 | divide_horizontally = np.random.random() < 0.8 575 | else: 576 | divide_horizontally = np.random.random() < 0.2 577 | 578 | if divide_horizontally: 579 | divide_at = np.random.randint(y + 1, y + h - 1) 580 | maze[divide_at, x : x + w] = 1 581 | opening = np.random.randint(x, x + w) 582 | maze[divide_at, opening] = 0 583 | recursive_divide(maze, x, y, w, divide_at - y, min_size) 584 | recursive_divide(maze, x, divide_at + 1, w, y + h - divide_at - 1, min_size) 585 | else: 586 | divide_at = np.random.randint(x + 1, x + w - 1) 587 | maze[y : y + h, divide_at] = 1 588 | opening = np.random.randint(y, y + h) 589 | maze[opening, divide_at] = 0 590 | recursive_divide(maze, x, y, divide_at - x, h, min_size) 591 | recursive_divide(maze, divide_at + 1, y, x + w - divide_at - 1, h, min_size) 592 | 593 | 594 | def create_fractal_maze(width, height, min_size=4): 595 | maze = np.zeros((height, width), dtype=np.int32) 596 | recursive_divide(maze, 0, 0, width, height, min_size) 597 | return maze 598 | 599 | 600 | def create_maze_from_image(width, height): 601 | image_path = "" # Enter file path here 602 | # Load and resize the image 603 | img = Image.open(image_path).convert("L") 604 | img = img.resize((width, height)) 605 | 606 | # Convert to numpy array and threshold 607 | maze = np.array(img) 608 | maze = (maze > 128).astype(int) 609 | 610 | # Ensure borders are walls 611 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 612 | return maze 613 | 614 | 615 | @nb.jit(nopython=True) 616 | def get_valid_tiles(maze, x, y, width, height, tiles): 617 | valid = np.ones(3, dtype=np.bool_) 618 | for dx, dy, direction in [(0, -1, 0), (1, 0, 1), (0, 1, 2), (-1, 0, 3)]: 619 | nx, ny = x + dx, y + dy 620 | if 0 <= nx < width and 0 <= ny < height: 621 | maze_value = maze[ny, nx] 622 | if 0 <= maze_value < 3: 623 | valid &= tiles[maze_value][direction] 624 | else: 625 | valid &= False 626 | return np.where(valid)[0] 627 | 628 | 629 | @nb.jit(nopython=True) 630 | def create_wave_function_collapse_maze_core(width, height, tiles, max_iterations): 631 | maze = np.full((height, width), -1, dtype=np.int32) 632 | stack = [(np.random.randint(1, width - 2), np.random.randint(1, height - 2))] 633 | 634 | iterations = 0 635 | while stack and iterations < max_iterations: 636 | idx = np.random.randint(0, len(stack)) 637 | x, y = stack[idx] 638 | stack.pop(idx) 639 | 640 | if maze[y, x] == -1: 641 | valid_tiles = get_valid_tiles(maze, x, y, width, height, tiles) 642 | if len(valid_tiles) > 0: 643 | maze[y, x] = np.random.choice(valid_tiles) 644 | for dx, dy in [(0, -1), (1, 0), (0, 1), (-1, 0)]: 645 | nx, ny = x + dx, y + dy 646 | if ( 647 | 0 < nx < width - 1 648 | and 0 < ny < height - 1 649 | and maze[ny, nx] == -1 650 | ): 651 | stack.append((nx, ny)) 652 | 653 | iterations += 1 654 | 655 | return maze 656 | 657 | 658 | def create_wave_function_collapse_maze(width, height, timeout=30): 659 | tiles = np.array( 660 | [ 661 | [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], # Empty 662 | [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], # Wall 663 | [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], # Special 664 | ], 665 | dtype=np.bool_, 666 | ) 667 | 668 | start_time = time.time() 669 | max_iterations = width * height * 10 # Adjust this factor as needed 670 | 671 | maze = create_wave_function_collapse_maze_core(width, height, tiles, max_iterations) 672 | 673 | # Fill any remaining -1 cells with random valid tiles 674 | for y in range(height): 675 | for x in range(width): 676 | if maze[y, x] == -1: 677 | valid_tiles = get_valid_tiles(maze, x, y, width, height, tiles) 678 | maze[y, x] = ( 679 | np.random.choice(valid_tiles) if len(valid_tiles) > 0 else 0 680 | ) 681 | 682 | # Ensure borders are walls 683 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 684 | 685 | elapsed_time = time.time() - start_time 686 | if elapsed_time > timeout: 687 | print( 688 | f"Warning: Wave Function Collapse maze generation exceeded timeout ({elapsed_time:.2f}s > {timeout}s)" 689 | ) 690 | 691 | return maze 692 | 693 | 694 | def create_growing_tree_maze(width, height): 695 | maze = np.ones((height, width), dtype=np.int32) 696 | stack = [(1, 1)] 697 | maze[1, 1] = 0 698 | 699 | directions = np.array([(0, -1), (1, 0), (0, 1), (-1, 0)]) 700 | 701 | while stack: 702 | if np.random.random() < 0.5: 703 | current = stack.pop(np.random.randint(0, len(stack))) 704 | else: 705 | current = stack.pop() 706 | 707 | x, y = current 708 | np.random.shuffle(directions) # This is now using numpy's shuffle 709 | 710 | for dx, dy in directions: 711 | nx, ny = x + dx, y + dy 712 | if 0 < nx < width - 1 and 0 < ny < height - 1 and maze[ny, nx] == 1: 713 | maze[ny, nx] = 0 714 | stack.append((nx, ny)) 715 | 716 | return maze 717 | 718 | 719 | def generate_terrain(width, height, scale, octaves, persistence, lacunarity): 720 | terrain = np.zeros((height, width), dtype=np.float32) 721 | for y in range(height): 722 | for x in range(width): 723 | terrain[y, x] = snoise2( 724 | x * scale, 725 | y * scale, 726 | octaves=octaves, 727 | persistence=persistence, 728 | lacunarity=lacunarity, 729 | ) 730 | return terrain 731 | 732 | 733 | def create_terrain_based_maze(width, height): 734 | scale = np.random.uniform(0.05, 0.1) 735 | octaves = np.random.randint(4, 6) 736 | persistence = np.random.uniform(0.5, 0.7) 737 | lacunarity = np.random.uniform(2.0, 2.5) 738 | 739 | terrain = generate_terrain(width, height, scale, octaves, persistence, lacunarity) 740 | terrain = (terrain - terrain.min()) / (terrain.max() - terrain.min()) 741 | maze = (terrain > np.percentile(terrain, 60)).astype(np.int32) 742 | 743 | # Apply erosion to create wider passages 744 | maze = binary_erosion(maze, np.ones((3, 3))) 745 | 746 | # Skeletonize to thin the passages 747 | maze = skeletonize(1 - maze).astype(np.int32) 748 | maze = 1 - maze 749 | 750 | # Ensure borders are walls 751 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 752 | 753 | return maze 754 | 755 | 756 | def create_musicalized_maze(width, height): 757 | frequencies = np.linspace(1, 10, num=width) 758 | time = np.linspace(0, 10, num=height) 759 | t, f = np.meshgrid(time, frequencies) 760 | 761 | harmonic1 = np.sin(2 * np.pi * f * t) 762 | harmonic2 = np.sin(3 * np.pi * f * t) 763 | harmonic3 = np.sin(5 * np.pi * f * t) 764 | 765 | combined = ( 766 | np.random.random() * harmonic1 767 | + np.random.random() * harmonic2 768 | + np.random.random() * harmonic3 769 | ) 770 | 771 | combined = (combined - combined.min()) / (combined.max() - combined.min()) 772 | threshold = np.random.uniform(0.3, 0.7) 773 | maze = (combined > threshold).astype(np.int32) 774 | 775 | # Apply binary dilation to create thicker walls 776 | structure = np.ones((3, 3), dtype=np.int32) 777 | maze = binary_dilation(maze, structure=structure).astype(np.int32) 778 | 779 | # Ensure borders are walls 780 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 781 | 782 | return maze 783 | 784 | 785 | def create_quantum_inspired_maze(width, height): 786 | x = np.linspace(-5, 5, width) 787 | y = np.linspace(-5, 5, height) 788 | xx, yy = np.meshgrid(x, y) 789 | 790 | psi1 = np.exp(-(xx**2 + yy**2) / 2) * np.exp(1j * (xx + yy)) 791 | psi2 = np.exp(-((xx - 2) ** 2 + (yy - 2) ** 2) / 2) * np.exp(1j * (xx - yy)) 792 | psi3 = np.exp(-((xx + 2) ** 2 + (yy + 2) ** 2) / 2) * np.exp(1j * (xx * yy)) 793 | 794 | psi_combined = psi1 + psi2 + psi3 795 | prob_density = np.abs(psi_combined) ** 2 796 | 797 | prob_density = (prob_density - prob_density.min()) / ( 798 | prob_density.max() - prob_density.min() 799 | ) 800 | maze = (prob_density > np.percentile(prob_density, 70)).astype(np.int32) 801 | 802 | # Apply erosion to create wider passages 803 | maze = binary_erosion(maze, np.ones((3, 3))) 804 | 805 | # Skeletonize to thin the passages 806 | maze = skeletonize(1 - maze).astype(np.int32) 807 | maze = 1 - maze 808 | 809 | # Ensure borders are walls 810 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 811 | 812 | return maze 813 | 814 | 815 | def create_artistic_maze(width, height): 816 | canvas = np.zeros((height, width), dtype=np.int32) 817 | 818 | def add_brush_strokes(canvas, width, height): 819 | for _ in range(np.random.randint(5, 15)): 820 | x, y = np.random.randint(0, width - 1), np.random.randint(0, height - 1) 821 | max_length = max(10, min(width, height) // 2) 822 | length = np.random.randint(10, max_length) 823 | angle = np.random.uniform(0, 2 * np.pi) 824 | dx, dy = length * np.cos(angle), length * np.sin(angle) 825 | rr, cc = np.linspace(x, x + dx, num=100), np.linspace(y, y + dy, num=100) 826 | rr = np.clip(rr.astype(np.int32), 0, width - 1) 827 | cc = np.clip(cc.astype(np.int32), 0, height - 1) 828 | canvas[cc, rr] = 1 829 | return canvas 830 | 831 | canvas = add_brush_strokes(canvas, width, height) 832 | 833 | # Add random "splatters" 834 | for _ in range(np.random.randint(3, 8)): 835 | x, y = np.random.randint(0, width - 1), np.random.randint(0, height - 1) 836 | radius = np.random.randint(5, min(20, min(width, height) // 4)) 837 | splatter = disk(radius) 838 | x_start, y_start = max(0, x - radius), max(0, y - radius) 839 | x_end, y_end = min(width, x + radius + 1), min(height, y + radius + 1) 840 | canvas_section = canvas[y_start:y_end, x_start:x_end] 841 | splatter_section = splatter[: y_end - y_start, : x_end - x_start] 842 | canvas_section[splatter_section > 0] = 1 843 | 844 | # Apply binary dilation to create thicker strokes 845 | structure = np.ones((3, 3), dtype=np.int32) 846 | canvas = binary_dilation(canvas, structure=structure).astype(np.int32) 847 | 848 | # Thin the result to create maze-like structures 849 | maze = thin(canvas).astype(np.int32) 850 | 851 | # Ensure borders are walls 852 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 853 | return maze 854 | 855 | 856 | def custom_rule(neighborhood): 857 | center = neighborhood[1, 1] 858 | neighbors_sum = np.sum(neighborhood) - center 859 | if center == 1: 860 | return 1 if neighbors_sum in [2, 3, 4] else 0 861 | else: 862 | return 1 if neighbors_sum in [3, 4, 5] else 0 863 | 864 | 865 | def create_cellular_automaton_maze(width, height): 866 | maze = np.random.choice([0, 1], size=(height, width), p=[0.6, 0.4]) 867 | 868 | for _ in range(np.random.randint(3, 7)): 869 | new_maze = np.zeros_like(maze) 870 | for i in range(1, height - 1): 871 | for j in range(1, width - 1): 872 | neighborhood = maze[i - 1 : i + 2, j - 1 : j + 2] 873 | new_maze[i, j] = custom_rule(neighborhood) 874 | maze = new_maze 875 | 876 | # Apply convolution to smooth the maze 877 | kernel = np.ones((3, 3)) / 9 878 | maze = convolve2d(maze, kernel, mode="same", boundary="wrap") 879 | maze = (maze > 0.5).astype(np.int32) 880 | 881 | # Ensure borders are walls 882 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 883 | return maze 884 | 885 | 886 | def create_fourier_maze_core(width, height): 887 | noise = np.random.rand(height, width) 888 | fft_noise = np.fft.fft2(noise) 889 | 890 | center_y, center_x = height // 2, width // 2 891 | y, x = np.ogrid[-center_y : height - center_y, -center_x : width - center_x] 892 | 893 | min_dim = min(width, height) 894 | low_freq = (x * x + y * y <= (min_dim // 8) ** 2).astype(np.float32) 895 | mid_freq = ( 896 | (x * x + y * y <= (min_dim // 4) ** 2) & (x * x + y * y > (min_dim // 8) ** 2) 897 | ).astype(np.float32) 898 | high_freq = ( 899 | (x * x + y * y <= (min_dim // 2) ** 2) & (x * x + y * y > (min_dim // 4) ** 2) 900 | ).astype(np.float32) 901 | 902 | mask = 0.6 * low_freq + 0.3 * mid_freq + 0.1 * high_freq 903 | 904 | filtered_fft = fft_noise * mask 905 | maze = np.real(np.fft.ifft2(filtered_fft)) 906 | 907 | return maze 908 | 909 | 910 | def create_fourier_maze(width, height): 911 | maze = create_fourier_maze_core(width, height) 912 | maze = (maze > np.percentile(maze, 60)).astype(np.int32) 913 | 914 | # Apply erosion to create wider passages 915 | maze = binary_erosion(maze, np.ones((3, 3))) 916 | 917 | # Skeletonize to thin the passages 918 | maze = skeletonize(1 - maze).astype(np.int32) 919 | maze = 1 - maze 920 | 921 | # Ensure borders are walls 922 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 923 | 924 | return maze 925 | 926 | 927 | @nb.jit(nopython=True) 928 | def convolve2d_numba(A, kernel): 929 | h, w = A.shape 930 | kh, kw = kernel.shape 931 | padh, padw = kh // 2, kw // 2 932 | 933 | result = np.zeros_like(A) 934 | 935 | for i in range(h): 936 | for j in range(w): 937 | for ki in range(kh): 938 | for kj in range(kw): 939 | ii = (i - padh + ki) % h 940 | jj = (j - padw + kj) % w 941 | result[i, j] += A[ii, jj] * kernel[ki, kj] 942 | 943 | return result 944 | 945 | 946 | @nb.jit(nopython=True) 947 | def reaction_diffusion_step(A, B, DA, DB, f, k, laplacian_kernel): 948 | A_lap = convolve2d_numba(A, laplacian_kernel) 949 | B_lap = convolve2d_numba(B, laplacian_kernel) 950 | A += DA * A_lap - A * B**2 + f * (1 - A) 951 | B += DB * B_lap + A * B**2 - (k + f) * B 952 | return np.clip(A, 0, 1), np.clip(B, 0, 1) 953 | 954 | 955 | @nb.jit(nopython=True) 956 | def create_reaction_diffusion_maze_core(width, height, num_iterations): 957 | A = np.random.rand(height, width) 958 | B = np.random.rand(height, width) 959 | 960 | laplacian_kernel = np.array([[0.05, 0.2, 0.05], [0.2, -1, 0.2], [0.05, 0.2, 0.05]]) 961 | 962 | DA, DB = 0.16, 0.08 963 | f, k = 0.035, 0.065 964 | 965 | for _ in range(num_iterations): 966 | A, B = reaction_diffusion_step(A, B, DA, DB, f, k, laplacian_kernel) 967 | 968 | return A 969 | 970 | 971 | def create_reaction_diffusion_maze(width, height): 972 | A = create_reaction_diffusion_maze_core(width, height, 20) 973 | 974 | maze = (A - A.min()) / (A.max() - A.min()) 975 | maze = (maze > np.random.uniform(0.4, 0.6)).astype(np.int32) 976 | 977 | # Apply binary dilation to create thicker walls 978 | structure = np.ones((3, 3), dtype=np.int32) 979 | maze = binary_dilation(maze, structure=structure).astype(np.int32) 980 | 981 | # Ensure borders are walls 982 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 983 | return maze 984 | 985 | 986 | def make_maze_based_on_custom_map_image(width, height): 987 | # Hardcoded path to the custom map image 988 | image_path = "path/to/your/custom_map_image.png" 989 | # Load the image and convert it to grayscale 990 | img = Image.open(image_path).convert('L') 991 | # Convert the image to a numpy array 992 | img_array = np.array(img) 993 | # Threshold the image to create a binary maze 994 | # You can adjust the threshold value (128) to fine-tune the maze generation 995 | binary_maze = (img_array > 128).astype(np.uint8) * 255 996 | # Convert back to an image for resizing 997 | binary_img = Image.fromarray(binary_maze, mode='L') 998 | # Resize the binary image to match the desired maze dimensions 999 | # Use nearest neighbor interpolation to preserve sharp edges 1000 | resized_img = binary_img.resize((width, height), Image.NEAREST) 1001 | # Convert the resized image back to a numpy array and create the final maze 1002 | maze = (np.array(resized_img) > 128).astype(np.int32) 1003 | # Ensure the border is all walls 1004 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 1005 | return maze 1006 | 1007 | 1008 | def create_better_maze(width, height, maze_generation_approach): 1009 | if maze_generation_approach == "dla": 1010 | return create_dla_maze(width, height) 1011 | elif maze_generation_approach == "random_game_of_life": 1012 | return create_game_of_life_maze(width, height) 1013 | elif maze_generation_approach == "random_one_dim_automata": 1014 | return create_one_dim_automata_maze(width, height) 1015 | elif maze_generation_approach == "langtons_ant": 1016 | return create_langtons_ant_maze(width, height) 1017 | elif maze_generation_approach == "voronoi": 1018 | return create_voronoi_maze(width, height) 1019 | elif maze_generation_approach == "fractal": 1020 | return create_fractal_maze(width, height) 1021 | elif maze_generation_approach == "wave_function_collapse": 1022 | return create_wave_function_collapse_maze(width, height) 1023 | elif maze_generation_approach == "growing_tree": 1024 | return create_growing_tree_maze(width, height) 1025 | elif maze_generation_approach == "terrain": 1026 | return create_terrain_based_maze(width, height) 1027 | elif maze_generation_approach == "maze_from_image": 1028 | return create_maze_from_image(width, height) 1029 | elif maze_generation_approach == "musicalized": 1030 | return create_musicalized_maze(width, height) 1031 | elif maze_generation_approach == "quantum_inspired": 1032 | return create_quantum_inspired_maze(width, height) 1033 | elif maze_generation_approach == "artistic": 1034 | return create_artistic_maze(width, height) 1035 | elif maze_generation_approach == "cellular_automaton": 1036 | return create_cellular_automaton_maze(width, height) 1037 | elif maze_generation_approach == "fourier": 1038 | return create_fourier_maze(width, height) 1039 | elif maze_generation_approach == "reaction_diffusion": 1040 | return create_reaction_diffusion_maze(width, height) 1041 | elif maze_generation_approach == "custom_map_image": 1042 | return make_maze_based_on_custom_map_image(width, height) 1043 | else: 1044 | raise ValueError( 1045 | f"Unknown maze generation approach: {maze_generation_approach}" 1046 | ) 1047 | 1048 | 1049 | @nb.jit 1050 | def get_neighbors(x, y, maze_shape): 1051 | neighbors = [] 1052 | for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]: 1053 | nx, ny = x + dx, y + dy 1054 | if 0 <= nx < maze_shape[1] and 0 <= ny < maze_shape[0]: 1055 | neighbors.append((nx, ny)) 1056 | return neighbors 1057 | 1058 | 1059 | @nb.jit 1060 | def manhattan_distance(a, b): 1061 | return abs(b[0] - a[0]) + abs(b[1] - a[1]) 1062 | 1063 | 1064 | @nb.jit 1065 | def a_star(start, goal, maze): 1066 | frontier = [(0, start)] 1067 | came_from = {} 1068 | cost_so_far = {start: 0} 1069 | 1070 | while frontier: 1071 | current = frontier.pop(0)[1] 1072 | 1073 | if current == goal: 1074 | break 1075 | 1076 | for next in get_neighbors(*current, maze.shape): 1077 | new_cost = cost_so_far[current] + 1 1078 | if next not in cost_so_far or new_cost < cost_so_far[next]: 1079 | cost_so_far[next] = new_cost 1080 | priority = new_cost + manhattan_distance(goal, next) 1081 | frontier.append((priority, next)) 1082 | frontier.sort(key=lambda x: x[0]) 1083 | came_from[next] = current 1084 | 1085 | return came_from 1086 | 1087 | 1088 | @nb.jit 1089 | def manhattan_distance_to_start(start_x, start_y, point): 1090 | return abs(point[0] - start_x) + abs(point[1] - start_y) 1091 | 1092 | 1093 | @nb.jit 1094 | def find_min_distance_neighbor(neighbors, start_x, start_y): 1095 | min_distance = float("inf") 1096 | min_neighbor = None 1097 | for neighbor in neighbors: 1098 | distance = manhattan_distance_to_start(start_x, start_y, neighbor) 1099 | if distance < min_distance: 1100 | min_distance = distance 1101 | min_neighbor = neighbor 1102 | return min_neighbor 1103 | 1104 | 1105 | @nb.jit 1106 | def ensure_connectivity(maze, start, goal): 1107 | path = a_star(start, goal, maze) 1108 | if goal not in path: 1109 | current = goal 1110 | while current != start: 1111 | x, y = current 1112 | maze[y, x] = 0 1113 | neighbors = get_neighbors(x, y, maze.shape) 1114 | current = find_min_distance_neighbor(neighbors, start[0], start[1]) 1115 | return maze 1116 | 1117 | 1118 | @nb.jit 1119 | def smart_hole_puncher(maze, start, goal): 1120 | path = a_star(start, goal, maze) 1121 | if goal not in path: 1122 | current = goal 1123 | start_x, start_y = start 1124 | while current != start: 1125 | x, y = current 1126 | if maze[y, x] == 1: 1127 | maze[y, x] = 0 1128 | neighbors = get_neighbors(x, y, maze.shape) 1129 | next_step = find_min_distance_neighbor(neighbors, start_x, start_y) 1130 | current = next_step 1131 | 1132 | return maze, True 1133 | 1134 | 1135 | def set_nice(): 1136 | try: 1137 | os.nice(22) # Increase the niceness value to lower the priority 1138 | except AttributeError: 1139 | pass # os.nice() is not available on Windows 1140 | 1141 | 1142 | @nb.jit 1143 | def add_walls(maze, target_percentage): 1144 | height, width = maze.shape 1145 | total_cells = height * width 1146 | initial_wall_count = np.sum(maze) 1147 | target_wall_count = int(total_cells * target_percentage) 1148 | 1149 | print( 1150 | "Adding walls. Initial count: " 1151 | + str(initial_wall_count) 1152 | + ", Target count: " 1153 | + str(target_wall_count) 1154 | ) 1155 | 1156 | while np.sum(maze) < target_wall_count: 1157 | y, x = np.random.randint(1, height - 1), np.random.randint(1, width - 1) 1158 | if maze[y, x] == 0: 1159 | maze[y, x] = 1 1160 | 1161 | final_wall_count = np.sum(maze) 1162 | print("Walls added. Final count: " + str(final_wall_count)) 1163 | return maze 1164 | 1165 | 1166 | @nb.jit 1167 | def remove_walls(maze, target_percentage): 1168 | height, width = maze.shape 1169 | total_cells = height * width 1170 | initial_wall_count = np.sum(maze) 1171 | target_wall_count = int(total_cells * target_percentage) 1172 | 1173 | print( 1174 | "Removing walls. Initial count: " 1175 | + str(initial_wall_count) 1176 | + ", Target count: " 1177 | + str(target_wall_count) 1178 | ) 1179 | 1180 | while np.sum(maze) > target_wall_count: 1181 | y, x = np.random.randint(1, height - 1), np.random.randint(1, width - 1) 1182 | if maze[y, x] == 1: 1183 | maze[y, x] = 0 1184 | 1185 | final_wall_count = np.sum(maze) 1186 | print("Walls removed. Final count: " + str(final_wall_count)) 1187 | return maze 1188 | 1189 | 1190 | @nb.jit 1191 | def add_room_separators(maze): 1192 | height, width = maze.shape 1193 | for _ in range(3): # Add a few separators 1194 | x = np.random.randint(1, width - 1) 1195 | maze[:, x] = 1 1196 | y = np.random.randint(1, height - 1) 1197 | maze[y, :] = 1 1198 | return maze 1199 | 1200 | 1201 | def break_up_large_room(maze, max_percentage, max_iterations=1000): 1202 | height, width = maze.shape 1203 | total_cells = height * width 1204 | 1205 | @nb.jit 1206 | def process_room( 1207 | maze, labeled_maze, num_rooms, room_sizes, largest_room, iterations 1208 | ): 1209 | y, x = ( 1210 | np.random.choice(np.where(labeled_maze == largest_room)[0]), 1211 | np.random.choice(np.where(labeled_maze == largest_room)[1]), 1212 | ) 1213 | 1214 | temp_maze = maze.copy() 1215 | temp_maze[y, x] = 1 1216 | temp_labeled, temp_num_rooms = label(1 - temp_maze) 1217 | 1218 | if temp_num_rooms <= num_rooms: 1219 | maze[y, x] = 1 1220 | labeled_maze, num_rooms = label(1 - maze) 1221 | room_sizes = np.bincount(labeled_maze.flatten())[1:] 1222 | largest_room = np.argmax(room_sizes) + 1 1223 | 1224 | iterations += 1 1225 | 1226 | if iterations % 100 == 0: 1227 | for _ in range(5): 1228 | ry, rx = ( 1229 | np.random.randint(1, height - 1), 1230 | np.random.randint(1, width - 1), 1231 | ) 1232 | maze[ry, rx] = 0 1233 | 1234 | return maze, labeled_maze, num_rooms, room_sizes, largest_room, iterations 1235 | 1236 | labeled_maze, num_rooms = label(1 - maze) 1237 | room_sizes = np.bincount(labeled_maze.flatten())[1:] 1238 | largest_room = np.argmax(room_sizes) + 1 1239 | 1240 | iterations = 0 1241 | while ( 1242 | np.max(room_sizes) / (total_cells - np.sum(maze)) > max_percentage 1243 | and iterations < max_iterations 1244 | ): 1245 | maze, labeled_maze, num_rooms, room_sizes, largest_room, iterations = ( 1246 | process_room( 1247 | maze, labeled_maze, num_rooms, room_sizes, largest_room, iterations 1248 | ) 1249 | ) 1250 | 1251 | if iterations >= max_iterations: 1252 | print( 1253 | f"Warning: Maximum iterations ({max_iterations}) reached in break_up_large_room" 1254 | ) 1255 | 1256 | return maze 1257 | 1258 | 1259 | def break_up_large_areas(maze, max_area_percentage=0.2): 1260 | labeled_areas, num_areas = label(1 - maze) 1261 | area_sizes = np.bincount(labeled_areas.ravel())[1:] 1262 | total_cells = maze.size 1263 | 1264 | print(f"Initial number of areas: {num_areas}") 1265 | print(f"Initial area sizes: {area_sizes}") 1266 | 1267 | @nb.jit 1268 | def process_area(maze, labeled_areas, i, size, total_cells, max_area_percentage): 1269 | if size / total_cells > max_area_percentage: 1270 | print( 1271 | f"Breaking up area {i} with size {size} ({size/total_cells:.2f} of total)" 1272 | ) 1273 | area_coords = np.argwhere(labeled_areas == i) 1274 | sub_maze_size = int(np.sqrt(size)) 1275 | sub_maze = np.ones((sub_maze_size, sub_maze_size)) 1276 | sub_maze = create_simple_maze(sub_maze) 1277 | x_offset, y_offset = area_coords.min(axis=0) 1278 | maze[ 1279 | x_offset : x_offset + sub_maze.shape[0], 1280 | y_offset : y_offset + sub_maze.shape[1], 1281 | ] = sub_maze 1282 | return maze 1283 | 1284 | for i, size in enumerate(area_sizes, 1): 1285 | maze = process_area( 1286 | maze, labeled_areas, i, size, total_cells, max_area_percentage 1287 | ) 1288 | 1289 | final_labeled_areas, final_num_areas = label(1 - maze) 1290 | final_area_sizes = np.bincount(final_labeled_areas.ravel())[1:] 1291 | print(f"Final number of areas: {final_num_areas}") 1292 | print(f"Final area sizes: {final_area_sizes}") 1293 | 1294 | return maze 1295 | 1296 | 1297 | @nb.jit 1298 | def create_simple_maze(sub_maze): 1299 | def carve_passages(x, y): 1300 | directions = np.array([(0, 1), (1, 0), (0, -1), (-1, 0)]) 1301 | np.random.shuffle(directions) 1302 | for dx, dy in directions: 1303 | nx, ny = x + dx * 2, y + dy * 2 1304 | if ( 1305 | 0 <= nx < sub_maze.shape[1] 1306 | and 0 <= ny < sub_maze.shape[0] 1307 | and sub_maze[ny, nx] == 1 1308 | ): 1309 | sub_maze[ny, nx] = 0 1310 | sub_maze[y + dy, x + dx] = 0 1311 | carve_passages(nx, ny) 1312 | 1313 | sub_maze[1::2, 1::2] = 0 1314 | start_x, start_y = 1, 1 1315 | carve_passages(start_x, start_y) 1316 | return sub_maze 1317 | 1318 | 1319 | @nb.njit 1320 | def connect_areas(maze, labeled_areas, num_areas): 1321 | if num_areas > 1: 1322 | for i in range(1, num_areas): 1323 | area1 = np.argwhere(labeled_areas == i) 1324 | area2 = np.argwhere(labeled_areas == i + 1) 1325 | if len(area1) > 0 and len(area2) > 0: 1326 | point1 = area1[np.random.randint(len(area1))] 1327 | point2 = area2[np.random.randint(len(area2))] 1328 | x1, y1 = point1 1329 | x2, y2 = point2 1330 | path = bresenham_line(x1, y1, x2, y2) 1331 | for x, y in path: 1332 | if 0 <= x < maze.shape[1] and 0 <= y < maze.shape[0]: 1333 | maze[y, x] = 0 1334 | return maze 1335 | 1336 | 1337 | def connect_disconnected_areas(maze): 1338 | labeled_areas, num_areas = label(1 - maze) 1339 | return connect_areas(maze, labeled_areas, num_areas) 1340 | 1341 | 1342 | @nb.njit 1343 | def bresenham_line(x0, y0, x1, y1): 1344 | dx = abs(x1 - x0) 1345 | dy = abs(y1 - y0) 1346 | sx = 1 if x0 < x1 else -1 1347 | sy = 1 if y0 < y1 else -1 1348 | err = dx - dy 1349 | 1350 | line_points = [] 1351 | while True: 1352 | line_points.append((x0, y0)) 1353 | if x0 == x1 and y0 == y1: 1354 | break 1355 | e2 = 2 * err 1356 | if e2 > -dy: 1357 | err -= dy 1358 | x0 += sx 1359 | if e2 < dx: 1360 | err += dx 1361 | y0 += sy 1362 | 1363 | return line_points 1364 | 1365 | 1366 | def line(y0, x0, y1, x1): 1367 | """Generate line pixels using Bresenham's line algorithm""" 1368 | dx = abs(x1 - x0) 1369 | dy = abs(y1 - y0) 1370 | sx = 1 if x0 < x1 else -1 1371 | sy = 1 if y0 < y1 else -1 1372 | err = dx - dy 1373 | 1374 | x, y = x0, y0 1375 | points_x, points_y = [], [] 1376 | 1377 | while True: 1378 | points_x.append(x) 1379 | points_y.append(y) 1380 | if x == x1 and y == y1: 1381 | break 1382 | e2 = 2 * err 1383 | if e2 > -dy: 1384 | err -= dy 1385 | x += sx 1386 | if e2 < dx: 1387 | err += dx 1388 | y += sy 1389 | 1390 | return np.array(points_y), np.array(points_x) 1391 | 1392 | 1393 | def draw_lines(maze, vertices, ridge_vertices): 1394 | for simplex in ridge_vertices: 1395 | if simplex[0] != -1 and simplex[1] != -1: 1396 | p1, p2 = vertices[simplex[0]], vertices[simplex[1]] 1397 | x1, y1 = int(p1[1]), int(p1[0]) 1398 | x2, y2 = int(p2[1]), int(p2[0]) 1399 | rr, cc = line(y1, x1, y2, x2) 1400 | valid = (rr >= 0) & (rr < maze.shape[0]) & (cc >= 0) & (cc < maze.shape[1]) 1401 | maze[rr[valid], cc[valid]] = 0 1402 | return maze 1403 | 1404 | 1405 | def validate_and_adjust_maze(maze, maze_generation_approach): 1406 | height, width = maze.shape 1407 | total_cells = height * width 1408 | 1409 | print(f"\nValidating and adjusting maze for {maze_generation_approach}") 1410 | print(f"Initial maze shape: {height}x{width}") 1411 | 1412 | target_wall_percentage = np.random.uniform(0.2, 0.3) 1413 | print(f"Target wall percentage: {target_wall_percentage:.2f}") 1414 | 1415 | wall_count = np.sum(maze) 1416 | wall_percentage = wall_count / total_cells 1417 | 1418 | print(f"Initial wall count: {wall_count:,}") 1419 | print(f"Initial wall percentage: {wall_percentage:.2f}") 1420 | 1421 | labeled_areas, num_areas = label(1 - maze) 1422 | if num_areas > 1: 1423 | print(f"Connecting {num_areas} disconnected areas...") 1424 | maze = connect_disconnected_areas(maze) 1425 | 1426 | adjustment_factor = 0.05 1427 | if wall_percentage < target_wall_percentage - adjustment_factor: 1428 | print("Wall percentage too low. Adding walls...") 1429 | maze = add_walls(maze, target_wall_percentage) 1430 | elif wall_percentage > target_wall_percentage + adjustment_factor: 1431 | print("Wall percentage too high. Removing walls...") 1432 | maze = remove_walls(maze, target_wall_percentage) 1433 | 1434 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 1435 | print("Ensured border walls") 1436 | 1437 | final_wall_count = np.sum(maze) 1438 | final_wall_percentage = final_wall_count / total_cells 1439 | print(f"Final wall count: {final_wall_count}") 1440 | print(f"Final wall percentage: {final_wall_percentage:.2f}") 1441 | 1442 | labeled_areas, num_areas = label(1 - maze) 1443 | print(f"Number of disconnected areas: {num_areas}") 1444 | 1445 | return maze 1446 | 1447 | 1448 | def generate_and_validate_maze(width, height, maze_generation_approach): 1449 | maze = create_better_maze(width, height, maze_generation_approach) 1450 | maze = validate_and_adjust_maze(maze, maze_generation_approach) 1451 | start = (1, 1) 1452 | goal = (width - 2, height - 2) 1453 | maze[start[1], start[0]] = maze[goal[1], goal[0]] = 0 1454 | is_solvable = is_maze_solvable(maze, start, goal) 1455 | if not is_solvable: 1456 | print(f"Maze is not solvable. Wall percentage: {np.sum(maze) / maze.size:.2f}") 1457 | return maze, start, goal, is_solvable 1458 | 1459 | 1460 | def generate_solvable_maze( 1461 | width, height, maze_generation_approach, max_attempts=20, max_workers=None 1462 | ): 1463 | print( 1464 | f"Attempting to generate a solvable maze using {maze_generation_approach} approach..." 1465 | ) 1466 | 1467 | generate_func = partial( 1468 | generate_and_validate_maze, width, height, maze_generation_approach 1469 | ) 1470 | 1471 | with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor: 1472 | futures = [executor.submit(generate_func) for _ in range(max_attempts)] 1473 | 1474 | for idx, future in enumerate(concurrent.futures.as_completed(futures)): 1475 | maze, start, goal, is_solvable = future.result() 1476 | print(f"Checked maze {idx + 1}") 1477 | 1478 | if is_solvable: 1479 | print(f"Solvable maze found after checking {idx + 1} mazes") 1480 | for f in futures: 1481 | f.cancel() 1482 | return maze, start, goal 1483 | 1484 | # Apply smart hole puncher even if not initially solvable 1485 | maze, success = smart_hole_puncher(maze, start, goal) 1486 | maze = ensure_connectivity(maze, start, goal) 1487 | if success: 1488 | is_solvable = is_maze_solvable(maze, start, goal) 1489 | if is_solvable: 1490 | print( 1491 | f"Solvable maze created using smart hole puncher after {idx + 1} attempts" 1492 | ) 1493 | return maze, start, goal 1494 | 1495 | print(f"Failed to generate a solvable maze after {max_attempts} attempts.") 1496 | return None, None, None 1497 | 1498 | 1499 | @nb.jit(nopython=True) 1500 | def prepare_exploration_map(exploration_order, frame, GRID_SIZE): 1501 | exploration_map = np.zeros((GRID_SIZE, GRID_SIZE), dtype=np.float32) 1502 | for idx, (x, y) in enumerate(exploration_order[:frame]): 1503 | exploration_map[y, x] = idx + 1 1504 | if exploration_map.max() > 0: 1505 | exploration_map /= exploration_map.max() 1506 | return exploration_map 1507 | 1508 | 1509 | @nb.jit(nopython=True) 1510 | def prepare_maze_rgba(maze, wall_color_rgba, floor_color_rgba): 1511 | height, width = maze.shape 1512 | maze_rgba = np.zeros((height, width, 4), dtype=np.float32) 1513 | for y in range(height): 1514 | for x in range(width): 1515 | if maze[y, x] == 1: 1516 | maze_rgba[y, x] = wall_color_rgba 1517 | else: 1518 | maze_rgba[y, x] = floor_color_rgba 1519 | return maze_rgba 1520 | 1521 | 1522 | def delete_small_files(folder, size_limit_kb=20): 1523 | one_hour_ago = time.time() - 3600 1524 | for filename in os.listdir(folder): 1525 | filepath = os.path.join(folder, filename) 1526 | if os.path.isfile(filepath): 1527 | file_size = os.path.getsize(filepath) 1528 | last_modified_time = os.path.getmtime(filepath) 1529 | if file_size < size_limit_kb * 1024 and last_modified_time < one_hour_ago: 1530 | os.remove(filepath) 1531 | print( 1532 | f"Deleted {filename} because it is smaller than {size_limit_kb}KB and was last modified over an hour ago." 1533 | ) 1534 | 1535 | 1536 | def remove_old_empty_directories(base_folder="maze_animations", age_limit_hours=1): 1537 | if not os.path.exists(base_folder): 1538 | return 1539 | 1540 | current_time = time.time() 1541 | age_limit_seconds = age_limit_hours * 3600 1542 | 1543 | for dir_name in os.listdir(base_folder): 1544 | dir_path = os.path.join(base_folder, dir_name) 1545 | if os.path.isdir(dir_path): 1546 | if not os.listdir(dir_path): 1547 | last_modified_time = os.path.getmtime(dir_path) 1548 | if (current_time - last_modified_time) > age_limit_seconds: 1549 | try: 1550 | shutil.rmtree(dir_path) 1551 | print(f"Removed empty and old directory: {dir_path}") 1552 | except OSError as e: 1553 | print(f"Error removing directory {dir_path}: {e}") 1554 | 1555 | 1556 | def create_output_folder(base_folder="maze_animations"): 1557 | os.makedirs(base_folder, exist_ok=True) 1558 | now = datetime.now() 1559 | date_time = now.strftime("%Y%m%d_%H%M%S") 1560 | animation_folder_name = f"animation_{date_time}" 1561 | output_folder = os.path.join(base_folder, animation_folder_name) 1562 | os.makedirs(output_folder, exist_ok=True) 1563 | return output_folder 1564 | 1565 | 1566 | def generate_and_save_frame( 1567 | frame, 1568 | all_mazes, 1569 | all_exploration_orders, 1570 | all_paths, 1571 | all_starts, 1572 | all_goals, 1573 | all_maze_approaches, 1574 | GRID_SIZE, 1575 | wall_color, 1576 | floor_color, 1577 | start_color, 1578 | goal_color, 1579 | path_color, 1580 | exploration_cmap, 1581 | DPI, 1582 | output_folder, 1583 | frame_format, 1584 | ): 1585 | slowdown_factor=5 # Slows down animation to illustrate solution path 1586 | fig = plt.figure(figsize=(24, 14), dpi=DPI) 1587 | fig.patch.set_facecolor("#1E1E1E") 1588 | 1589 | # Main title 1590 | title_color = plt.cm.viridis(0.5 + 0.2 * np.sin(frame * 0.001)) 1591 | title = fig.suptitle( 1592 | "2D Maze Pathfinding Visualization", 1593 | fontsize=22, 1594 | fontweight="bold", 1595 | color=title_color, 1596 | y=0.97, 1597 | ) 1598 | title.set_path_effects([withStroke(linewidth=3, foreground="black")]) 1599 | 1600 | gs_mazes = fig.add_gridspec( 1601 | 1, len(all_mazes), left=0.05, right=0.95, top=0.9, bottom=0.2 1602 | ) 1603 | 1604 | wall_color_rgba = ( 1605 | np.array( 1606 | [int(wall_color[i : i + 2], 16) for i in (1, 3, 5)] + [255], 1607 | dtype=np.float32, 1608 | ) 1609 | / 255 1610 | ) 1611 | floor_color_rgba = ( 1612 | np.array( 1613 | [int(floor_color[i : i + 2], 16) for i in (1, 3, 5)] + [255], 1614 | dtype=np.float32, 1615 | ) 1616 | / 255 1617 | ) 1618 | 1619 | for i in range(len(all_mazes)): 1620 | ax = fig.add_subplot(gs_mazes[i]) 1621 | 1622 | maze = all_mazes[i] 1623 | maze_rgba = prepare_maze_rgba(maze, wall_color_rgba, floor_color_rgba) 1624 | 1625 | ax.imshow(maze_rgba) 1626 | 1627 | exploration_length = len(all_exploration_orders[i]) 1628 | path_length = len(all_paths[i]) 1629 | total_steps = exploration_length + path_length * slowdown_factor 1630 | 1631 | if frame < exploration_length: 1632 | exploration_map = prepare_exploration_map( 1633 | all_exploration_orders[i], frame, GRID_SIZE 1634 | ) 1635 | ax.imshow(exploration_map, cmap=exploration_cmap, alpha=0.7) 1636 | current_step = frame 1637 | else: 1638 | path_frame = (frame - exploration_length) // slowdown_factor 1639 | current_step = min(frame, total_steps) 1640 | 1641 | # Animated path tracing with gradient and pulsating effect 1642 | if path_frame < path_length: 1643 | path_segment = all_paths[i][: path_frame + 1] 1644 | points = np.array(path_segment) 1645 | 1646 | # Create gradient colors for the path 1647 | colors = plt.cm.viridis(np.linspace(0, 1, len(points))) 1648 | 1649 | # Pulsating effect 1650 | pulse = 0.5 + 0.5 * np.sin(frame * 0.001) 1651 | linewidths = 2 + 2 * pulse 1652 | 1653 | # Plot the path 1654 | for j in range(len(points) - 1): 1655 | ax.plot( 1656 | points[j : j + 2, 0], 1657 | points[j : j + 2, 1], 1658 | color=colors[j], 1659 | linewidth=linewidths, 1660 | alpha=0.8, 1661 | zorder=5, 1662 | ) 1663 | elif path_frame >= path_length: 1664 | # Keep the final path visible with pulsating effect 1665 | final_path = np.array(all_paths[i]) 1666 | colors = plt.cm.viridis(np.linspace(0, 1, len(final_path))) 1667 | pulse = 0.5 + 0.5 * np.sin(frame * 0.001) 1668 | linewidths = 2 + 2 * pulse 1669 | for j in range(len(final_path) - 1): 1670 | ax.plot( 1671 | final_path[j : j + 2, 0], 1672 | final_path[j : j + 2, 1], 1673 | color=colors[j], 1674 | linewidth=linewidths, 1675 | alpha=0.8, 1676 | zorder=5, 1677 | ) 1678 | 1679 | start_x, start_y = all_starts[i] 1680 | goal_x, goal_y = all_goals[i] 1681 | 1682 | # Larger and more prominent start and goal markers 1683 | start_circle = Circle( 1684 | (start_x, start_y), 1685 | 0.8, 1686 | color=start_color, 1687 | alpha=0.9 + 0.1 * np.sin(frame * 0.2), 1688 | zorder=10, 1689 | ) 1690 | goal_star = Line2D( 1691 | [goal_x], 1692 | [goal_y], 1693 | marker="*", 1694 | color=goal_color, 1695 | markersize=20, 1696 | markeredgecolor="white", 1697 | markeredgewidth=1.5, 1698 | zorder=10, 1699 | ) 1700 | 1701 | ax.add_patch(start_circle) 1702 | ax.add_line(goal_star) 1703 | 1704 | ax.set_title( 1705 | f"{all_maze_approaches[i].replace('_', ' ').title()}", 1706 | color="white", 1707 | fontsize=14, 1708 | fontweight="bold", 1709 | pad=20, 1710 | ) 1711 | 1712 | ax.set_axis_off() 1713 | 1714 | # Progress bar 1715 | progress = current_step / total_steps 1716 | ax.add_patch( 1717 | Rectangle( 1718 | (0.05, -0.15), 1719 | 0.9, 1720 | 0.04, 1721 | facecolor="#3E3E3E", 1722 | edgecolor="none", 1723 | transform=ax.transAxes, 1724 | zorder=20, 1725 | clip_on=False, 1726 | ) 1727 | ) 1728 | ax.add_patch( 1729 | Rectangle( 1730 | (0.05, -0.15), 1731 | 0.9 * progress, 1732 | 0.04, 1733 | facecolor="#4CAF50", 1734 | edgecolor="none", 1735 | transform=ax.transAxes, 1736 | zorder=21, 1737 | clip_on=False, 1738 | ) 1739 | ) 1740 | 1741 | # Progress text 1742 | ax.text( 1743 | 0.5, 1744 | -0.2, 1745 | f"Progress: {current_step}/{total_steps} ({progress:.1%})", 1746 | ha="center", 1747 | va="center", 1748 | color="white", 1749 | fontsize=10, 1750 | transform=ax.transAxes, 1751 | zorder=22, 1752 | clip_on=False, 1753 | ) 1754 | 1755 | # Create a layout for the legend and info text 1756 | gs_info = fig.add_gridspec(1, 2, left=0.05, right=0.95, top=0.15, bottom=0.02) 1757 | 1758 | # Add general information with a modern look 1759 | info_ax = fig.add_subplot(gs_info[0]) 1760 | info_ax.axis("off") 1761 | info_text = f"Frame: {frame} | Grid Size: {GRID_SIZE}x{GRID_SIZE}" 1762 | info_ax.text( 1763 | 0.5, 1764 | 0.5, 1765 | info_text, 1766 | ha="center", 1767 | va="center", 1768 | fontsize=10, 1769 | color="white", 1770 | bbox=dict( 1771 | facecolor="#3E3E3E", edgecolor="none", alpha=0.7, pad=3, boxstyle="round" 1772 | ), 1773 | ) 1774 | 1775 | # Add a legend 1776 | legend_ax = fig.add_subplot(gs_info[1]) 1777 | legend_ax.axis("off") 1778 | legend_elements = [ 1779 | Line2D([0], [0], color=path_color, lw=2, label="Path"), 1780 | Patch(facecolor=start_color, edgecolor="none", label="Start"), 1781 | Line2D( 1782 | [0], 1783 | [0], 1784 | marker="*", 1785 | color=goal_color, 1786 | label="Goal", 1787 | linestyle="None", 1788 | markersize=15, 1789 | ), 1790 | Patch( 1791 | facecolor=exploration_cmap(0.5), 1792 | edgecolor="none", 1793 | label="Exploration", 1794 | alpha=0.5, 1795 | ), 1796 | ] 1797 | legend_ax.legend( 1798 | handles=legend_elements, 1799 | loc="center", 1800 | ncol=4, 1801 | frameon=False, 1802 | fontsize=10, 1803 | labelcolor="white", 1804 | ) 1805 | 1806 | plt.tight_layout(rect=[0, 0.05, 1, 0.95]) 1807 | 1808 | frame_filename = os.path.join(output_folder, f"frame_{frame:05d}.{frame_format}") 1809 | plt.savefig( 1810 | frame_filename, 1811 | facecolor=fig.get_facecolor(), 1812 | edgecolor="none", 1813 | bbox_inches="tight", 1814 | ) 1815 | plt.close(fig) 1816 | return frame_filename 1817 | 1818 | 1819 | async def save_animation_async(anim, filepath, writer, DPI): 1820 | await to_thread(anim.save, filepath, writer=writer, dpi=DPI) 1821 | 1822 | 1823 | async def run_complex_examples( 1824 | num_animations=1, 1825 | GRID_SIZE=31, 1826 | num_problems=1, 1827 | DPI=50, 1828 | FPS=60, 1829 | save_as_frames_only=False, 1830 | frame_format="png", 1831 | dark_mode=False, 1832 | override_maze_approach=None, 1833 | ): 1834 | if dark_mode: 1835 | wall_color = "#ECF0F1" # Light pastel for walls 1836 | floor_color = "#2C3E50" # Dark gray for background 1837 | start_color = "#1ABC9C" # Cool pastel cyan for start 1838 | goal_color = "#E74C3C" # Pastel red for goal 1839 | path_color = "#9B59B6" # Cool pastel purple for path 1840 | else: 1841 | wall_color = "#2C3E50" # Default dark wall color 1842 | floor_color = "#ECF0F1" # Default light floor color 1843 | start_color = "#27AE60" # Default green for start 1844 | goal_color = "#E74C3C" # Default red for goal 1845 | path_color = "#3498DB" # Default blue for path 1846 | exploration_colors = [ 1847 | "#FFF9C4", 1848 | "#FFE082", 1849 | "#FFB74D", 1850 | "#FF8A65", 1851 | "#E57373", 1852 | ] # Warm colors 1853 | exploration_cmap = LinearSegmentedColormap.from_list( 1854 | "exploration", exploration_colors, N=100 1855 | ) 1856 | 1857 | maze_generation_approaches = [ 1858 | "dla", 1859 | "random_game_of_life", 1860 | "random_one_dim_automata", 1861 | "voronoi", 1862 | "fractal", 1863 | "wave_function_collapse", 1864 | "growing_tree", 1865 | "terrain", 1866 | "musicalized", 1867 | "quantum_inspired", 1868 | "artistic", 1869 | "cellular_automaton", 1870 | "fourier", 1871 | "reaction_diffusion", 1872 | ] 1873 | 1874 | for animation_index in range(num_animations): 1875 | print(f"\nGenerating animation {animation_index + 1} of {num_animations}") 1876 | output_folder = create_output_folder() 1877 | 1878 | all_exploration_orders = [] 1879 | all_paths = [] 1880 | all_mazes = [] 1881 | all_starts = [] 1882 | all_goals = [] 1883 | all_maze_approaches = [] 1884 | 1885 | for i in range(num_problems): 1886 | attempts = 0 1887 | max_attempts = 5 # Allow more attempts per problem 1888 | 1889 | while attempts < max_attempts: 1890 | maze_generation_approach = override_maze_approach or random.choice( 1891 | maze_generation_approaches 1892 | ) 1893 | 1894 | print( 1895 | f"Starting maze generation for problem {i+1} using {maze_generation_approach} approach (attempt {attempts+1})..." 1896 | ) 1897 | maze, start, goal = generate_solvable_maze( 1898 | GRID_SIZE, GRID_SIZE, maze_generation_approach 1899 | ) 1900 | 1901 | if maze is not None: 1902 | break 1903 | 1904 | attempts += 1 1905 | print("Failed to generate a solvable maze. Trying again.") 1906 | 1907 | if maze is None: 1908 | print( 1909 | f"Failed to generate a solvable maze for problem {i+1} after {max_attempts} attempts. Skipping this problem." 1910 | ) 1911 | continue 1912 | 1913 | start_x, start_y = start 1914 | goal_x, goal_y = goal 1915 | print("Maze generation complete.") 1916 | 1917 | exploration_order = [] 1918 | path = [] 1919 | 1920 | def optimized_a_star(start, goal, maze): 1921 | nonlocal exploration_order, path 1922 | start_x, start_y = start 1923 | goal_x, goal_y = goal 1924 | 1925 | frontier = PriorityQueue() 1926 | frontier.insert(encode_integer_coordinates(start_x, start_y), 0) 1927 | came_from = {} 1928 | cost_so_far = {encode_integer_coordinates(start_x, start_y): 0} 1929 | 1930 | while not frontier.is_empty(): 1931 | current = frontier.pop() 1932 | current_x, current_y = decode_integer_coordinates(current) 1933 | 1934 | if (current_x, current_y) == goal: 1935 | break 1936 | 1937 | for dx, dy in [ 1938 | (0, -1), 1939 | (0, 1), 1940 | (-1, 0), 1941 | (1, 0), 1942 | (-1, -1), 1943 | (-1, 1), 1944 | (1, -1), 1945 | (1, 1), 1946 | ]: 1947 | next_x, next_y = current_x + dx, current_y + dy 1948 | if ( 1949 | 0 <= next_x < GRID_SIZE 1950 | and 0 <= next_y < GRID_SIZE 1951 | and maze[next_y, next_x] == 0 1952 | ): 1953 | if abs(dx) == 1 and abs(dy) == 1: 1954 | if ( 1955 | maze[current_y + dy, current_x] == 1 1956 | or maze[current_y, current_x + dx] == 1 1957 | ): 1958 | continue 1959 | 1960 | new_cost = cost_so_far[current] + ( 1961 | 1 if abs(dx) + abs(dy) == 1 else 1.414 1962 | ) 1963 | next_node = encode_integer_coordinates(next_x, next_y) 1964 | 1965 | if ( 1966 | next_node not in cost_so_far 1967 | or new_cost < cost_so_far[next_node] 1968 | ): 1969 | cost_so_far[next_node] = new_cost 1970 | priority = new_cost + math.sqrt( 1971 | (goal_x - next_x) ** 2 + (goal_y - next_y) ** 2 1972 | ) 1973 | frontier.insert(next_node, priority) 1974 | came_from[next_node] = current 1975 | exploration_order.append((next_x, next_y)) 1976 | 1977 | if encode_integer_coordinates(goal_x, goal_y) not in came_from: 1978 | print( 1979 | f"No path found from ({start_x}, {start_y}) to ({goal_x}, {goal_y})" 1980 | ) 1981 | return None 1982 | 1983 | path.clear() 1984 | current = encode_integer_coordinates(goal_x, goal_y) 1985 | while current != encode_integer_coordinates(start_x, start_y): 1986 | x, y = decode_integer_coordinates(current) 1987 | path.append((x, y)) 1988 | current = came_from[current] 1989 | path.append((start_x, start_y)) 1990 | path.reverse() 1991 | 1992 | print(f"Path found with length {len(path)}") 1993 | return path 1994 | 1995 | print("Starting pathfinding...") 1996 | result = optimized_a_star((start_x, start_y), (goal_x, goal_y), maze) 1997 | if result is None: 1998 | print( 1999 | f"Failed to find a path for problem {i+1}. Skipping visualization for this problem." 2000 | ) 2001 | continue 2002 | print("Pathfinding complete.") 2003 | 2004 | all_exploration_orders.append(exploration_order) 2005 | all_paths.append(path) 2006 | all_mazes.append(maze) 2007 | all_starts.append((start_x, start_y)) 2008 | all_goals.append((goal_x, goal_y)) 2009 | all_maze_approaches.append(maze_generation_approach) 2010 | 2011 | if not all_paths: 2012 | print("No valid paths found. Skipping this animation.") 2013 | continue 2014 | slowdown_factor = 5 # Corresponds to the slowdown_factor in `generate_and_save_frame`` 2015 | max_frames = max( 2016 | len(exploration_order) + len(path) * slowdown_factor 2017 | for exploration_order, path in zip(all_exploration_orders, all_paths) 2018 | ) 2019 | print(f"Max frames: {max_frames}") 2020 | max_frames = max(1, max_frames) 2021 | num_cores = max(1, os.cpu_count() - 5) # Leave 5 cores for system processes 2022 | max_concurrent_tasks = min( 2023 | num_cores, 16 2024 | ) # Limit to 16 concurrent tasks or number of cores, whichever is smaller 2025 | semaphore = Semaphore(max_concurrent_tasks) 2026 | print( 2027 | f"Using {num_cores} cores for frame generation and a semaphore with {max_concurrent_tasks} permits." 2028 | ) 2029 | 2030 | frame_generator = partial( 2031 | generate_and_save_frame, 2032 | all_mazes=all_mazes, 2033 | all_exploration_orders=all_exploration_orders, 2034 | all_paths=all_paths, 2035 | all_starts=all_starts, 2036 | all_goals=all_goals, 2037 | all_maze_approaches=all_maze_approaches, 2038 | GRID_SIZE=GRID_SIZE, 2039 | wall_color=wall_color, 2040 | floor_color=floor_color, 2041 | start_color=start_color, 2042 | goal_color=goal_color, 2043 | path_color=path_color, 2044 | exploration_cmap=exploration_cmap, 2045 | DPI=DPI, 2046 | output_folder=output_folder, 2047 | frame_format=frame_format, 2048 | ) 2049 | 2050 | async def process_frame(executor, frame): 2051 | async with semaphore: 2052 | return await asyncio.get_event_loop().run_in_executor( 2053 | executor, frame_generator, frame 2054 | ) 2055 | 2056 | # Generate and save frames concurrently using ProcessPoolExecutor and Semaphore 2057 | with ProcessPoolExecutor(max_workers=num_cores) as executor: 2058 | tasks = [process_frame(executor, frame) for frame in range(max_frames)] 2059 | 2060 | pbar = tqdm( 2061 | asyncio.as_completed(tasks), total=max_frames, desc="Generating frames" 2062 | ) 2063 | for i, task in enumerate(pbar, start=1): 2064 | try: 2065 | frame_filename = await task 2066 | pbar.set_description(f"Generated frame {i}/{max_frames}") 2067 | pbar.set_postfix( 2068 | {"Current file": frame_filename.split("/")[-1]}, refresh=True 2069 | ) 2070 | except Exception as e: 2071 | pbar.set_description(f"Error in frame {i}/{max_frames}") 2072 | pbar.set_postfix({"Error": str(e)}, refresh=True) 2073 | 2074 | if i % 100 == 0: # Every 100 frames 2075 | gc.collect() # Force garbage collection 2076 | 2077 | print("\nFrame generation complete.") 2078 | 2079 | # If saving as a video, compile the saved frames 2080 | if not save_as_frames_only: 2081 | fig, axs = plt.subplots(1, num_problems, figsize=(20, 8), dpi=DPI) 2082 | if num_problems == 1: 2083 | axs = [axs] 2084 | 2085 | def update_frame(frame): 2086 | for i, ax in enumerate(axs): 2087 | frame_filename = os.path.join( 2088 | output_folder, f"frame_{frame:05d}.{frame_format}" 2089 | ) 2090 | img = Image.open(frame_filename) 2091 | ax.clear() 2092 | ax.imshow(img) 2093 | ax.axis("off") 2094 | 2095 | anim = FuncAnimation( 2096 | fig, update_frame, frames=max_frames, interval=100, blit=False 2097 | ) 2098 | 2099 | ffmpeg_params = [ 2100 | "-threads", 2101 | str(num_cores), 2102 | "-c:v", 2103 | "libx265", 2104 | "-preset", 2105 | "medium", 2106 | "-crf", 2107 | "25", 2108 | "-x265-params", 2109 | "frame-threads=5:numa-pools=36:wpp=1:pmode=1:pme=1:bframes=8:b-adapt=2:rc-lookahead=60", 2110 | "-movflags", 2111 | "+faststart", 2112 | ] 2113 | print(f"Selected FFmpeg parameters: {ffmpeg_params}") 2114 | 2115 | writer = FFMpegWriter(fps=FPS, codec="libx265", extra_args=ffmpeg_params) 2116 | 2117 | now = datetime.now() 2118 | date_time = now.strftime("%Y%m%d_%H%M%S") 2119 | maze_approach = all_maze_approaches[0] if all_maze_approaches else "unknown" 2120 | filename = f"{maze_approach}_{date_time}.mp4" 2121 | filepath = os.path.join(output_folder, filename) 2122 | 2123 | print("Saving MP4 for encoding with optimized settings...") 2124 | await save_animation_async(anim, filepath, writer, DPI) 2125 | print(f"Animation saved as '{filepath}'") 2126 | delete_small_files(output_folder) 2127 | plt.close(fig) 2128 | remove_old_empty_directories() 2129 | 2130 | 2131 | def test_a_star_implementations(num_tests=100, grid_size=31): 2132 | def optimized_a_star(start, goal, maze): 2133 | start_x, start_y = start 2134 | goal_x, goal_y = goal 2135 | 2136 | frontier = PriorityQueue() 2137 | frontier.insert(encode_integer_coordinates(start_x, start_y), 0) 2138 | came_from = {} 2139 | cost_so_far = {encode_integer_coordinates(start_x, start_y): 0} 2140 | 2141 | while not frontier.is_empty(): 2142 | current = frontier.pop() 2143 | current_x, current_y = decode_integer_coordinates(current) 2144 | 2145 | if (current_x, current_y) == goal: 2146 | break 2147 | 2148 | for dx, dy in [ 2149 | (0, -1), 2150 | (0, 1), 2151 | (-1, 0), 2152 | (1, 0), 2153 | (-1, -1), 2154 | (-1, 1), 2155 | (1, -1), 2156 | (1, 1), 2157 | ]: 2158 | next_x, next_y = current_x + dx, current_y + dy 2159 | if ( 2160 | 0 <= next_x < grid_size 2161 | and 0 <= next_y < grid_size 2162 | and maze[next_y, next_x] == 0 2163 | ): 2164 | if abs(dx) == 1 and abs(dy) == 1: 2165 | if ( 2166 | maze[current_y + dy, current_x] == 1 2167 | or maze[current_y, current_x + dx] == 1 2168 | ): 2169 | continue 2170 | 2171 | new_cost = cost_so_far[current] + ( 2172 | 1 if abs(dx) + abs(dy) == 1 else 1.414 2173 | ) 2174 | next_node = encode_integer_coordinates(next_x, next_y) 2175 | 2176 | if ( 2177 | next_node not in cost_so_far 2178 | or new_cost < cost_so_far[next_node] 2179 | ): 2180 | cost_so_far[next_node] = new_cost 2181 | priority = new_cost + math.sqrt( 2182 | (goal_x - next_x) ** 2 + (goal_y - next_y) ** 2 2183 | ) 2184 | frontier.insert(next_node, priority) 2185 | came_from[next_node] = current 2186 | 2187 | if encode_integer_coordinates(goal_x, goal_y) not in came_from: 2188 | return None 2189 | 2190 | path = [] 2191 | current = encode_integer_coordinates(goal_x, goal_y) 2192 | while current != encode_integer_coordinates(start_x, start_y): 2193 | x, y = decode_integer_coordinates(current) 2194 | path.append((x, y)) 2195 | current = came_from[current] 2196 | path.append((start_x, start_y)) 2197 | path.reverse() 2198 | 2199 | return path 2200 | 2201 | def simple_a_star(start, goal, maze): 2202 | start_x, start_y = start 2203 | goal_x, goal_y = goal 2204 | 2205 | exploration_order = [] 2206 | path = [] 2207 | obstacles = set( 2208 | (x, y) for y, row in enumerate(maze) for x, cell in enumerate(row) if cell 2209 | ) 2210 | 2211 | frontier = PriorityQueue() 2212 | frontier.insert(encode_integer_coordinates(start_x, start_y), 0) 2213 | came_from = {} 2214 | cost_so_far = {encode_integer_coordinates(start_x, start_y): 0} 2215 | 2216 | while not frontier.is_empty(): 2217 | current = frontier.pop() 2218 | current_x, current_y = decode_integer_coordinates(current) 2219 | 2220 | if (current_x, current_y) == (goal_x, goal_y): 2221 | break 2222 | 2223 | for dx, dy in [ 2224 | (0, -1), 2225 | (0, 1), 2226 | (-1, 0), 2227 | (1, 0), 2228 | (-1, -1), 2229 | (-1, 1), 2230 | (1, -1), 2231 | (1, 1), 2232 | ]: 2233 | next_x, next_y = current_x + dx, current_y + dy 2234 | if ( 2235 | 0 <= next_x < grid_size 2236 | and 0 <= next_y < grid_size 2237 | and (next_x, next_y) not in obstacles 2238 | ): 2239 | if abs(dx) == 1 and abs(dy) == 1: 2240 | if (current_x + dx, current_y) in obstacles or ( 2241 | current_x, 2242 | current_y + dy, 2243 | ) in obstacles: 2244 | continue 2245 | 2246 | new_cost = cost_so_far[current] + ( 2247 | 1 if abs(dx) + abs(dy) == 1 else 1.414 2248 | ) 2249 | next_node = encode_integer_coordinates(next_x, next_y) 2250 | 2251 | if ( 2252 | next_node not in cost_so_far 2253 | or new_cost < cost_so_far[next_node] 2254 | ): 2255 | cost_so_far[next_node] = new_cost 2256 | priority = new_cost + math.sqrt( 2257 | (goal_x - next_x) ** 2 + (goal_y - next_y) ** 2 2258 | ) 2259 | frontier.insert(next_node, priority) 2260 | came_from[next_node] = current 2261 | exploration_order.append((next_x, next_y)) 2262 | 2263 | if encode_integer_coordinates(goal_x, goal_y) not in came_from: 2264 | return None 2265 | 2266 | current = encode_integer_coordinates(goal_x, goal_y) 2267 | while current != encode_integer_coordinates(start_x, start_y): 2268 | x, y = decode_integer_coordinates(current) 2269 | path.append((x, y)) 2270 | current = came_from[current] 2271 | path.append((start_x, start_y)) 2272 | path.reverse() 2273 | 2274 | return path 2275 | 2276 | def generate_random_maze(size): 2277 | maze = np.random.choice([0, 1], size=(size, size), p=[0.7, 0.3]) 2278 | maze[0, :] = maze[-1, :] = maze[:, 0] = maze[:, -1] = 1 2279 | return maze 2280 | 2281 | def is_valid_path(path, maze): 2282 | if not path: 2283 | return False 2284 | for x, y in path: 2285 | if maze[y, x] == 1: 2286 | return False 2287 | return True 2288 | 2289 | print(f"Running {num_tests} tests...") 2290 | optimized_times = [] 2291 | simple_times = [] 2292 | for i in range(num_tests): 2293 | maze = generate_random_maze(grid_size) 2294 | start = (random.randint(1, grid_size - 2), random.randint(1, grid_size - 2)) 2295 | goal = (random.randint(1, grid_size - 2), random.randint(1, grid_size - 2)) 2296 | 2297 | while maze[start[1], start[0]] == 1 or maze[goal[1], goal[0]] == 1: 2298 | start = (random.randint(1, grid_size - 2), random.randint(1, grid_size - 2)) 2299 | goal = (random.randint(1, grid_size - 2), random.randint(1, grid_size - 2)) 2300 | 2301 | # Time the optimized implementation 2302 | start_time = time.time() 2303 | optimized_path = optimized_a_star(start, goal, maze) 2304 | optimized_time = time.time() - start_time 2305 | optimized_times.append(optimized_time) 2306 | 2307 | # Time the current implementation 2308 | start_time = time.time() 2309 | current_path = simple_a_star(start, goal, maze) 2310 | simple_time = time.time() - start_time 2311 | simple_times.append(simple_time) 2312 | 2313 | if optimized_path is None and current_path is None: 2314 | print(f"Test {i+1}: Both implementations correctly found no path.") 2315 | elif optimized_path is None or current_path is None: 2316 | print( 2317 | f"Test {i+1}: Mismatch - One implementation found a path, the other didn't." 2318 | ) 2319 | return False 2320 | elif len(optimized_path) != len(current_path): 2321 | print(f"Test {i+1}: Mismatch - Different path lengths.") 2322 | return False 2323 | elif not is_valid_path(optimized_path, maze) or not is_valid_path( 2324 | current_path, maze 2325 | ): 2326 | print(f"Test {i+1}: Invalid path detected.") 2327 | return False 2328 | else: 2329 | print( 2330 | f"Test {i+1}: Both implementations found valid paths of the same length." 2331 | ) 2332 | print(f" Optimized time: {optimized_time:.6f} seconds") 2333 | print(f" Simple time: {simple_time:.6f} seconds") 2334 | 2335 | print("\nAll tests passed successfully!") 2336 | 2337 | # Calculate and print benchmark results 2338 | avg_optimized_time = sum(optimized_times) / len(optimized_times) 2339 | avg_simple_time = sum(simple_times) / len(simple_times) 2340 | speedup = avg_simple_time / avg_optimized_time 2341 | 2342 | print("\nBenchmark Results:") 2343 | print(f"Average Optimized A* Time: {avg_optimized_time:.6f} seconds") 2344 | print(f"Average Simple A* Time: {avg_simple_time:.6f} seconds") 2345 | print(f"Speedup: {speedup:.2f}x") 2346 | 2347 | return True 2348 | 2349 | 2350 | if __name__ == "__main__": 2351 | use_test = 0 2352 | if use_test: 2353 | test_result = test_a_star_implementations(num_tests=20, grid_size=231) 2354 | print(f"Overall test result: {'Passed' if test_result else 'Failed'}") 2355 | 2356 | num_animations = 1 # Set this to the desired number of animations to generate 2357 | GRID_SIZE = 31 # Resolution of the maze grid 2358 | num_problems = 3 # Number of mazes to show side by side in each animation 2359 | DPI = 150 # DPI for the animation 2360 | FPS = 4 # FPS for the animation 2361 | save_as_frames_only = 1 # Set this to 1 to save frames as individual images in a generated sub-folder; 0 to save as a single video as well 2362 | dark_mode = 0 # Change the theme of the maze visualization 2363 | asyncio.run( 2364 | run_complex_examples( 2365 | num_animations, 2366 | GRID_SIZE, 2367 | num_problems, 2368 | DPI, 2369 | FPS, 2370 | save_as_frames_only, 2371 | frame_format="png", 2372 | dark_mode=dark_mode, 2373 | ) 2374 | ) 2375 | --------------------------------------------------------------------------------