├── .gitignore
├── LICENSE
├── README.md
├── main
├── Algorithms.py
├── Colors.py
├── Grid.py
├── Maze.py
├── Node.py
├── Pathfinding.py
├── Testing.py
├── algorithms
│ ├── AStar.py
│ ├── BFS.py
│ ├── BStar.py
│ ├── BeamSearch.py
│ ├── BellmanFord.py
│ ├── BestFS.py
│ ├── DFS.py
│ ├── Dijkstra.py
│ ├── FastMarching.py
│ ├── FloodFill.py
│ ├── FloydWarshall.py
│ ├── Fringe.py
│ ├── GBFS.py
│ ├── GBLS.py
│ ├── IDAStar.py
│ ├── IDDFS.py
│ ├── JPS.py
│ ├── LPAStar.py
│ ├── LexicographicBFS.py
│ ├── RP.py
│ ├── RandomLIFO.py
│ ├── RandomWalk.py
│ ├── SPFA.py
│ ├── ThetaStar.py
│ ├── __init__.py
│ ├── bidirectional
│ │ ├── AStar.py
│ │ ├── BFS.py
│ │ └── __init__.py
│ └── random
│ │ ├── grid_to_tree.md
│ │ ├── tree.png
│ │ └── tree.txt
└── testing
│ ├── AStar.py
│ ├── BFS.py
│ ├── BStar.py
│ ├── BeamSearch.py
│ ├── BellmanFord.py
│ ├── BestFS.py
│ ├── DFS.py
│ ├── Dijkstra.py
│ ├── FastMarching.py
│ ├── FloodFill.py
│ ├── FloydWarshall.py
│ ├── Fringe.py
│ ├── GBFS.py
│ ├── GBLS.py
│ ├── IDAStar.py
│ ├── IDDFS.py
│ ├── JPS.py
│ ├── LPAStar.py
│ ├── LexicographicBFS.py
│ ├── RP.py
│ ├── RandomLIFO.py
│ ├── RandomWalk.py
│ ├── SPFA.py
│ ├── ThetaStar.py
│ ├── __init__.py
│ ├── bidirectional
│ ├── AStar.py
│ ├── BFS.py
│ └── __init__.py
│ └── results
│ └── Generated_Maze-Example.csv
├── resources
├── AlgorithmExplanations.md
├── Best-First-Search.gif
├── OtherAlgorithms.md
├── Thoughts.md
├── astar_maze.gif
├── b_star.pdf
├── d-star.pdf
├── fast_marching.pdf
├── gabows.pdf
├── interesting_ideas.md
├── links.md
└── theta_star.pdf
├── upload.sh
└── website
├── TODO
├── package.json
├── public
├── favicon.ico
├── favicon.xcf
├── index.html
├── logo192.png
├── logo192.xcf
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── algorithms
│ ├── a*.ts
│ ├── b*.ts
│ ├── beamsearch.ts
│ ├── bellford.ts
│ ├── bestfirstsearch.ts
│ ├── bfs.ts
│ ├── dfs.ts
│ ├── dijkstra.ts
│ ├── floodfill.ts
│ ├── floydwarshall.ts
│ ├── fmm.ts
│ ├── fringe.ts
│ ├── gbfs.ts
│ ├── gbls.ts
│ ├── helper.ts
│ ├── ida*.ts
│ ├── iddfs.ts
│ ├── jps.ts
│ ├── lbfs.ts
│ ├── lpa*.ts
│ ├── r-lifo.ts
│ ├── spfa.ts
│ └── theta*.ts
├── components
│ └── Panel.tsx
├── css
│ ├── index.css
│ └── panel.css
└── index.tsx
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | .vscode/
3 | *.ipynb
4 | node_modules/
5 | build/
6 | *.yaml
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pathfinding/Graph Search Algorithms 
2 |
3 | This is a Python (3.10+) implementation and visualization of various pathfinding algorithms.
4 |
5 | There is also a web version of this at [this hyperlink](https://gurknathe.github.io/Pathfinding-Algorithms). However, there are a number of features that are missing including node-to-node updating. So, the final grid will display basically immediately after you click Run. Additionally, not every algorithm is implemented yet.
6 |
7 | The graph used is a static, undirected, unweighted, strongly connected graph (aka, a grid that doesn't change while algorithm is running).
8 |
9 | The visualization is implemented using PyGame. Credits to [Tech With Tim](https://www.youtube.com/watch?v=JtiK0DOeI4A).
10 |
11 | A maze generator is implemented, it has at least one valid path from the start to end node. The implementation is based on [this](https://github.com/OrWestSide/python-scripts/blob/master/maze.py).
12 |
13 | 
14 |
15 | See the explanations for the implemented algorithms [here](./resources/AlgorithmExplanations.md).
16 |
17 | Warning: There is an issue with PyGame that can cause the program to crash. On my system running Linux Mint, the update display function causes a segmentation fault. Non-Windows users seem to be the group affected by this, so if you're using Windows, you should be fine.
18 |
19 | ## Command line arguments
20 |
21 | There are three optional command line arguments: width, # rows, algorithm type.
22 |
23 | Disclaimer: Testing is done on default settings, there is no guarantee, as of now, that things work smoothly for different widths and rows.
24 |
25 | ```
26 | width >= 2
27 | rows >= 2
28 | algorithm = astar, bfs, dfs, dijkstra, rand, yen [See Algorithms.py for full list]
29 | ```
30 |
31 | ```
32 | ./pathfinding.py <# rows>
33 | ```
34 |
35 | ## Keydown Events
36 |
37 | While an algorithm isn't running:
38 |
39 | - Press T to test algorithms
40 | - Press B to go to previous algorithm in list
41 | - Press N to go to next algorithm in list
42 | - Press Q to exit
43 | - Press C to clear grid
44 | - Press G to generate a new maze
45 | - Left Click to place Start, then End, then Obstacles
46 | - Right Click to remove Start, End, or Obstacles
47 |
48 | After an algorithm has run:
49 |
50 | - W, Left Click, or Right Click to clear the Algorithm Mark-up
51 |
52 | While an algorithm is running:
53 |
54 | - Press S key to stop the algorithm
55 |
56 | ## Testing
57 |
58 | The testing function runs every implemented algorithm for the current grid. It outputs the time it took to run the algorithm and the number of node checks while running. The results are written into a CSV file, which can be found in the `main/testing/results` directory. An example output is given for a randomly generated maze, with default settings.
59 |
60 | The testing will take longer as the visitable nodes increases. For my system, and a randomly generated maze on default settings, it takes ~4 minutes to run and save the data. The Floyd-Warshall algorithm takes up the most time and can vary significantly in its execution time (e.g., most of the time I get a run time of ~260 seconds where as of writing this I record a run time of ~670 seconds).
61 |
62 | ## Node Types
63 |
64 | - Start: where the search algorithm will start
65 | - End: where the search algorithm is trying to get to
66 | - Obstacle: a position the algorithms avoid
67 | - Check/Uncheck: markup for visualizing the algorithm
68 | - Path: markup for visualizing the found path
69 | - Default: a position that can be traversed
70 |
71 | ## Algorithm Progress
72 |
73 | ```
74 | Currently implemented algorithms (25/25):
75 |
76 | - A*
77 | - Beam Search
78 | - Bellman Ford's Algorithm
79 | - Best First Search
80 | - Bidirectional A*
81 | - Bidirectional search
82 | - Breadth First Search (BFS)
83 | - B*
84 | - Depth First Search (DFS)
85 | - Dijkstra's Algorithms
86 | - Fast Marching Method (FMM)
87 | - Flood Fill Algorithm
88 | - Floyd-Warshall's algorithm
89 | - Fringe search
90 | - Greedy Best First Search (GBFS)
91 | - Greedy Best Line Search (GBLS)
92 | - Iterative Deepening (IDA*)
93 | - Iterative Deepening DFS (IDDFS)
94 | - Jump Point Search (JPS)
95 | - Lexicographic BFS (LBFS)
96 | - Lifelong Planning A* (LPA*)
97 | - Random Walk
98 | - Random Walk with LIFO Queue
99 | - Shortest Path Faster Algorithm (SPFA)
100 | - Theta*
101 |
102 | Currently looking at:
103 |
104 | - None
105 |
106 | Planned algorithms (Going to look at them):
107 |
108 | - None
109 |
110 | ```
111 |
112 | Possible incorrectly implemented algorithms:
113 |
114 | - [B\*](./resources/b_star.pdf)
115 | - [Jump Point Search](https://en.wikipedia.org/wiki/Jump_point_search)
116 |
--------------------------------------------------------------------------------
/main/Algorithms.py:
--------------------------------------------------------------------------------
1 | from algorithms.AStar import a_star
2 | from algorithms.BeamSearch import beam_search
3 | from algorithms.BellmanFord import bell_ford
4 | from algorithms.BestFS import best_fs
5 | from algorithms.BFS import bfs
6 | from algorithms.bidirectional.BFS import bi_search
7 | from algorithms.bidirectional.AStar import bi_a_star
8 | from algorithms.BStar import b_star
9 | from algorithms.DFS import dfs
10 | from algorithms.Dijkstra import dijkstra
11 | from algorithms.FloodFill import flood_fill
12 | from algorithms.FloydWarshall import floyd_warshall
13 | from algorithms.FastMarching import fmm
14 | from algorithms.Fringe import fringe_search
15 | from algorithms.GBFS import gbfs
16 | from algorithms.GBLS import gbls
17 | from algorithms.IDAStar import ida_star
18 | from algorithms.IDDFS import iddfs
19 | from algorithms.JPS import jps
20 | from algorithms.LexicographicBFS import lbfs
21 | from algorithms.LPAStar import lpa_star
22 | from algorithms.RandomLIFO import rand_lifo
23 | from algorithms.RandomWalk import rand_walk
24 | from algorithms.SPFA import spfa
25 | from algorithms.ThetaStar import theta_star
26 |
27 |
28 | ALGORITHMS = [
29 | "astar",
30 | "beam",
31 | "bellford",
32 | "bestfs",
33 | "bfs",
34 | "biastar",
35 | "bisearch",
36 | "bstar",
37 | "dfs",
38 | "dijkstra",
39 | "flood",
40 | "floyd",
41 | "fmm",
42 | "fringe",
43 | "gbfs",
44 | "gbls",
45 | "ida",
46 | "iddfs",
47 | "jps",
48 | "lbfs",
49 | "lpa",
50 | "rand",
51 | "rand_lifo",
52 | "spfa",
53 | "theta",
54 | ]
55 |
56 | run_algs = {
57 | "astar": "a_star(grid)",
58 | "beam": "beam_search(grid, 50)",
59 | "bellford": "bell_ford(grid, 1)",
60 | "bestfs": "best_fs(grid)",
61 | "bfs": "bfs(grid)",
62 | "biastar": "bi_a_star(grid)",
63 | "bisearch": "bi_search(grid)",
64 | "bstar": "b_star(grid)",
65 | "dfs": "dfs(grid)",
66 | "dijkstra": "dijkstra(grid)",
67 | "flood": "flood_fill(grid)",
68 | "floyd": "floyd_warshall(grid)",
69 | "fmm": "fmm(grid)",
70 | "fringe": "fringe_search(grid)",
71 | "gbfs": "gbfs(grid)",
72 | "gbls": "gbls(grid)",
73 | "ida": "ida_star(grid)",
74 | "iddfs": "iddfs(grid, len(grid.grid) * len(grid.grid[0]))",
75 | "jps": "jps(grid)",
76 | "lbfs": "lbfs(grid)",
77 | "lpa": "lpa_star(grid)",
78 | "rand": "rand_walk(grid)",
79 | "rand_lifo": "rand_lifo(grid)",
80 | "spfa": "spfa(grid)",
81 | "theta": "theta_star(grid)",
82 | }
83 |
84 |
85 | def algorithm(grid: object, algorithm: str, *args):
86 | """
87 | Select and run the specified pathfinding algorithm.
88 |
89 | Args:
90 | grid (Grid): An object representing the current grid
91 | algorithm (str): name of the algorithm to use
92 |
93 | Returns:
94 | None
95 | """
96 | if algorithm in run_algs:
97 | eval(run_algs[algorithm])
98 | else:
99 | a_star(grid)
100 |
--------------------------------------------------------------------------------
/main/Colors.py:
--------------------------------------------------------------------------------
1 | COLORS = {
2 | "RED": (255, 0, 0),
3 | "GREEN": (0, 255, 0),
4 | "BLUE": (0, 0, 255),
5 | "YELLOW": (255, 255, 0),
6 | "WHITE": (255, 255, 255),
7 | "BLACK": (0, 0, 0),
8 | "PURPLE": (128, 0, 128),
9 | "ORANGE": (255, 165, 0),
10 | "GREY": (128, 128, 128),
11 | "TURQUOISE": (64, 224, 208),
12 | }
13 |
--------------------------------------------------------------------------------
/main/Grid.py:
--------------------------------------------------------------------------------
1 | from Node import Node
2 | import pygame
3 |
4 |
5 | class Grid:
6 | def __init__(self, win: object, ROWS: int, width: int):
7 | self.rows = ROWS
8 | self.width = width
9 | self.node_width = width // ROWS
10 | self.win = win
11 | self.grid = []
12 | self.start = None
13 | self.end = None
14 |
15 | self.make_grid()
16 |
17 | def reset_grid(self):
18 | self.start = None
19 | self.end = None
20 | self.make_grid()
21 |
22 | def get_clicked_pos(self, pos: tuple):
23 | """
24 | Get the position of the node that was clicked in the grid.
25 |
26 | Args:
27 | pos (tuple): A tuple containing the x and y coordinates of the mouse click.
28 | rows (int): The number of rows in the grid.
29 | width (int): The width of the grid in pixels.
30 |
31 | Returns:
32 | tuple: A tuple containing the row and column indices of the clicked node.
33 | """
34 | # Unpack the x and y coordinates of the mouse click
35 | y, x = pos
36 |
37 | # Calculate the row and column index of the clicked node
38 | row = y // self.node_width
39 | col = x // self.node_width
40 |
41 | return row, col
42 |
43 | def make_grid(self):
44 | """
45 | Create a 2D list representing the grid of the maze.
46 |
47 | Args:
48 | rows (int): The number of rows in the grid.
49 | width (int): The width of the grid in pixels.
50 |
51 | Returns:
52 | List[List[Node]]: A 2D list of Node objects representing the grid.
53 | """
54 | grid = []
55 |
56 | # Iterate through the rows and columns of the grid
57 | for i in range(self.rows):
58 | grid.append([])
59 | for j in range(self.rows):
60 | # Create a new Node object for the current position and
61 | # add it to the grid
62 | node = Node(i, j, self.node_width, self.rows)
63 | grid[i].append(node)
64 |
65 | self.grid = grid
66 |
67 | def clear_grid(self):
68 | """
69 | Clear the grid by creating a new 2D list of Node objects,
70 | keeping the start, end, and obstacles nodes from the
71 | original grid.
72 |
73 | Args:
74 | current_grid (List[List[Node]]): The original grid.
75 | rows (int): The number of rows in the grid.
76 | width (int): The width of the grid in pixels.
77 |
78 | Returns:
79 | List[List[Node]]: A 2D list of Node objects representing the cleared grid.
80 | """
81 |
82 | # Iterate through the rows and columns of the grid
83 | for i in range(self.rows):
84 | for j in range(self.rows):
85 | # If the current node is not the start, end, or an obstacle, create a new Node
86 | # object for the current position and add it to the grid
87 | if (
88 | not self.grid[i][j].is_start()
89 | and not self.grid[i][j].is_end()
90 | and not self.grid[i][j].is_obstacle()
91 | ):
92 | self.grid[i][j].reset()
93 |
94 | def draw_grid_lines(self):
95 | """
96 | Draw the lines separating the nodes in the grid.
97 |
98 | Args:
99 | None
100 |
101 | Returns:
102 | None
103 | """
104 | for i in range(self.rows):
105 | # Draw a horizontal line at the top and bottom of each node
106 | pygame.draw.line(
107 | self.win,
108 | (128, 128, 128),
109 | (0, i * self.node_width),
110 | (self.width, i * self.node_width),
111 | )
112 | for j in range(self.rows):
113 | # Draw a vertical line at the left and right of each node
114 | pygame.draw.line(
115 | self.win,
116 | (128, 128, 128),
117 | (j * self.node_width, 0),
118 | (j * self.node_width, self.width),
119 | )
120 |
121 | def draw(self):
122 | """
123 | Draw the grid and nodes on the window.
124 |
125 | Args:
126 | None
127 |
128 | Returns:
129 | None
130 | """
131 | self.win.fill((255, 255, 255))
132 |
133 | # Draw nodes on grid
134 | for row in self.grid:
135 | for node in row:
136 | node.draw(self.win)
137 |
138 | self.draw_grid_lines()
139 |
140 | pygame.display.update()
141 |
142 | def __getitem__(self, key):
143 | return self.grid[key]
--------------------------------------------------------------------------------
/main/Node.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from pygame import Rect
3 | from Colors import COLORS
4 |
5 |
6 | # Define colors for different node states
7 | DEFAULT = COLORS.get("WHITE")
8 | CHECKED = COLORS.get("RED")
9 | UNCHECKED = COLORS.get("GREEN")
10 | OBSTACLE = COLORS.get("BLACK")
11 | START = COLORS.get("ORANGE")
12 | END = COLORS.get("TURQUOISE")
13 | PATH = COLORS.get("PURPLE")
14 |
15 |
16 | class Node:
17 | def __init__(self, row: int, col: int, width: int, total_rows: int):
18 | """
19 | Initializes a node with its position, size, and color.
20 |
21 | Args:
22 | row (int): row index of the node in the grid
23 | col (int): column index of the node in the grid
24 | width (int): width of the node
25 | total_rows (int): total number of rows in the grid
26 | """
27 | self.row = row
28 | self.col = col
29 | self.x = row * width
30 | self.y = col * width
31 | self.color = DEFAULT
32 | self.neighbors = []
33 | self.width = width
34 | self.total_rows = total_rows
35 | self.rect = Rect(self.x, self.y, self.width, self.width)
36 |
37 | # Flag for whether the node has been checked
38 | self.been_checked = False
39 |
40 | # Color for checked nodes that have been checked multiple times
41 | self.checked_color = None
42 |
43 | def get_pos(self):
44 | """
45 | Returns the position (row, column) of the node in the grid.
46 |
47 | Returns:
48 | tuple: (row, column) of the node in the grid
49 | """
50 | return self.row, self.col
51 |
52 | # Checking node states
53 | def is_default(self):
54 | return self.color == DEFAULT
55 |
56 | def is_checked(self):
57 | return self.color == CHECKED
58 |
59 | def is_unchecked(self):
60 | return self.color == UNCHECKED
61 |
62 | def is_obstacle(self):
63 | return self.color == OBSTACLE
64 |
65 | def is_start(self):
66 | return self.color == START
67 |
68 | def is_end(self):
69 | return self.color == END
70 |
71 | # Setting node states
72 | def check(self):
73 | """
74 | Marks the node as checked and sets the checked flag to True.
75 | """
76 | self.color = CHECKED
77 | self.been_checked = True
78 | self.checked_color = CHECKED
79 |
80 | def uncheck(self):
81 | self.color = UNCHECKED
82 |
83 | def make_obstacle(self):
84 | self.color = OBSTACLE
85 |
86 | def make_start(self):
87 | self.color = START
88 |
89 | def make_end(self):
90 | self.color = END
91 |
92 | def make_path(self):
93 | self.color = PATH
94 |
95 | def make_color(self, color: tuple):
96 | """
97 | Makes the node the specified color.
98 |
99 | Args:
100 | color (tuple): tuple of (R, G, B) values representing the
101 | desired color of the node
102 |
103 | Returns:
104 | None
105 | """
106 | self.color = color
107 |
108 | def mult_check(self, dec: int):
109 | """
110 | Changes the color of a checked node that has been checked multiple times.
111 |
112 | Args:
113 | dec (int): value to change the color by
114 |
115 | Returns:
116 | None
117 | """
118 | if self.checked_color[0] > dec:
119 | self.color = (self.checked_color[0] - dec, 0, self.checked_color[2] + dec)
120 | self.checked_color = self.color
121 | else:
122 | self.color = self.checked_color
123 |
124 | def reset(self):
125 | self.color = DEFAULT
126 | self.been_checked = False
127 | self.checked_color = None
128 |
129 | # Visualization functions
130 | def draw(self, win: object):
131 | """
132 | Draws the node on the given window.
133 |
134 | Args:
135 | win (pygame.Surface): window to draw the node on
136 |
137 | Returns:
138 | None
139 | """
140 | pygame.draw.rect(win, self.color, self.rect)
141 |
142 | def update_neighbors(self, grid: list):
143 | """
144 | Updates the neighbors of the node in the grid.
145 |
146 | Args:
147 | grid (List[List[Node]]): 2D list of nodes representing the grid
148 |
149 | Returns:
150 | None
151 | """
152 | self.neighbors = []
153 |
154 | # BOTTOM CELL
155 | if (
156 | self.row < self.total_rows - 1
157 | and not grid[self.row + 1][self.col].is_obstacle()
158 | ):
159 | self.neighbors.append(grid[self.row + 1][self.col])
160 |
161 | # TOP CELL
162 | if self.row > 0 and not grid[self.row - 1][self.col].is_obstacle():
163 | self.neighbors.append(grid[self.row - 1][self.col])
164 |
165 | # LEFT CELL
166 | if self.col > 0 and not grid[self.row][self.col - 1].is_obstacle():
167 | self.neighbors.append(grid[self.row][self.col - 1])
168 |
169 | # RIGHT CELL
170 | if (
171 | self.col < self.total_rows - 1
172 | and not grid[self.row][self.col + 1].is_obstacle()
173 | ):
174 | self.neighbors.append(grid[self.row][self.col + 1])
175 |
176 | def __lt__(self, other: object):
177 | """
178 | Comparison operator for the priority queue.
179 |
180 | Args:
181 | other (Node): node to compare to
182 |
183 | Returns:
184 | bool: False (nodes are not meant to be sorted)
185 | """
186 | return False
187 |
--------------------------------------------------------------------------------
/main/Testing.py:
--------------------------------------------------------------------------------
1 | import time
2 | import csv
3 | from os import system, name, path
4 |
5 | from testing.AStar import a_star
6 | from testing.BeamSearch import beam_search
7 | from testing.BellmanFord import bell_ford
8 | from testing.BestFS import best_fs
9 | from testing.BFS import bfs
10 | from testing.bidirectional.AStar import bi_a_star
11 | from testing.bidirectional.BFS import bi_search
12 | from testing.BStar import b_star
13 | from testing.DFS import dfs
14 | from testing.Dijkstra import dijkstra
15 | from testing.FloydWarshall import floyd_warshall
16 | from testing.FloodFill import flood_fill
17 | from testing.FastMarching import fmm
18 | from testing.Fringe import fringe_search
19 | from testing.GBFS import gbfs
20 | from testing.GBLS import gbls
21 | from testing.IDAStar import ida_star
22 | from testing.IDDFS import iddfs
23 | from testing.JPS import jps
24 | from testing.LexicographicBFS import lbfs
25 | from testing.LPAStar import lpa_star
26 | from testing.RandomLIFO import rand_lifo
27 | from testing.RandomWalk import rand_walk
28 | from testing.SPFA import spfa
29 | from testing.ThetaStar import theta_star
30 | from testing.RP import get_unvisited_nodes
31 |
32 | from Algorithms import ALGORITHMS, run_algs
33 | from Node import Node
34 |
35 |
36 | def testing(grid: object):
37 | """
38 | Main function for testing
39 |
40 | Args:
41 | grid (Grid): An object representing the current grid
42 |
43 | Returns:
44 | None
45 | """
46 |
47 | # Clear grid of any potential markup
48 | grid.clear_grid()
49 |
50 | for row in grid.grid:
51 | for node in row:
52 | node.update_neighbors(grid.grid)
53 |
54 | animation: list = [
55 | "[■□□□□□□□□□]",
56 | "[■■□□□□□□□□]",
57 | "[■■■□□□□□□□]",
58 | "[■■■■□□□□□□]",
59 | "[■■■■■□□□□□]",
60 | "[■■■■■■□□□□]",
61 | "[■■■■■■■□□□]",
62 | "[■■■■■■■■□□]",
63 | "[■■■■■■■■■□]",
64 | "[■■■■■■■■■■]",
65 | ]
66 | completion: int = 0
67 |
68 | # Clear the terminal of all text
69 | clear()
70 |
71 | print("Testing Algorithms...")
72 |
73 | # Creates a new CSV file using the current time
74 | with open(
75 | path.join(
76 | path.dirname(__file__),
77 | f"testing/results/{int(time.time())}.csv",
78 | ),
79 | "w",
80 | newline="",
81 | ) as myfile:
82 | wr = csv.writer(myfile, delimiter=",", quotechar="|", quoting=csv.QUOTE_MINIMAL)
83 |
84 | wr.writerow(
85 | [
86 | "Algorithm",
87 | "Local Run Time",
88 | "Times Nodes Checked",
89 | "Path Length",
90 | "General Data",
91 | ]
92 | )
93 | algo_data: list = [
94 | [
95 | "",
96 | "",
97 | "",
98 | "",
99 | f"Total Open Nodes: {len(get_unvisited_nodes(grid.start))}",
100 | ]
101 | ]
102 |
103 | for alg in ALGORITHMS:
104 | # Clear grid after every run
105 | grid.clear_grid()
106 |
107 | for row in grid.grid:
108 | for node in row:
109 | node.update_neighbors(grid.grid)
110 |
111 | # Run and get statistics for ran algorithm
112 | start_time = time.perf_counter()
113 | num_visited_nodes, path_size = algorithm(grid, alg)
114 | end_time = time.perf_counter()
115 |
116 | # Used for handling Theta* turn points
117 | # ! Will most likely change in the future when more data is being collected
118 | # ! from each algorithm
119 | if type(path_size) == tuple:
120 | algo_data.append(
121 | [
122 | alg,
123 | end_time - start_time,
124 | num_visited_nodes,
125 | path_size[1],
126 | f"Turn points: {path_size[0]}",
127 | ]
128 | )
129 | else:
130 | algo_data.append(
131 | [alg, end_time - start_time, num_visited_nodes, path_size]
132 | )
133 |
134 | # Progress markup
135 | # ? Unreplicable error with printing the completion animation
136 | completion += 1
137 | index = int((completion / len(ALGORITHMS)) * len(animation))
138 |
139 | if alg == ALGORITHMS[-1]:
140 | break
141 |
142 | print(
143 | f" {int(100 * (completion / len(ALGORITHMS)))}% {animation[index]} \r",
144 | flush=True,
145 | end="",
146 | )
147 |
148 | print(f"100% {animation[9]} \rTesting Finished!\n", flush=True, end="")
149 |
150 | wr.writerows(algo_data)
151 | grid.clear_grid()
152 |
153 |
154 | def clear():
155 | # For Windows
156 | if name == "nt":
157 | _ = system("cls")
158 | # For MacOS and Linux
159 | else:
160 | _ = system("clear")
161 |
162 |
163 | def algorithm(grid: object, algorithm: str):
164 | """
165 | Select and run the specified pathfinding algorithm.
166 |
167 | Args:
168 | grid (Grid): An object representing the current grid
169 | algorithm (str): name of the algorithm to use
170 |
171 | Returns:
172 | int: The number of nodes visited in the algorithm
173 | int or tuple: Length of path found; number of turning points
174 | """
175 | return eval(run_algs[algorithm])
176 |
--------------------------------------------------------------------------------
/main/algorithms/AStar.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import PriorityQueue
3 | from .RP import reconstruct_path, heuristic, check
4 |
5 |
6 | def a_star(grid: object):
7 | """
8 | Perform an A* search from start to end.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 |
13 | Returns:
14 | None: The function updates the screen with the search progress and path.
15 | """
16 |
17 | # Initialize counters and sets
18 | count = 0
19 | open_set = PriorityQueue()
20 | open_set.put((0, count, grid.start))
21 | came_from = {}
22 |
23 | # Initialize dictionaries to store the g scores for each node
24 | g_score = {node: float("inf") for row in grid.grid for node in row}
25 | g_score[grid.start] = 0
26 |
27 | # Initialize a set to store the nodes in the open set
28 | open_set_hash = {grid.start}
29 |
30 | # Initialize a flag to track whether the search should continue
31 | run = True
32 |
33 | # Perform the search
34 | while not open_set.empty() and run:
35 | # Check for exit events
36 | run = check(pygame.event.get(), run)
37 |
38 | # Get the current node from the open set
39 | _, _, current = open_set.get()
40 | open_set_hash.remove(current)
41 |
42 | # End the search if the current node is the end node
43 | if current.is_end():
44 | reconstruct_path(came_from, grid.end, grid.draw)
45 | grid.end.make_end()
46 | break
47 |
48 | # Check the neighbors of the current node
49 | for neighbor in current.neighbors:
50 | # Calculate the tentative g score for the neighbor
51 | temp_g_score = g_score[current] + 1
52 |
53 | # Update the g score for the neighbor if the tentative g score is lower
54 | if temp_g_score < g_score[neighbor]:
55 | came_from[neighbor] = current
56 | g_score[neighbor] = temp_g_score
57 |
58 | # Add the neighbor to the open set if it is not already there
59 | if neighbor not in open_set_hash:
60 | f_score = temp_g_score + heuristic("manhattan", neighbor, grid.end)
61 | count += 1
62 | open_set.put((f_score, count, neighbor))
63 | open_set_hash.add(neighbor)
64 |
65 | if not neighbor.is_start() and not neighbor.is_end():
66 | neighbor.uncheck()
67 |
68 | # Update the screen with the search progress
69 | grid.draw()
70 |
71 | # Check the current node if it is not the start node
72 | if not current.is_start():
73 | current.check()
74 |
--------------------------------------------------------------------------------
/main/algorithms/BFS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from .RP import reconstruct_path, check, markup
3 |
4 |
5 | def bfs(grid: object):
6 | """
7 | Perform a breadth-first search from start to end.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid
11 |
12 | Returns:
13 | None: The function updates the screen with the search progress and path.
14 | """
15 |
16 | # Initialize a list to store the nodes to visit (queue)
17 | nodes = []
18 | nodes.append(grid.start)
19 |
20 | # Initialize a dictionary to store the predecessor of each node
21 | previous = {}
22 |
23 | # Initialize flags to track the search status
24 | found = False
25 | run = True
26 |
27 | # Perform the search
28 | while nodes and not found and run:
29 | # Check for exit events
30 | run = check(pygame.event.get(), run)
31 |
32 | # Get the next node to visit
33 | current = nodes.pop(0)
34 |
35 | # Skip the node if it has already been checked
36 | if current.is_checked():
37 | continue
38 |
39 | # Draw the current node
40 | markup(grid.draw, current)
41 |
42 | # Check the neighbors of the current node
43 | for neighbor in current.neighbors:
44 | # Skip the neighbor if it has already been checked
45 | if not neighbor.is_checked():
46 | # End the search if the neighbor is the end node
47 | if neighbor.is_end():
48 | previous[neighbor] = current
49 | found = True
50 | reconstruct_path(previous, grid.end, grid.draw)
51 | break
52 | # Add the neighbor to the list of nodes to visit if it is not the end node
53 | else:
54 | previous[neighbor] = current
55 | nodes.append(neighbor)
56 |
57 | # Uncheck the child if it is not the start or end node
58 | # for markup
59 | if not neighbor.is_start() and not neighbor.is_end():
60 | neighbor.uncheck()
61 |
--------------------------------------------------------------------------------
/main/algorithms/BStar.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import PriorityQueue
3 | from .RP import reconstruct_path, check, heuristic
4 |
5 |
6 | def b_star(grid: object):
7 | """
8 | Perform a B* search from start to end.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 |
13 | Returns:
14 | None: The function updates the screen with the search progress and path.
15 | """
16 |
17 | # Initialize counters and sets
18 | count = 0
19 | open_set = PriorityQueue()
20 | open_set.put((0, count, grid.start))
21 | came_from = {}
22 |
23 | # Initialize dictionaries to store the g scores for each node
24 | g_score = {node: float("inf") for row in grid.grid for node in row}
25 | g_score[grid.start] = 0
26 |
27 | # Initialize a set to store the nodes in the open set
28 | open_set_hash = {grid.start}
29 |
30 | # Initialize a flag to track whether the search should continue
31 | run = True
32 |
33 | # Perform the search
34 | while not open_set.empty() and run:
35 | # Check for exit events
36 | run = check(pygame.event.get(), run)
37 |
38 | # Get the current node from the open set
39 | _, _, current = open_set.get()
40 | open_set_hash.remove(current)
41 |
42 | # End the search if the current node is the end node
43 | if current.is_end():
44 | reconstruct_path(came_from, grid.end, grid.draw)
45 | grid.end.make_end()
46 | break
47 |
48 | # Check the neighbors of the current node
49 | for neighbor in current.neighbors:
50 | # Calculate the tentative g score for the neighbor
51 | temp_g_score = g_score[current] + 1
52 |
53 | # Update the g and f scores for the neighbor if the
54 | # tentative g score is lower
55 | if temp_g_score < g_score[neighbor]:
56 | came_from[neighbor] = current
57 | g_score[neighbor] = temp_g_score
58 |
59 | # Add the neighbor to the open set if it is not already there
60 | if neighbor not in open_set_hash:
61 | f_score = temp_g_score + heuristic("d_manhattan", neighbor, grid.end)
62 | count += 1
63 | open_set.put((f_score, count, neighbor))
64 | open_set_hash.add(neighbor)
65 |
66 | if not neighbor.is_start() and not neighbor.is_end():
67 | neighbor.uncheck()
68 |
69 | # Update the screen with the search progress
70 | grid.draw()
71 |
72 | # Check the current node if it is not the start node
73 | if not current.is_start():
74 | current.check()
75 |
--------------------------------------------------------------------------------
/main/algorithms/BeamSearch.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import heapq
3 | from .RP import reconstruct_path, heuristic, check, markup
4 |
5 |
6 | def beam_search(grid: object, beam_size: int):
7 | """
8 | Perform a beam search from start to end with a given beam size.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 | beam_size (int): The maximum number of nodes to consider at each step.
13 |
14 | Returns:
15 | None: The function updates the screen with the search progress and path.
16 | """
17 |
18 | # Initialize the beam with the root node
19 | beam = [(0, grid.start)]
20 |
21 | # Initialize a dictionary to store the previous nodes for each node
22 | previous = {}
23 | previous[grid.start] = grid.start
24 |
25 | # Initialize a flag to track whether the search should continue
26 | run = True
27 |
28 | # Perform the search
29 | while beam and run:
30 | # Check for exit events
31 | run = check(pygame.event.get(), run)
32 |
33 | # Get the most promising node from the beam
34 | _, current = heapq.heappop(beam)
35 |
36 | # End the search if the current node is the end node
37 | if current.is_end():
38 | reconstruct_path(previous, grid.end, grid.draw)
39 | grid.end.make_end()
40 | break
41 |
42 | # Draw the current node
43 | markup(grid.draw, current)
44 |
45 | # Expand the current node and add its children to the beam
46 | children = current.neighbors
47 |
48 | # Remove the previous node from the list of children
49 | if previous[current] in children:
50 | children.remove(previous[current])
51 |
52 | # Add the children to the beam
53 | for child in children:
54 | # Skip the child if it has already been checked
55 | if not child.is_checked():
56 | previous[child] = current
57 | heapq.heappush(beam, (heuristic("manhattan", child, grid.end), child))
58 |
59 | # Uncheck the child if it is not the start or end node
60 | # for markup
61 | if not child.is_start() and not child.is_end():
62 | child.uncheck()
63 |
64 | # Trim the beam to the desired size
65 | beam = beam[:beam_size]
66 |
--------------------------------------------------------------------------------
/main/algorithms/BellmanFord.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import Queue
3 | from .RP import reconstruct_path, get_unvisited_nodes, check, markup
4 |
5 |
6 | def bell_ford(grid: object, accuracy: float):
7 | """
8 | Perform a Bellman-Ford search from start to end with a given accuracy.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 | accuracy (float): The fraction of nodes to visit at each step. (0, 1]
13 |
14 | Returns:
15 | None: The function updates the screen with the search progress and path.
16 | """
17 |
18 | # Get the list of unvisited nodes
19 | nodes = get_unvisited_nodes(grid.start)
20 |
21 | # Initialize dictionaries to store the distance and predecessor for each node
22 | distance = {node: float("inf") for node in nodes}
23 | predecessor = {}
24 |
25 | # Set the distance of the start node to 0
26 | distance[grid.start] = 0
27 |
28 | # Calculate the number of nodes to visit at each step
29 | counter = int((len(nodes) - 1) * accuracy)
30 |
31 | # Initialize a flag to track whether the search should continue
32 | run = True
33 |
34 | # Perform the search
35 | while counter >= 0 and run:
36 | # Check for exit events
37 | run = check(pygame.event.get(), run)
38 |
39 | # Visit each node
40 | for current in nodes:
41 | # Check for exit events
42 | run = check(pygame.event.get(), run)
43 |
44 | # Exit the loop if the user interrupted
45 | if not run:
46 | break
47 |
48 | # Draw the current node
49 | markup(grid.draw, current)
50 |
51 | # Check the neighbors of the current node
52 | for neighbor in current.neighbors:
53 | # Update the distance and predecessor for the neighbor if the
54 | # current distance is shorter
55 | if distance[current] + 1 < distance[neighbor]:
56 | distance[neighbor] = distance[current] + 1
57 | predecessor[neighbor] = current
58 |
59 | # Uncheck the neighbor if it is not the start or end node
60 | # for markup
61 | if not neighbor.is_end() and not neighbor.is_start():
62 | neighbor.uncheck()
63 |
64 | # Decrement the counter
65 | counter -= 1
66 |
67 | # Draw the path from the end node to the start node
68 | if run:
69 | reconstruct_path(predecessor, grid.end, grid.draw)
70 |
--------------------------------------------------------------------------------
/main/algorithms/BestFS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import PriorityQueue
3 | from .RP import heuristic, check, markup
4 |
5 |
6 | def get_checked_neighbors(node: object):
7 | """
8 | Get the neighbors of a node that have been checked or are the start node.
9 |
10 | Args:
11 | node (Node): The node to check.
12 |
13 | Returns:
14 | List[Node]: The list of checked or start neighbors.
15 | """
16 |
17 | checked = []
18 | for neighbor in node.neighbors:
19 | if neighbor.is_checked() or neighbor.is_start():
20 | checked.append(neighbor)
21 | return checked
22 |
23 |
24 | def reconstruct_path(current: object, costs: dict, draw: object):
25 | """
26 | Reconstruct the path from the end node to the start node.
27 |
28 | Args:
29 | current (Node): The current node in the search.
30 | costs (Dict[Node, int]): The cost of reaching each node from the start.
31 | draw (function): A function used to draw the search on the screen.
32 |
33 | Returns:
34 | None: The function updates the screen with the path.
35 | """
36 |
37 | # Continue until the current node is the start node
38 | while not current.is_start():
39 | # Find the neighbor with the lowest cost
40 | current = min(get_checked_neighbors(current), key=lambda x: costs[x])
41 |
42 | # Break if the current node is the start node
43 | if current.is_start():
44 | break
45 |
46 | # Draw the path to the current node
47 | current.make_path()
48 | draw()
49 |
50 |
51 | def best_fs(grid: object):
52 | """
53 | Perform a best-first search from start to end.
54 |
55 | Args:
56 | grid (Grid): An object representing the current grid
57 |
58 | Returns:
59 | None: The function updates the screen with the search progress and path.
60 | """
61 |
62 | # Initialize the priority queue with the start node
63 | queue = PriorityQueue()
64 | queue.put((0, 0, grid.start))
65 | count = 0
66 |
67 | # Initialize a dictionary to store the cost of reaching each node from the start
68 | costs = {grid.start: 0}
69 |
70 | # Initialize a flag to track whether the search should continue
71 | run = True
72 |
73 | # Perform the search
74 | while not queue.empty() and run:
75 | # Check for exit events
76 | run = check(pygame.event.get(), run)
77 |
78 | # Get the node with the lowest cost from the queue
79 | cost, _, current = queue.get()
80 |
81 | # End the search if the current node is the end node
82 | if current.is_end():
83 | reconstruct_path(current, costs, grid.draw)
84 | grid.end.make_end()
85 | break
86 |
87 | # Draw the current node
88 | markup(grid.draw, current)
89 |
90 | # Check the neighbors of the current node
91 | for neighbor in current.neighbors:
92 | # Calculate the cost of reaching the neighbor
93 | cost = costs[current] + 1
94 |
95 | # Update the cost in the dictionary if it is lower than the current value
96 | if neighbor not in costs or cost < costs[neighbor]:
97 | costs[neighbor] = cost
98 | # Add the neighbor to the queue with the calculated cost as the priority
99 | count += 1
100 | queue.put(
101 | (
102 | cost + heuristic("manhattan", neighbor, grid.end),
103 | count,
104 | neighbor,
105 | )
106 | )
107 |
108 | # Uncheck the child if it is not the start or end node
109 | # for markup
110 | if not neighbor.is_start() and not neighbor.is_end():
111 | neighbor.uncheck()
112 |
--------------------------------------------------------------------------------
/main/algorithms/DFS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from .RP import reconstruct_path, check, markup
3 |
4 |
5 | def dfs(grid: object):
6 | """
7 | Perform a depth-first search from start to end.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid
11 |
12 | Returns:
13 | None: The function updates the screen with the search progress and path.
14 | """
15 |
16 | # Initialize a stack to store the nodes to visit
17 | stack = [grid.start]
18 |
19 | # Initialize a dictionary to store the predecessor of each node
20 | previous = {}
21 |
22 | # Initialize flags to track the search status
23 | found = False
24 | run = True
25 |
26 | # Perform the search
27 | while len(stack) and not found and run:
28 | # Check for exit events
29 | run = check(pygame.event.get(), run)
30 |
31 | # Get the next node to visit
32 | current = stack.pop()
33 |
34 | # Skip the node if it has already been checked
35 | if current.is_checked():
36 | continue
37 |
38 | # Draw the current node
39 | markup(grid.draw, current)
40 |
41 | # Check the neighbors of the current node
42 | for neighbor in current.neighbors:
43 | # Skip the neighbor if it has already been checked
44 | if not neighbor.is_checked():
45 | # End the search if the neighbor is the end node
46 | if neighbor.is_end():
47 | previous[neighbor] = current
48 | found = True
49 | reconstruct_path(previous, grid.end, grid.draw)
50 | break
51 | # Add the neighbor to the stack of nodes to visit if it is not the end node
52 | else:
53 | previous[neighbor] = current
54 | stack.append(neighbor)
55 | # Uncheck the neighbor if it is not the start or end node
56 | # for markup
57 | if not neighbor.is_start():
58 | neighbor.uncheck()
59 |
60 |
--------------------------------------------------------------------------------
/main/algorithms/Dijkstra.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import PriorityQueue
3 | from .RP import reconstruct_path, check, markup
4 |
5 |
6 | def dijkstra(grid: object):
7 | """
8 | Perform Dijkstra's algorithm from start to end.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 |
13 | Returns:
14 | None: The function updates the screen with the search progress and path.
15 | """
16 |
17 | # Initialize a priority queue to store the nodes to visit
18 | queue = PriorityQueue()
19 |
20 | # Set up the node values
21 | distance = {node: float("inf") for row in grid.grid for node in row}
22 | distance[grid.start] = 0
23 |
24 | # Holds the path from start to end
25 | previous = {}
26 |
27 | # Add the start node to the priority queue
28 | queue.put((0, 0, grid.start))
29 | count = 0
30 |
31 | # Initialize a flag to track the search status
32 | run = True
33 |
34 | # Perform the search
35 | while not queue.empty() and run:
36 | # Check for exit events
37 | run = check(pygame.event.get(), run)
38 |
39 | # Get the next node to visit
40 | _, _, current_min = queue.get()
41 |
42 | # End the search if the current node is the end node
43 | if current_min.is_end():
44 | reconstruct_path(previous, grid.end, grid.draw)
45 | break
46 |
47 | # Draw the current node
48 | markup(grid.draw, current_min)
49 |
50 | # Check the neighbors of the current node
51 | for neighbor in current_min.neighbors:
52 | # Don't recheck for performance
53 | if not neighbor.is_checked():
54 | # edges between vertecies are not weighted
55 | # (using constant weight of 1)
56 | temp_value = distance[current_min] + 1
57 | if temp_value < distance[neighbor]:
58 | distance[neighbor] = temp_value
59 | previous[neighbor] = current_min
60 |
61 | # Add the neighbor to the priority queue
62 | count += 1
63 | queue.put((distance[neighbor], count, neighbor))
64 |
65 | # Uncheck the neighbor if it is not the start or end node
66 | # for markup
67 | if not neighbor.is_end() and not neighbor.is_start():
68 | neighbor.uncheck()
69 |
--------------------------------------------------------------------------------
/main/algorithms/FastMarching.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import heapq
3 | from .RP import check, reconstruct_path, markup
4 |
5 | def fmm(grid: object):
6 | """
7 | Performs the Fast Marching Method on a uniform cost grid
8 | to solve the shortest path problem.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 |
13 | Returns:
14 | None: The function updates the screen with the search progress and path.
15 | """
16 | # Initialize dictionary to store the cost for each node
17 | costs = {node: float("inf") for row in grid.grid for node in row}
18 | costs[grid.start] = 0
19 |
20 | # Record all the unvisited nodes in the grid
21 | far = [node for row in grid.grid for node in row if not node.is_start()]
22 |
23 | # Initialize the visited priority queue
24 | considered = []
25 | heapq.heappush(considered, (0, grid.start))
26 |
27 | came_from = {}
28 |
29 | run = True
30 |
31 | # Perform the search
32 | while considered and run:
33 | # Check for exit events
34 | run = check(pygame.event.get(), run)
35 |
36 | # Get the current node from the open set
37 | ccost, current = heapq.heappop(considered)
38 |
39 | # End the search if the current node is the end node
40 | if current.is_end():
41 | reconstruct_path(came_from, grid.end, grid.draw)
42 | break
43 |
44 | # Draw the current node
45 | markup(grid.draw, current)
46 |
47 | # Check the neighbors of the current node
48 | for neighbor in current.neighbors:
49 | # If the neighbor hasn't been checked
50 | if neighbor not in considered and not neighbor.is_checked():
51 | # Edges between nodes are not weighted
52 | # (using constant weight of 1)
53 | cost = ccost + 1
54 | if cost < costs[neighbor]:
55 | costs[neighbor] = cost
56 | came_from[neighbor] = current
57 |
58 | # If neighbor hasn't been checked or been visited before
59 | # add it to the queue
60 | if neighbor in far:
61 | heapq.heappush(considered, (cost, neighbor))
62 | far.remove(neighbor)
63 |
64 | # Uncheck the neighbor if it is not the start or end node
65 | # for markup
66 | if not neighbor.is_start() and not neighbor.is_end():
67 | neighbor.uncheck()
--------------------------------------------------------------------------------
/main/algorithms/FloodFill.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import heapq
3 | from .RP import check, markup
4 |
5 | def flood_fill(grid: object):
6 | # Create a dictionary to store distance values
7 | distances = { node: float("inf") for row in grid for node in row }
8 | distances[grid.end] = 0
9 |
10 | # Start from end
11 | queue = [(0, grid.end)]
12 | run = True
13 |
14 | while queue and run:
15 | # Check for exit events
16 | run = check(pygame.event.get(), run)
17 |
18 | dist, current = heapq.heappop(queue)
19 |
20 | if current.is_checked():
21 | continue
22 |
23 | if current.is_start():
24 | break
25 |
26 | markup(grid.draw, current)
27 |
28 | # Check every neighbor of the current cell and
29 | # add it to the queue if it hasn't been checked before
30 | for neighbor in current.neighbors:
31 | if not neighbor.is_checked():
32 | distances[neighbor] = dist + 1
33 | heapq.heappush(queue, (dist + 1, neighbor))
34 |
35 | if not neighbor.is_start() and not neighbor.is_end():
36 | neighbor.uncheck()
37 |
38 | # Create path if the algorithm wasn't stopped
39 | if run and distances[grid.start] != float("inf"):
40 | current = grid.start
41 | found = False
42 | while not found:
43 | best = (float("inf"), None)
44 | for neighbor in current.neighbors:
45 | if neighbor.is_end():
46 | found = True
47 | break
48 | if distances[neighbor] < best[0]:
49 | best = (distances[neighbor], neighbor)
50 | if not found:
51 | best[1].make_path()
52 | grid.draw()
53 | current = best[1]
54 |
--------------------------------------------------------------------------------
/main/algorithms/Fringe.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from .RP import heuristic, markup, check
3 |
4 | def reconstruct_path(cache: dict, current: object, draw: object):
5 | """
6 | Iterates through the cache and constructs the path using the data stored.
7 |
8 | Args:
9 | came_from (Dict[Node, Node]): A dictionary containing the nodes traversed
10 | during the pathfinding algorithm.
11 | current (Node): The end node of the path.
12 | draw (function): A function for drawing the maze.
13 |
14 | Returns:
15 | None
16 | """
17 | while not current.is_start():
18 | if not current.is_end():
19 | current.make_path()
20 | _, current = cache[current]
21 | draw()
22 |
23 | def fringe_search(grid: object):
24 | """
25 | Performs a Fringe Search over the grid.
26 |
27 | Args:
28 | grid (Grid): An object representing the current grid
29 |
30 | Returns:
31 | None: The function updates the screen with the search progress and path.
32 | """
33 | # Initialize data structures
34 | fringe = []
35 | cache = {}
36 | cache[grid.start] = (0, None)
37 | fringe.append(grid.start)
38 |
39 | f_limit = heuristic("manhattan", grid.start, grid.end)
40 |
41 | found = False
42 | run = True
43 |
44 | while not found and run and fringe:
45 | # Check for exit events
46 | run = check(pygame.event.get(), run)
47 |
48 | f_min = float("inf")
49 |
50 | # Check every node in the fringe
51 | for node in fringe:
52 | g, parent = cache[node]
53 |
54 | # Get f-score for current node
55 | f = g + heuristic("manhattan", node, grid.end)
56 |
57 | # Check if the f-score is greater than the allowed limit
58 | if f > f_limit:
59 | f_min = min(f, f_min)
60 | continue
61 | if node.is_end():
62 | reconstruct_path(cache, node, grid.draw)
63 | found = True
64 | break
65 |
66 | markup(grid.draw, node)
67 |
68 | for child in node.neighbors:
69 | g_child = g + 1
70 | # If the child node has already been seen
71 | if child in cache:
72 | g_cached, c_parent = cache[child]
73 | if g_child >= g_cached:
74 | continue
75 |
76 | if child in fringe:
77 | fringe.remove(child)
78 | fringe.append(child)
79 | cache[child] = (g_child, node)
80 |
81 | if not child.is_start() and not child.is_end():
82 | child.uncheck()
83 | fringe.remove(node)
84 | f_limit = f_min
--------------------------------------------------------------------------------
/main/algorithms/GBFS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import PriorityQueue
3 | from .RP import reconstruct_path, heuristic, check, markup
4 |
5 |
6 | def gbfs(grid: object):
7 | """
8 | Perform a greedy best-first search from start to end.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 |
13 | Returns:
14 | None: The function updates the screen with the search progress and path.
15 | """
16 |
17 | # Initialize priority queue with the start node
18 | Q = PriorityQueue()
19 | Q.put((heuristic("manhattan", grid.start, grid.end), 0, grid.start))
20 |
21 | # Initialize counters and flags
22 | counter = 0
23 | run = True
24 | found = False
25 |
26 | # Initialize a dictionary to store the previous nodes for each node
27 | previous = {}
28 |
29 | # Perform the search
30 | while not Q.empty() and run and not found:
31 | # Check for exit events
32 | run = check(pygame.event.get(), run)
33 |
34 | # Get the current node from the queue
35 | _, _, current = Q.get()
36 |
37 | # Skip if the node has already been checked
38 | if current.is_checked():
39 | continue
40 |
41 | # Draw the current node
42 | markup(grid.draw, current)
43 |
44 | # Check the neighbors of the current node
45 | for neighbor in current.neighbors:
46 | # Skip if the neighbor has already been checked
47 | if not neighbor.is_checked():
48 | # End the search if the neighbor is the end node
49 | if neighbor.is_end():
50 | previous[neighbor] = current
51 | found = True
52 | reconstruct_path(previous, grid.end, grid.draw)
53 | break
54 |
55 | # Markup for drawing neighbor
56 | if not neighbor.is_start() and not neighbor.is_end():
57 | neighbor.uncheck()
58 |
59 | # Add the neighbor to the queue
60 | counter += 1
61 | distance = heuristic("manhattan", neighbor, grid.end)
62 | previous[neighbor] = current
63 | Q.put((distance, counter, neighbor))
64 |
--------------------------------------------------------------------------------
/main/algorithms/GBLS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import PriorityQueue
3 | from .RP import reconstruct_path, heuristic, check, markup
4 |
5 |
6 | def gbls(grid: object):
7 | """
8 | Modified version of the greedy best-first search algorithm that explores neighbors in the direction of the last chosen node
9 | as long as the estimated distance to the goal is shorter than the current node.
10 |
11 | Args:
12 | grid (Grid): An object representing the current grid
13 |
14 | Returns:
15 | None
16 | """
17 |
18 | # Initialize the priority queue with the starting node
19 | Q = PriorityQueue()
20 | Q.put((heuristic("manhattan", grid.start, grid.end), 0, grid.start))
21 |
22 | # Initialize the counter and flags
23 | counter = 0
24 | run = True
25 | found = False
26 |
27 | previous = {}
28 | last_direction = None
29 |
30 | # Continue the search as long as there are nodes in the queue and the search
31 | # has not been stopped or the goal has not been found
32 | while not Q.empty() and run and not found:
33 | # Check for events that may stop the search
34 | run = check(pygame.event.get(), run)
35 |
36 | _, _, current = Q.get()
37 |
38 | if current.is_checked():
39 | continue
40 |
41 | markup(grid.draw, current)
42 |
43 | # Choose the neighbors in the last direction first,
44 | # if a direction has been chosen
45 | if last_direction:
46 | neighbors = [
47 | n
48 | for n in current.neighbors
49 | if n.get_pos()[0] - current.get_pos()[0] == last_direction[0]
50 | and n.get_pos()[1] - current.get_pos()[1] == last_direction[1]
51 | ]
52 |
53 | # Add the other neighbors to the list
54 | neighbors += [n for n in current.neighbors if n not in neighbors]
55 | # If no direction has been chosen, explore all neighbors
56 | else:
57 | neighbors = current.neighbors
58 |
59 | for neighbor in neighbors:
60 | if not neighbor.is_checked():
61 | if neighbor.is_end():
62 | previous[neighbor] = current
63 | found = True
64 | reconstruct_path(previous, grid.end, grid.draw)
65 | break
66 |
67 | # Markup for drawing neighbor
68 | if not neighbor.is_start() and not neighbor.is_end():
69 | neighbor.uncheck()
70 |
71 | counter += 1
72 | distance = heuristic("manhattan", neighbor, grid.end)
73 |
74 | # Add the neighbor to the queue with the estimated
75 | # distance as the priority
76 | previous[neighbor] = current
77 | Q.put((distance, counter, neighbor))
78 |
79 | # Save the direction to the neighbor
80 | last_direction = (
81 | neighbor.get_pos()[0] - current.get_pos()[0],
82 | neighbor.get_pos()[1] - current.get_pos()[1],
83 | )
84 |
--------------------------------------------------------------------------------
/main/algorithms/IDAStar.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from .RP import heuristic, markup, check
3 |
4 |
5 | def reconstruct_path(draw: object, path: list):
6 | """
7 | Reconstructs the path taken by the search algorithm and marks the
8 | path on the grid.
9 |
10 | Args:
11 | draw (function): The draw function to update the grid display.
12 | path (list): The list of nodes representing the path taken by
13 | the search algorithm.
14 |
15 | Returns:
16 | None
17 | """
18 | for node in path:
19 | if not node.is_start() and not node.is_end():
20 | node.make_path()
21 | draw()
22 |
23 |
24 | def ida_star(grid: object):
25 | """
26 | Implements the IDA* search algorithm.
27 |
28 | Args:
29 | grid (Grid): An object representing the current grid
30 |
31 | Returns:
32 | None
33 | """
34 | bound = heuristic("manhattan", grid.start, grid.end)
35 |
36 | # Stack for path being searched
37 | path = [grid.start]
38 |
39 | run = True
40 |
41 | while True and run:
42 | run = check(pygame.event.get(), run)
43 |
44 | result = search(grid.draw, path, 0, bound, grid.end)
45 |
46 | if result is True:
47 | path.reverse()
48 | reconstruct_path(grid.draw, path)
49 | break
50 | if result == float("inf"):
51 | break
52 |
53 | bound = result
54 |
55 |
56 | def search(draw: object, path: list, g: int, bound: float, end: object):
57 | """
58 | Recursive search function used by the IDA* algorithm.
59 |
60 | Args:
61 | draw (function): The draw function to update the grid display.
62 | path (list): The list of nodes representing the path taken by the
63 | search algorithm.
64 | g (int): The cost of the path represented by the list of nodes.
65 | bound (float): The bound on the cost of the path.
66 | end (Node): The ending node.
67 | Returns:
68 | True (boolean): end was reached
69 | f (float): g-score + heuristic is greater than current depth (bound)
70 | min_val (float): minimum value for the path
71 | """
72 | # Get last node in path
73 | node = path[-1]
74 |
75 | # Calculate the f-score
76 | f = g + heuristic("manhattan", node, end)
77 |
78 | if f > bound:
79 | return f
80 | if node.is_end():
81 | return True
82 |
83 | markup(draw, node)
84 |
85 | min_val = float("inf")
86 | for neighbor in node.neighbors:
87 | if neighbor not in path:
88 | if not neighbor.is_start() and not neighbor.is_end():
89 | neighbor.uncheck()
90 |
91 | path.append(neighbor)
92 | result = search(draw, path, g + 1, bound, end)
93 |
94 | if result is True:
95 | return True
96 | if result < min_val:
97 | min_val = result
98 |
99 | path.pop()
100 | return min_val
101 |
--------------------------------------------------------------------------------
/main/algorithms/IDDFS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from .RP import check, markup, reconstruct_path
3 |
4 |
5 | def depth_limit(draw: object, current: object, depth: int, visited: set, path: dict):
6 | """
7 | Recursive DFS with a depth limit.
8 |
9 | Args:
10 | draw (function): Function to draw the current state of the search.
11 | current (Node): Current node being visited.
12 | end (Node): Goal node.
13 | depth (int): Current depth of the search.
14 | visited (set): Set of nodes already visited.
15 | path (dict): Dictionary of parent nodes, used to reconstruct the path.
16 |
17 | Returns:
18 | dict: Dictionary of parent nodes, used to reconstruct the path.
19 | Returns an empty dictionary if no path is found.
20 | """
21 | if depth < 0:
22 | return {}
23 | if current.is_end():
24 | return path
25 |
26 | visited.add(current)
27 |
28 | markup(draw, current)
29 |
30 | for neighbor in current.neighbors:
31 | if neighbor not in visited:
32 | if not neighbor.is_start() and not neighbor.is_end():
33 | neighbor.uncheck()
34 |
35 | path[neighbor] = current
36 |
37 | result = depth_limit(draw, neighbor, depth - 1, visited, path)
38 |
39 | if result:
40 | return result
41 |
42 | return {}
43 |
44 |
45 | def iddfs(grid: object, length: int):
46 | """
47 | Iterative deepening DFS.
48 |
49 | Args:
50 | grid (Grid): An object representing the current grid
51 | length (int): Length of the grid.
52 |
53 | Returns:
54 | None
55 | """
56 | run = True
57 |
58 | for depth in range(0, length):
59 | if not run:
60 | break
61 |
62 | run = check(pygame.event.get(), run)
63 |
64 | visited = set()
65 | path = depth_limit(grid.draw, grid.start, depth, visited, {grid.start: None})
66 |
67 | if path:
68 | reconstruct_path(path, grid.end, grid.draw)
69 | break
70 |
--------------------------------------------------------------------------------
/main/algorithms/JPS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from .RP import heuristic, check, markup, reconstruct_path
3 |
4 |
5 | def jps(grid: object):
6 | """
7 | Perform an Jump Point Search from start to end.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid
11 |
12 | Returns:
13 | None: The function updates the screen with the search progress and path.
14 | """
15 | open_set = set([grid.start])
16 | closed_set = set()
17 |
18 | came_from = {}
19 |
20 | g_values = {grid.start: 0}
21 | f_values = {grid.start: heuristic("manhattan", grid.start, grid.end)}
22 |
23 | run = True
24 |
25 | while open_set and run:
26 | # Check for exit events
27 | run = check(pygame.event.get(), run)
28 |
29 | # Find the node with the lowest f-value in the open set.
30 | current = min(open_set, key=lambda node: f_values[node])
31 |
32 | if current.is_end():
33 | reconstruct_path(came_from, grid.end, grid.draw)
34 | break
35 |
36 | # Move current from open set to closed set
37 | open_set.remove(current)
38 | closed_set.add(current)
39 |
40 | markup(grid.draw, current)
41 |
42 | for neighbor in current.neighbors:
43 | # Check if neighbor is already closed
44 | if neighbor in closed_set:
45 | continue
46 |
47 | if neighbor not in open_set:
48 | g_value = g_values[current] + heuristic("manhattan", current, neighbor)
49 |
50 | g_values[neighbor] = g_value
51 | f_values[neighbor] = g_value + heuristic(
52 | "manhattan", neighbor, grid.end
53 | )
54 |
55 | came_from[neighbor] = current
56 | open_set.add(neighbor)
57 |
58 | if not neighbor.is_start() and not neighbor.is_end():
59 | neighbor.uncheck()
60 |
--------------------------------------------------------------------------------
/main/algorithms/LPAStar.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import heapq
3 | from .RP import heuristic, check
4 |
5 |
6 | def get_checked_neighbors(node: object):
7 | """
8 | Get the neighbors of a node that have been checked or are the start node.
9 |
10 | Args:
11 | node (Node): The node to check.
12 |
13 | Returns:
14 | List[Node]: The list of checked or start neighbors.
15 | """
16 |
17 | checked = []
18 | for neighbor in node.neighbors:
19 | if neighbor.is_checked() or neighbor.is_start():
20 | checked.append(neighbor)
21 | return checked
22 |
23 |
24 | def reconstruct_path(current: object, costs: dict, draw: object):
25 | """
26 | Reconstruct the path from the end node to the start node.
27 |
28 | Args:
29 | current (Node): The current node in the search.
30 | costs (Dict[Node, int]): The cost of reaching each node from the start.
31 | draw (function): A function used to draw the search on the screen.
32 |
33 | Returns:
34 | None: The function updates the screen with the path.
35 | """
36 |
37 | # Continue until the current node is the start node
38 | while not current.is_start():
39 | # Find the neighbor with the lowest cost
40 | current = min(get_checked_neighbors(current), key=lambda x: costs[x])
41 |
42 | # Break if the current node is the start node
43 | if current.is_start():
44 | break
45 |
46 | # Draw the path to the current node
47 | current.make_path()
48 | draw()
49 |
50 |
51 | def update_vertex(grid: object, node: object, rhs: dict, g: dict, open_list: list):
52 | """
53 | Updates rhs values, and open list
54 |
55 | Args:
56 | grid (Grid): An object representing the current grid.
57 | node (Node): Current node being checked.
58 | rhs (dict): Dictionary of the rhs values.
59 | g (dict): Dictionary of the g values.
60 | open_list (list): List of open nodes.
61 |
62 | Returns:
63 | open_list: List of open nodes.
64 | """
65 | if node != grid.start:
66 | rhs[node] = float("inf")
67 | for neighbor in node.neighbors:
68 | if (
69 | neighbor.is_checked()
70 | or neighbor.is_start()
71 | or neighbor.is_unchecked()
72 | or (neighbor.is_end() and neighbor.been_checked)
73 | ):
74 | rhs[node] = min(rhs[node], g[neighbor] + 1)
75 |
76 | open_list = [(key, value) for key, value in open_list if value != node]
77 | heapq.heapify(open_list)
78 |
79 | if g[node] != rhs[node]:
80 | heapq.heappush(open_list, (calc_key(grid.end, node, g, rhs), node))
81 | return open_list
82 |
83 |
84 | def calc_key(end: object, node: object, g: dict, rhs: dict):
85 | """
86 | Calculates the key for the priority queue
87 |
88 | Args:
89 | end (Node): The end node of the current grid.
90 | node (Node): Current node.
91 | g (dict): Dictionary of the g values.
92 | rhs (dict): Dictionary of the rhs values.
93 |
94 | Returns:
95 | tuple: (min of g & rhs + heuristic, min of g & rhs)
96 | """
97 | return (
98 | min(g[node], rhs[node]) + heuristic("manhattan", node, end),
99 | min(g[node], rhs[node]),
100 | )
101 |
102 |
103 | def lpa_star(grid: object):
104 | """
105 | Performs the LPA* algorithm from start to end.
106 |
107 | Args:
108 | grid (Grid): An object representing the current grid
109 |
110 | Returns:
111 | None: The function updates the screen with the search progress and path.
112 | """
113 | rhs = {node: float("inf") for row in grid.grid for node in row}
114 | g = {node: float("inf") for row in grid.grid for node in row}
115 | open_list = []
116 |
117 | rhs[grid.start] = 0
118 | topKey = calc_key(grid.end, grid.start, g, rhs)
119 | open_list.append((topKey, grid.start))
120 |
121 | # Initialize a flag to track whether the search should continue
122 | run = True
123 |
124 | while (
125 | topKey < calc_key(grid.end, grid.end, g, rhs) or rhs[grid.end] != g[grid.end]
126 | ) and run:
127 | # Check for exit events
128 | run = check(pygame.event.get(), run)
129 |
130 | # Check for no path found
131 | if open_list:
132 | topKey, node = heapq.heappop(open_list)
133 | else:
134 | break
135 |
136 | if g[node] > rhs[node]:
137 | g[node] = rhs[node]
138 | else:
139 | g[node] = float("inf")
140 | open_list = update_vertex(grid, node, rhs, g, open_list)
141 |
142 | # Update neighboring nodes
143 | for neighbor in node.neighbors:
144 | if (
145 | not neighbor.is_start()
146 | and not neighbor.is_end()
147 | and not neighbor.is_checked()
148 | ):
149 | neighbor.uncheck()
150 | open_list = update_vertex(grid, neighbor, rhs, g, open_list)
151 |
152 | # Update the screen with the search progress
153 | grid.draw()
154 |
155 | # Check the current node if it is not the start node or end node
156 | if not node.is_start() and not node.is_end():
157 | node.check()
158 |
159 | # Reconstruct path if at end
160 | if node == grid.end:
161 | reconstruct_path(node, rhs, grid.draw)
162 |
--------------------------------------------------------------------------------
/main/algorithms/LexicographicBFS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from .RP import markup, check, reconstruct_path
3 |
4 |
5 | def LexBFS(draw: object, start: object):
6 | """
7 | Implements the Lexicographic Breadth-First Search (LexBFS) algorithm.
8 |
9 | Args:
10 | draw (function): The draw function to update the grid display.
11 | start (start): The starting node.
12 |
13 | Returns:
14 | order (list): A list of nodes in the graph, in the order they were
15 | visited by LexBFS.
16 | levels (dict): A dictionary containing the level of each node in the
17 | breadth-first search tree.
18 | parent (dict): A dictionary containing the parent of each node in the
19 | breadth-first search tree.
20 | """
21 |
22 | # Initialize the list to store the order of the vertices
23 | order = []
24 |
25 | # Initialize the dictionaries to store the levels and parents of
26 | # the vertices
27 | levels = {}
28 | parent = {}
29 |
30 | # Set the level of the starting vertex to 0 and its parent to None
31 | levels[start] = 0
32 | parent[start] = None
33 |
34 | # Initialize the counter and the queue
35 | level = 0
36 | Q = [start]
37 |
38 | # Set the flag to run the algorithm
39 | run = True
40 |
41 | # Run the algorithm until the queue is empty or the flag is set to False
42 | while Q and run:
43 | # Check if the flag to run the algorithm is set to False
44 | run = check(pygame.event.get(), run)
45 |
46 | # Pop the first element from the queue
47 | current = Q.pop(0)
48 |
49 | order.append(current)
50 |
51 | # Mark the node as visited
52 | markup(draw, current)
53 |
54 | for neighbor in current.neighbors:
55 | # If neighbor has not been visited before
56 | if neighbor not in levels:
57 | if not neighbor.is_start() and not neighbor.is_end():
58 | neighbor.uncheck()
59 | # Set the level of neighbor to the current level + 1
60 | levels[neighbor] = level + 1
61 | parent[neighbor] = current
62 | Q.append(neighbor)
63 | level = level + 1
64 |
65 | # If the flag to run the algorithm is set to False, return False
66 | # for all outputs
67 | if not run:
68 | return False, False, False
69 | return order, levels, parent
70 |
71 |
72 | def lbfs(grid: object):
73 | """
74 | Solves the shortest path problem using LexBFS.
75 |
76 | Args:
77 | grid (Grid): An object representing the current grid
78 |
79 | Returns:
80 | None
81 | """
82 |
83 | # Run LexBFS to get the order of the vertices
84 | order, levels, parent = LexBFS(grid.draw, grid.start)
85 |
86 | if order is not False:
87 | # Initialize the distances dictionary
88 | distances = {node: float("inf") for row in grid.grid for node in row}
89 | distances[grid.start] = 0
90 |
91 | # Iterate through the vertices in the order produced by LexBFS
92 | for current in order:
93 | # Update the distances of the neighbors of current
94 | for neighbor in current.neighbors:
95 | if distances[current] + 1 < distances[neighbor]:
96 | distances[neighbor] = distances[current] + 1
97 | parent[neighbor] = current
98 |
99 | reconstruct_path(parent, grid.end, grid.draw)
100 |
--------------------------------------------------------------------------------
/main/algorithms/RandomLIFO.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import random
3 | from queue import LifoQueue
4 | from .RP import check, markup, reconstruct_path
5 |
6 | def rand_lifo(grid: object):
7 | """
8 | Runs a Random search on the grid using a LIFO Queue
9 | to choose the next node to check.
10 |
11 | Args:
12 | grid (Grid): An object representing the current grid
13 |
14 | Returns:
15 | None: The function updates the screen with the search progress and path.
16 | """
17 | # Initialize path and queue
18 | came_from = {}
19 | queue = LifoQueue()
20 | queue.put(grid.start)
21 |
22 | run = True
23 |
24 | # Run search till queue is empty
25 | while not queue.empty() and run:
26 | # Check for events that may stop the search
27 | run = check(pygame.event.get(), run)
28 |
29 | current = queue.get()
30 |
31 | # End search if target is found
32 | if current.is_end():
33 | reconstruct_path(came_from, current, grid.draw)
34 | break
35 |
36 | markup(grid.draw, current)
37 |
38 | # Choose a random neighbor of the current node
39 | next_node = random.randint(0, len(current.neighbors) - 1)
40 |
41 | # Add every possible neighbor to the queue in the correct order
42 | for index, neighbor in enumerate(current.neighbors):
43 | if neighbor.is_checked():
44 | continue
45 | came_from[neighbor] = current
46 | if index != next_node:
47 | queue.put(neighbor)
48 | if not neighbor.is_start() and not neighbor.is_end():
49 | neighbor.uncheck()
50 |
51 | if not current.neighbors[next_node].is_checked():
52 | queue.put(current.neighbors[next_node])
--------------------------------------------------------------------------------
/main/algorithms/RandomWalk.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import random
3 | from .RP import check, get_unvisited_nodes
4 |
5 |
6 | def reconstruct_path(came_from: list, current: object, draw: object):
7 | """
8 | Reconstructs the path from the starting node to the current node,
9 | by following the `came_from` dictionary.
10 |
11 | Args:
12 | came_from (List[Node]): list containing which nodes were checked
13 | current (Node): current node
14 | draw (function): function for drawing the search path on the grid
15 |
16 | Returns:
17 | None
18 | """
19 |
20 | for current in reversed(came_from):
21 | if not current.is_start():
22 | current.make_path()
23 | draw()
24 |
25 |
26 | def rand_walk(grid: object):
27 | """
28 | Generates a random walk from the starting node to the goal node.
29 |
30 | Args:
31 | grid (Grid): An object representing the current grid
32 |
33 | Returns:
34 | None
35 | """
36 |
37 | # Initialize the list of previous nodes and the current node
38 | came_from = []
39 | current = grid.start
40 |
41 | # Flag for continuing the search
42 | run = True
43 |
44 | # Flag for making the path
45 | make_path = True
46 |
47 | possible_nodes = get_unvisited_nodes(grid.start)
48 |
49 | maxed = []
50 |
51 | # Continue the search until the search is stopped or the goal is reached
52 | while run and len(possible_nodes) - 1 > len(maxed):
53 | # Check for events that may stop the search
54 | run = check(pygame.event.get(), run)
55 |
56 | # Choose a random neighbor of the current node
57 | neighbor = random.randint(0, len(current.neighbors) - 1)
58 |
59 | # Add the current node to the list of previous nodes if it
60 | # has not already been added
61 | if current not in came_from:
62 | came_from.append(current)
63 |
64 | # Move to the chosen neighbor
65 | current = current.neighbors[neighbor]
66 |
67 | # If the current node is the goal node and the path has not
68 | # been made yet, reconstruct the path and make the goal node
69 | if current.is_end() and make_path:
70 | reconstruct_path(came_from, grid.end, grid.draw)
71 | grid.end.make_end()
72 | break
73 |
74 | # Markup for drawing path with color change
75 | if not current.is_start():
76 | current.uncheck()
77 |
78 | grid.draw()
79 |
80 | if not current.is_start() and not current.been_checked:
81 | current.check()
82 | elif not current.is_start():
83 | current.mult_check(3)
84 |
85 | if current.color[0] <= 3 and not current in maxed:
86 | maxed.append(current)
87 |
--------------------------------------------------------------------------------
/main/algorithms/SPFA.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import Queue
3 | from .RP import check, markup, reconstruct_path
4 |
5 | def spfa(grid: object):
6 | """
7 | Performs the Shortest Path Faster Algorithm for the given grid.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid.
11 |
12 | Returns:
13 | None: The function updates the screen with the search progress and path.
14 | """
15 | d = {node: float("inf") for row in grid.grid for node in row}
16 | d[grid.start] = 0
17 |
18 | queue = Queue()
19 | queue.put(grid.start)
20 |
21 | path = {}
22 |
23 | run = True
24 |
25 | while not queue.empty() and run:
26 | # Check for exit events
27 | run = check(pygame.event.get(), run)
28 |
29 | # Exit the loop if the user interrupted
30 | if not run:
31 | break
32 |
33 | u = queue.get()
34 |
35 | # Draw the current node
36 | markup(grid.draw, u)
37 |
38 | for v in u.neighbors:
39 | if d[u] + 1 < d[v]:
40 | # Update the distance and predecessor for the neighbor if the
41 | # current distance is shorter
42 | d[v] = d[u] + 1
43 | path[v] = u
44 |
45 | if v not in queue.queue:
46 | queue.put(v)
47 |
48 | # Uncheck the neighbor if it is not the start or end node
49 | # for markup
50 | if not v.is_end() and not v.is_start():
51 | v.uncheck()
52 |
53 | if run:
54 | reconstruct_path(path, grid.end, grid.draw)
--------------------------------------------------------------------------------
/main/algorithms/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/main/algorithms/__init__.py
--------------------------------------------------------------------------------
/main/algorithms/bidirectional/AStar.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from queue import PriorityQueue
3 | from ..RP import heuristic, check, markup, thread_construct
4 |
5 |
6 | def bi_a_star(grid: object):
7 | """
8 | Performs a bidirectional search to find the shortest path between the
9 | start and end nodes. The search is visualized using the given draw object.
10 |
11 | Args:
12 | grid (Grid): An object representing the current grid
13 |
14 | Returns:
15 | None: The function updates the screen with the search progress and path.
16 | """
17 |
18 | # Initialize two queues, one for each direction of the search
19 | count_start = 0
20 | count_end = 0
21 |
22 | open_set_start = PriorityQueue()
23 | open_set_end = PriorityQueue()
24 |
25 | open_set_start.put((0, 0, grid.start))
26 | open_set_end.put((0, 0, grid.end))
27 |
28 | came_from_start = {}
29 | came_from_end = {}
30 |
31 | g_score_start = {node: float("inf") for row in grid.grid for node in row}
32 | g_score_start[grid.start] = 0
33 |
34 | g_score_end = {node: float("inf") for row in grid.grid for node in row}
35 | g_score_end[grid.end] = 0
36 |
37 | # Initialize two sets to keep track of visited nodes, one for
38 | # each direction of the search
39 | visited_start = set()
40 | visited_end = set()
41 |
42 | # Add the start and end nodes to the path dictionaries
43 | came_from_start[grid.start] = grid.start
44 | came_from_end[grid.end] = grid.end
45 |
46 | run = True
47 |
48 | # Loop until one of the queues is empty
49 | while not open_set_start.empty() and not open_set_end.empty() and run:
50 | # Check for events and update the run flag accordingly
51 | run = check(pygame.event.get(), run)
52 |
53 | # Dequeue a node from each queue and process it
54 | f_score1, count1, node1 = open_set_start.get()
55 |
56 | # Check for redundant checks
57 | if node1 in visited_start:
58 | continue
59 |
60 | _, _, node2 = open_set_end.get()
61 | # Check for redundant checks
62 | if node2 in visited_end:
63 | open_set_start.put((f_score1, count1, node1))
64 | continue
65 |
66 | # Check if the nodes have already been visited from the other direction
67 | if node1 in visited_end:
68 | # Construct two threads to reconstruct the path from the start and
69 | # end directions
70 | thread_construct(
71 | (came_from_start, node1, grid.start, grid.draw),
72 | (came_from_end, node1, grid.end, grid.draw),
73 | )
74 | break
75 | if node2 in visited_start:
76 | # Construct two threads to reconstruct the path from the start and
77 | # end directions
78 | thread_construct(
79 | (came_from_start, node2, grid.start, grid.draw),
80 | (came_from_end, node2, grid.end, grid.draw),
81 | )
82 | break
83 |
84 | # Mark up the nodes that are being processed
85 | markup(grid.draw, node1)
86 | markup(grid.draw, node2)
87 |
88 | # Add the neighbors of the dequeued nodes to their respective queues
89 | for neighbor in node1.neighbors:
90 | temp_g_score = g_score_start[node1] + 1
91 |
92 | if temp_g_score < g_score_start[neighbor]:
93 | came_from_start[neighbor] = node1
94 | g_score_start[neighbor] = temp_g_score
95 | if neighbor not in visited_start:
96 | f_score = temp_g_score + heuristic("manhattan", neighbor, grid.end)
97 | count_start += 1
98 | open_set_start.put((f_score, count_start, neighbor))
99 | visited_start.add(node1)
100 |
101 | if not neighbor.is_start() and not neighbor.is_end():
102 | neighbor.uncheck()
103 |
104 | for neighbor in node2.neighbors:
105 | temp_g_score = g_score_end[node2] + 1
106 |
107 | if temp_g_score < g_score_end[neighbor]:
108 | came_from_end[neighbor] = node2
109 | g_score_end[neighbor] = temp_g_score
110 | if neighbor not in visited_end:
111 | f_score = temp_g_score + heuristic("manhattan", neighbor, grid.start)
112 | count_end += 1
113 | open_set_end.put((f_score, count_end, neighbor))
114 | visited_end.add(node2)
115 |
116 | if not neighbor.is_start() and not neighbor.is_end():
117 | neighbor.uncheck()
118 |
--------------------------------------------------------------------------------
/main/algorithms/bidirectional/BFS.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | from ..RP import check, markup, thread_construct
3 |
4 | def bi_search(grid: object):
5 | """
6 | Performs a bidirectional search to find the shortest path between the
7 | start and end nodes. The search is visualized using the given draw object.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid
11 | """
12 |
13 | # Initialize two queues, one for each direction of the search
14 | queue_start = [grid.start]
15 | queue_end = [grid.end]
16 |
17 | # Initialize two sets to keep track of visited nodes, one for
18 | # each direction of the search
19 | visited_start = set()
20 | visited_end = set()
21 |
22 | # Initialize dictionaries to store the paths taken by each search direction
23 | start_path = {}
24 | end_path = {}
25 |
26 | # Add the start and end nodes to the path dictionaries
27 | start_path[grid.start] = grid.start
28 | end_path[grid.end] = grid.end
29 |
30 | run = True
31 |
32 | # Loop until one of the queues is empty
33 | while queue_start and queue_end and run:
34 | # Check for events and update the run flag accordingly
35 | run = check(pygame.event.get(), run)
36 |
37 | # Dequeue a node from each queue and process it
38 | node1 = queue_start.pop(0)
39 | # Check for redundant checks
40 | if node1 in visited_start:
41 | continue
42 |
43 | node2 = queue_end.pop(0)
44 | # Check for redundant checks
45 | if node2 in visited_end:
46 | queue_start.insert(0, node1)
47 | continue
48 |
49 | # Check if the nodes have already been visited from the other direction
50 | if node1 in visited_end:
51 | # Construct two threads to reconstruct the path from the start and
52 | # end directions
53 | thread_construct(
54 | (start_path, node1, grid.start, grid.draw),
55 | (end_path, node1, grid.end, grid.draw),
56 | )
57 | break
58 | if node2 in visited_start:
59 | # Construct two threads to reconstruct the path from the start and
60 | # end directions
61 | thread_construct(
62 | (start_path, node2, grid.start, grid.draw),
63 | (end_path, node2, grid.end, grid.draw),
64 | )
65 | break
66 |
67 | # Mark up the nodes that are being processed
68 | markup(grid.draw, node1)
69 | markup(grid.draw, node2)
70 |
71 | # Mark the nodes as visited
72 | visited_start.add(node1)
73 | visited_end.add(node2)
74 |
75 | loop_helper(node1, visited_start, queue_start, start_path)
76 | loop_helper(node2, visited_end, queue_end, end_path)
77 |
78 |
79 | def loop_helper(node, visited, queue, path):
80 | for neighbor in node.neighbors:
81 | if neighbor not in visited:
82 | queue.append(neighbor)
83 | path[neighbor] = node
84 | neighbor.uncheck()
--------------------------------------------------------------------------------
/main/algorithms/bidirectional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/main/algorithms/bidirectional/__init__.py
--------------------------------------------------------------------------------
/main/algorithms/random/grid_to_tree.md:
--------------------------------------------------------------------------------
1 | # Grid Graph to Tree from starting Node Algorithm (GGTN Algorithm):
2 |
3 | ## In words:
4 |
5 | The starting node is labeled with a value of 0.
6 |
7 | From the start node, label all neighbors in increasing order starting from 1 to n-neighbors
8 | (neighbors are the up, down, left, and right in this case; labeling was done counter-clockwise,
9 | starting from the up neighbor).
10 |
11 | Then, go to the smallest neighbor (1) and label all unvalued neighbors with "1" + 1 to
12 | n-neighbors (Example: a neighbor of 1 is labeled as 012 or 011 or 013).
13 |
14 | Repeat this, going from smallest to largest value neighbor, and continue with the newly
15 | labeled neighbors until the grid is fully labeled.
16 |
17 | ### From the start node you can then construct a tree based off of the labelings:
18 |
19 | For a 5x5 grid with start at 3 down from to left and 3 right from left
20 |
21 | 
22 |
23 | To reconstruct the graph based on the tree you need to know the method of labeling (i.e. did you label them in a certain order [e.g. clockwise, counter-clockwise])
24 |
25 | There is probably a better labeling system than this, but it's the simplest thing I could come up with without thinking too much on it.
26 |
27 | ## Pseudocode:
28 |
29 | ```
30 | func grid_graph_to_tree(Start, Direction : tuple indicating starting direction and labeling rotation (e.g. (up, clockwise))):
31 | create a priority queue Q
32 | create a dictionary D to store node node_value pairs
33 | set all node values to infinity
34 | set Start node value to 0
35 | add Start to Q
36 |
37 | while Q is not empty:
38 | Current is popped from Q
39 | for every Neighbor of Current node gotten according to Direction:
40 | label neighbors with the number of Current with the digit in increasing order from 1 - n-neighbors concatenated to the end of the number (e.g. Current label = 021 -> Neighbor label = 0211, 0212, 0213, 0214, ..., 0218)
41 |
42 | create a tree T with start as the root
43 | digit_count = 1
44 | while D is not empty:
45 | find node N with smallest node_value
46 | add N as a child of the parent of N (parent = N.node_value[digit_count - 1])
47 | remove N from D
48 |
49 | return T
50 | ```
--------------------------------------------------------------------------------
/main/algorithms/random/tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/main/algorithms/random/tree.png
--------------------------------------------------------------------------------
/main/algorithms/random/tree.txt:
--------------------------------------------------------------------------------
1 | _______________0___________
2 | / | | \
3 | 1 2 3 4
4 | / | \ / \ / \ |
5 | 11 12 13 21 22 31 32 41
6 | / \ \ \ | | | |
7 | 111 112 121 131 211 221 311 321
8 | | | | |
9 | 1111 1121 2111 3111
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/main/testing/AStar.py:
--------------------------------------------------------------------------------
1 | from queue import PriorityQueue
2 | from .RP import heuristic, count_path
3 |
4 |
5 | def a_star(grid: object):
6 | """
7 | Perform an A* search from start to end.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid.
11 |
12 | Returns:
13 | visited_nodes (int): Count of the number of nodes visited.
14 | path_size (int): Length of the path found.
15 | """
16 |
17 | # Initialize counters and sets
18 | count = 0
19 | open_set = PriorityQueue()
20 | open_set.put((0, count, grid.start))
21 | came_from = {}
22 |
23 | # Initialize dictionary to store the g scores for each node
24 | g_score = {node: float("inf") for row in grid.grid for node in row}
25 | g_score[grid.start] = 0
26 |
27 | # Initialize a set to store the nodes in the open set
28 | open_set_hash = {grid.start}
29 |
30 | visited_nodes: int = 0
31 | path_size: int = 0
32 |
33 | # Perform the search
34 | while not open_set.empty():
35 | # Get the current node from the open set
36 | _, _, current = open_set.get()
37 | open_set_hash.remove(current)
38 |
39 | visited_nodes += 1
40 |
41 | # End the search if the current node is the end node
42 | if current.is_end():
43 | path_size = count_path(came_from, grid.end)
44 | break
45 |
46 | # Check the neighbors of the current node
47 | for neighbor in current.neighbors:
48 | # Calculate the tentative g score for the neighbor
49 | temp_g_score = g_score[current] + 1
50 |
51 | # Update the g core for the neighbor if the tentative g score is lower
52 | if temp_g_score < g_score[neighbor]:
53 | came_from[neighbor] = current
54 | g_score[neighbor] = temp_g_score
55 |
56 | # Add the neighbor to the open set if it is not already there
57 | if neighbor not in open_set_hash:
58 | f_score = temp_g_score + heuristic("manhattan", neighbor, grid.end)
59 | count += 1
60 | open_set.put((f_score, count, neighbor))
61 | open_set_hash.add(neighbor)
62 | return visited_nodes, path_size
63 |
--------------------------------------------------------------------------------
/main/testing/BFS.py:
--------------------------------------------------------------------------------
1 | from .RP import check, count_path
2 |
3 |
4 | def bfs(grid: object):
5 | """
6 | Perform a breadth-first search from start to end.
7 |
8 | Args:
9 | grid (Grid): An object representing the current grid.
10 |
11 | Returns:
12 | visited_nodes (int): Count of the number of nodes visited.
13 | count_path (int): Length of the path found.
14 | """
15 |
16 | # Initialize a list to store the nodes to visit (queue)
17 | nodes = []
18 | nodes.append(grid.start)
19 |
20 | # Initialize a dictionary to store the predecessor of each node
21 | previous = {}
22 |
23 | # Initialize flags to track the search status
24 | found = False
25 |
26 | visited_nodes: int = 0
27 |
28 | # Perform the search
29 | while nodes and not found:
30 | # Get the next node to visit
31 | current = nodes.pop(0)
32 |
33 | # Skip the node if it has already been checked
34 | if current.is_checked():
35 | continue
36 |
37 | visited_nodes += 1
38 |
39 | check(current)
40 |
41 | # Check the neighbors of the current node
42 | for neighbor in current.neighbors:
43 | # Skip the neighbor if it has already been checked
44 | if not neighbor.is_checked():
45 | # End the search if the neighbor is the end node
46 | if neighbor.is_end():
47 | previous[neighbor] = current
48 | found = True
49 | break
50 | # Add the neighbor to the list of nodes to visit if it is not the end node
51 | else:
52 | previous[neighbor] = current
53 | nodes.append(neighbor)
54 | return visited_nodes, count_path(previous, grid.end)
55 |
--------------------------------------------------------------------------------
/main/testing/BStar.py:
--------------------------------------------------------------------------------
1 | from queue import PriorityQueue
2 | from .RP import count_path, check, heuristic
3 |
4 |
5 | def b_star(grid: object):
6 | """
7 | Perform a B* search from start to end.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid.
11 |
12 | Returns:
13 | None
14 | """
15 |
16 | # Initialize counters and sets
17 | count = 0
18 | open_set = PriorityQueue()
19 | open_set.put((0, count, grid.start))
20 | came_from = {}
21 |
22 | # Initialize dictionaries to store the g and f scores for each node
23 | g_score = {node: float("inf") for row in grid.grid for node in row}
24 | g_score[grid.start] = 0
25 |
26 | # Initialize a set to store the nodes in the open set
27 | open_set_hash = {grid.start}
28 |
29 | visited_nodes: int = 0
30 | path_size: int = 0
31 |
32 | # Perform the search
33 | while not open_set.empty():
34 | # Get the current node from the open set
35 | _, _, current = open_set.get()
36 | open_set_hash.remove(current)
37 |
38 | visited_nodes += 1
39 |
40 | # End the search if the current node is the end node
41 | if current.is_end():
42 | path_size = count_path(came_from, grid.end)
43 | break
44 |
45 | # Check the neighbors of the current node
46 | for neighbor in current.neighbors:
47 | # Calculate the tentative g score for the neighbor
48 | temp_g_score = g_score[current] + 1
49 |
50 | # Update the g and f scores for the neighbor if the
51 | # tentative g score is lower
52 | if temp_g_score < g_score[neighbor]:
53 | came_from[neighbor] = current
54 | g_score[neighbor] = temp_g_score
55 | f_score = temp_g_score + heuristic("d_manhattan", neighbor, grid.end)
56 |
57 | # Add the neighbor to the open set if it is not already there
58 | if neighbor not in open_set_hash:
59 | count += 1
60 | open_set.put((f_score, count, neighbor))
61 | open_set_hash.add(neighbor)
62 |
63 | return visited_nodes, path_size
--------------------------------------------------------------------------------
/main/testing/BeamSearch.py:
--------------------------------------------------------------------------------
1 | import heapq
2 | from .RP import heuristic, check, count_path
3 |
4 |
5 | def beam_search(grid: object, beam_size: int):
6 | """
7 | Perform a beam search from start to end with a given beam size.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid.
11 | beam_size (int): The maximum number of nodes to consider at each step.
12 |
13 | Returns:
14 | visited_nodes (int): Count of the number of nodes visited.
15 | path_size (int): Length of the path found.
16 | """
17 |
18 | # Initialize the beam with the root node
19 | beam = [(0, grid.start)]
20 |
21 | # Initialize a dictionary to store the previous nodes for each node
22 | previous = {}
23 | previous[grid.start] = grid.start
24 |
25 | visited_nodes: int = 0
26 | path_size: int = 0
27 |
28 | # Perform the search
29 | while beam:
30 | # Get the most promising node from the beam
31 | _, current = heapq.heappop(beam)
32 |
33 | visited_nodes += 1
34 |
35 | # End the search if the current node is the end node
36 | if current.is_end():
37 | path_size = count_path(previous, grid.end)
38 | break
39 |
40 | check(current)
41 |
42 | # Expand the current node and add its children to the beam
43 | children = current.neighbors
44 |
45 | # Remove the previous node from the list of children
46 | if previous[current] in children:
47 | children.remove(previous[current])
48 |
49 | # Add the children to the beam
50 | for child in children:
51 | # Skip the child if it has already been checked
52 | if not child.is_checked():
53 | previous[child] = current
54 | heapq.heappush(beam, (heuristic("manhattan", child, grid.end), child))
55 |
56 | # Trim the beam to the desired size
57 | beam = beam[:beam_size]
58 | return visited_nodes, path_size
59 |
--------------------------------------------------------------------------------
/main/testing/BellmanFord.py:
--------------------------------------------------------------------------------
1 | from queue import Queue
2 | from .RP import get_unvisited_nodes, count_path
3 |
4 |
5 | def bell_ford(grid: object, accuracy: float):
6 | """
7 | Perform a Bellman-Ford search from start to end with a given accuracy.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid.
11 | accuracy (float): The fraction of nodes to visit at each step. (0, 1]
12 |
13 | Returns:
14 | visited_nodes (int): Count of the number of nodes visited.
15 | count_path (int): Length of the path found.
16 | """
17 |
18 | # Get the list of unvisited nodes
19 | nodes = get_unvisited_nodes(grid.start)
20 |
21 | # Initialize dictionaries to store the distance and predecessor for each node
22 | distance = {node: float("inf") for node in nodes}
23 | predecessor = {}
24 |
25 | # Set the distance of the start node to 0
26 | distance[grid.start] = 0
27 |
28 | # Calculate the number of nodes to visit at each step
29 | counter = int((len(nodes) - 1) * accuracy)
30 |
31 | visited_nodes: int = 0
32 |
33 | # Perform the search
34 | while counter >= 0:
35 | # Visit each node
36 | for current in nodes:
37 |
38 | visited_nodes += 1
39 |
40 | # Check the neighbors of the current node
41 | for neighbor in current.neighbors:
42 | # Update the distance and predecessor for the neighbor if the
43 | # current distance is shorter
44 | if distance[current] + 1 < distance[neighbor]:
45 | distance[neighbor] = distance[current] + 1
46 | predecessor[neighbor] = current
47 | # Decrement the counter
48 | counter -= 1
49 |
50 | return visited_nodes, count_path(predecessor, grid.end)
51 |
--------------------------------------------------------------------------------
/main/testing/BestFS.py:
--------------------------------------------------------------------------------
1 | from queue import PriorityQueue
2 | from .RP import heuristic, check
3 |
4 |
5 | def get_checked_neighbors(node: object):
6 | """
7 | Get the neighbors of a node that have been checked or are the start node.
8 |
9 | Args:
10 | node (Node): The node to check.
11 |
12 | Returns:
13 | List[Node]: The list of checked or start neighbors.
14 | """
15 |
16 | checked = []
17 | for neighbor in node.neighbors:
18 | if neighbor.is_checked() or neighbor.is_start():
19 | checked.append(neighbor)
20 | return checked
21 |
22 |
23 | def count_path(current: object, costs: dict):
24 | """
25 | Reconstruct the path from the end node to the start node.
26 |
27 | Args:
28 | current (Node): The current node in the search.
29 | costs (Dict[Node, int]): The cost of reaching each node from the start.
30 |
31 | Returns:
32 | path_size (int): Length of the path found.
33 | """
34 | path_size: int = 0
35 |
36 | # Continue until the current node is the start node
37 | while not current.is_start():
38 | # Find the neighbor with the lowest cost
39 | current = min(get_checked_neighbors(current), key=lambda x: costs[x])
40 |
41 | # Break if the current node is the start node
42 | if current.is_start():
43 | break
44 | path_size += 1
45 | return path_size
46 |
47 |
48 | def best_fs(grid: object):
49 | """
50 | Perform a best-first search from start to end.
51 |
52 | Args:
53 | grid (Grid): An object representing the current grid.
54 |
55 | Returns:
56 | visited_nodes (int): Count of the number of nodes visited.
57 | path_size (int): Length of the path found.
58 | """
59 |
60 | # Initialize the priority queue with the start node
61 | queue = PriorityQueue()
62 | queue.put((0, 0, grid.start))
63 | count = 0
64 |
65 | # Initialize a dictionary to store the cost of reaching each node from the start
66 | costs = {grid.start: 0}
67 |
68 | visited_nodes: int = 0
69 | path_size: int = 0
70 |
71 | # Perform the search
72 | while not queue.empty():
73 | # Get the node with the lowest cost from the queue
74 | cost, _, current = queue.get()
75 |
76 | visited_nodes += 1
77 |
78 | # End the search if the current node is the end node
79 | if current.is_end():
80 | path_size = count_path(current, costs)
81 | break
82 |
83 | check(current)
84 |
85 | # Check the neighbors of the current node
86 | for neighbor in current.neighbors:
87 | # Calculate the cost of reaching the neighbor
88 | cost = costs[current] + 1
89 |
90 | # Update the cost in the dictionary if it is lower than the current value
91 | if neighbor not in costs or cost < costs[neighbor]:
92 | costs[neighbor] = cost
93 | count += 1
94 | # Add the neighbor to the queue with the calculated cost as the priority
95 | queue.put(
96 | (cost + heuristic("manhattan", neighbor, grid.end), count, neighbor)
97 | )
98 | return visited_nodes, path_size
99 |
--------------------------------------------------------------------------------
/main/testing/DFS.py:
--------------------------------------------------------------------------------
1 | from .RP import check, count_path
2 |
3 |
4 | def dfs(grid: object):
5 | """
6 | Perform a depth-first search from start to end.
7 |
8 | Args:
9 | grid (Grid): An object representing the current grid.
10 |
11 | Returns:
12 | visited_nodes (int): Count of the number of nodes visited.
13 | count_path (int): Length of the path found.
14 | """
15 |
16 | # Initialize a stack to store the nodes to visit
17 | stack = [grid.start]
18 |
19 | # Initialize a dictionary to store the predecessor of each node
20 | previous = {}
21 |
22 | # Initialize flags to track the search status
23 | found = False
24 |
25 | visited_nodes: int = 0
26 |
27 | # Perform the search
28 | while len(stack) and not found:
29 | # Get the next node to visit
30 | current = stack.pop()
31 |
32 | # Skip the node if it has already been checked
33 | if current.is_checked():
34 | continue
35 |
36 | visited_nodes += 1
37 |
38 | check(current)
39 |
40 | # Check the neighbors of the current node
41 | for neighbor in current.neighbors:
42 | # Skip the neighbor if it has already been checked
43 | if not neighbor.is_checked():
44 | # End the search if the neighbor is the end node
45 | if neighbor.is_end():
46 | previous[neighbor] = current
47 | found = True
48 | break
49 | # Add the neighbor to the stack of nodes to visit if it is not the end node
50 | else:
51 | previous[neighbor] = current
52 | stack.append(neighbor)
53 |
54 | return visited_nodes, count_path(previous, grid.end)
55 |
--------------------------------------------------------------------------------
/main/testing/Dijkstra.py:
--------------------------------------------------------------------------------
1 | from queue import PriorityQueue
2 | from .RP import check, count_path
3 |
4 |
5 | def dijkstra(grid: object):
6 | """
7 | Perform Dijkstra's algorithm from start to end.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid.
11 |
12 | Returns:
13 | visited_nodes (int): Count of the number of nodes visited.
14 | path_size (int): Length of the path found.
15 | """
16 |
17 | # Initialize a priority queue to store the nodes to visit
18 | queue = PriorityQueue()
19 |
20 | # Set up the node values
21 | distance = {node: float("inf") for row in grid.grid for node in row}
22 | distance[grid.start] = 0
23 |
24 | # Holds the path from start to end
25 | previous = {}
26 |
27 | # Add the start node to the priority queue
28 | queue.put((distance[grid.start], 0, grid.start))
29 | count = 0
30 |
31 | visited_nodes: int = 0
32 | path_size: int = 0
33 |
34 | # Perform the search
35 | while not queue.empty():
36 | # Get the next node to visit
37 | current_distance, _, current_min = queue.get()
38 |
39 | visited_nodes += 1
40 |
41 | # End the search if the current node is the end node
42 | if current_min.is_end():
43 | path_size = count_path(previous, grid.end)
44 | break
45 |
46 | check(current_min)
47 |
48 | # Check the neighbors of the current node
49 | for neighbor in current_min.neighbors:
50 | # Don't recheck for performance
51 | if not neighbor.is_checked():
52 | # edges between vertecies are not weighted
53 | # (using constant weight of 1)
54 | temp_value = distance[current_min] + 1
55 | if temp_value < distance[neighbor]:
56 | distance[neighbor] = temp_value
57 | previous[neighbor] = current_min
58 | count += 1
59 |
60 | # Add the neighbor to the priority queue
61 | queue.put((distance[neighbor], count, neighbor))
62 |
63 | return visited_nodes, path_size
64 |
--------------------------------------------------------------------------------
/main/testing/FastMarching.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import heapq
3 | from .RP import count_path
4 |
5 | def fmm(grid: object):
6 | """
7 | Performs the Fast Marching Method on a uniform cost grid
8 | to solve the shortest path problem.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 |
13 | Returns:
14 | visited_nodes (int): Count of the number of nodes visited.
15 | path_size (int): Length of the path found.
16 | """
17 | # Initialize dictionary to store the cost for each node
18 | costs = {node: float("inf") for row in grid.grid for node in row}
19 | costs[grid.start] = 0
20 |
21 | # Record all the unvisited nodes in the grid
22 | far = [node for row in grid.grid for node in row if not node.is_start()]
23 |
24 | # Initialize the visited priority queue
25 | considered = []
26 | heapq.heappush(considered, (0, grid.start))
27 |
28 | came_from = {}
29 |
30 | visited_nodes: int = 0
31 | path_size: int = 0
32 |
33 | # Perform the search
34 | while considered:
35 | # Get the current node from the open set
36 | ccost, current = heapq.heappop(considered)
37 |
38 | visited_nodes += 1
39 |
40 | # End the search if the current node is the end node
41 | if current.is_end():
42 | path_size = count_path(came_from, grid.end)
43 | break
44 |
45 | # Check the neighbors of the current node
46 | for neighbor in current.neighbors:
47 | # If the neighbor hasn't been checked
48 | if neighbor not in considered and not neighbor.is_checked():
49 | # Edges between nodes are not weighted
50 | # (using constant weight of 1)
51 | cost = ccost + 1
52 | if cost < costs[neighbor]:
53 | costs[neighbor] = cost
54 | came_from[neighbor] = current
55 |
56 | # If neighbor hasn't been checked or been visited before
57 | # add it to the queue
58 | if neighbor in far:
59 | heapq.heappush(considered, (cost, neighbor))
60 | far.remove(neighbor)
61 | return visited_nodes, path_size
--------------------------------------------------------------------------------
/main/testing/FloodFill.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import heapq
3 |
4 | def flood_fill(grid: object):
5 | """
6 | Performs the Flood Fill algorithm to solve the
7 | shortest path problem.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid
11 |
12 | Returns:
13 | visited_nodes (int): Count of the number of nodes visited.
14 | path_size (int): Length of the path found.
15 | """
16 | # Create a dictionary to store distance values
17 | distances = { node: float("inf") for row in grid for node in row }
18 | distances[grid.end] = 0
19 |
20 | # Start from end
21 | queue = [(0, grid.end)]
22 |
23 | visited_nodes: int = 0
24 | path_size: int = 0
25 |
26 | while queue:
27 | dist, current = heapq.heappop(queue)
28 |
29 | if current.is_checked():
30 | continue
31 |
32 |
33 | if current.is_start():
34 | break
35 |
36 | if not current.is_end():
37 | current.check()
38 |
39 | visited_nodes += 1
40 |
41 | # Check every neighbor of the current cell and
42 | # add it to the queue if it hasn't been checked before
43 | for neighbor in current.neighbors:
44 | if not neighbor.is_checked():
45 | distances[neighbor] = dist + 1
46 | heapq.heappush(queue, (dist + 1, neighbor))
47 |
48 | # Count path
49 | if distances[grid.start] != float("inf"):
50 | current = grid.start
51 | found = False
52 | while not found:
53 | best = (float("inf"), None)
54 | for neighbor in current.neighbors:
55 | if neighbor.is_end():
56 | found = True
57 | break
58 | if distances[neighbor] < best[0]:
59 | best = (distances[neighbor], neighbor)
60 | if not found:
61 | path_size += 1
62 | current = best[1]
63 |
64 | return visited_nodes, path_size
65 |
--------------------------------------------------------------------------------
/main/testing/FloydWarshall.py:
--------------------------------------------------------------------------------
1 | from .RP import get_unvisited_nodes
2 |
3 |
4 | def count_path(
5 | dist: list,
6 | V: int,
7 | start: object,
8 | end: object,
9 | nodes: list,
10 | checked_nodes: list,
11 | ):
12 | """
13 | Function to reconstruct a path from the start node to the end node based on
14 | the given distance matrix, list of nodes, and list of checked nodes.
15 |
16 | Args:
17 | dist (list): A 2D list representing the distance matrix between nodes.
18 | V (int): The number of nodes in the grid.
19 | start (Node): The starting node.
20 | end (Node): The ending node.
21 | nodes (list): A list of all nodes in the grid.
22 | checked_nodes (list): A list of nodes that have been visited during the search.
23 |
24 | Returns:
25 | error_value or path_size (int):
26 | -1 if the end node wasn't checked;
27 | -2 if the end node wasn't in the list of connected nodes;
28 | Length of the path found.
29 | """
30 |
31 | path_size: int = 0
32 |
33 | # If the end node is not in the list of nodes, return False
34 | try:
35 | test = nodes.index(end)
36 | if end not in checked_nodes:
37 | return -1
38 | except ValueError:
39 | return -2
40 |
41 | # Get the indices of the start and end nodes in the list of nodes
42 | u, v = nodes.index(start), nodes.index(end)
43 |
44 | # Initialize empty lists for the path, left-side distances,
45 | # and right-side distances
46 | path = []
47 | left = []
48 | right = []
49 | current = v
50 |
51 | # Iterate backwards through the distance matrix
52 | for k in range(V - 1, -1, -1):
53 | # If the distance between the start and end nodes is equal to the
54 | # distance between the start node and the k-th node plus the distance
55 | # between the k-th node and the end node, add the k-th node to the
56 | # path and add the coresponding distances to the left and right lists
57 | if dist[u][v] == dist[u][k] + dist[k][v]:
58 | # Only add the node to the path if it is not the start or end node
59 | # and its distance values are not already in the
60 | # left and right lists
61 | if (
62 | not nodes[k].is_start()
63 | and not nodes[k].is_end()
64 | and dist[u][k] not in left
65 | and dist[k][v] not in right
66 | ):
67 | path.append(nodes[k])
68 | left.append(dist[u][k])
69 | right.append(dist[k][v])
70 | current = k
71 |
72 | # Set the current node to the end node
73 | curr = end
74 | # Keep looping until the current node is the start node
75 | while curr != start:
76 | # Check each neighbor of the current node
77 | for node in curr.neighbors:
78 | # If the neighbor is the start node, return True
79 | if node.is_start():
80 | return path_size
81 |
82 | # If the neighbor is in the path, mark it as part of the path and
83 | # set the current node to the neighbor
84 | if node in path:
85 | path_size += 1
86 | path.remove(node)
87 | curr = node
88 |
89 | return path_size
90 |
91 |
92 | def floyd_warshall(grid: object):
93 | """
94 | Implements the Floyd-Warshall algorithm to find the shortest path between
95 | the start and end nodes in the given grid.
96 |
97 | Args:
98 | grid (Grid): An object representing the current grid.
99 |
100 | Returns:
101 | visited_nodes (int): Count of the number of nodes visited.
102 | count_path (int): Length of the path found.
103 | """
104 |
105 | # Get a list of all unvisited nodes, excluding the start node
106 | nodes = get_unvisited_nodes(grid.start)
107 |
108 | # Get the number of nodes
109 | V = len(nodes)
110 |
111 | # Initialize the distance matrix with all values set to infinity
112 | distance = [[float("inf") for _ in range(V)] for _ in range(V)]
113 |
114 | # Used for path reconstruction
115 | checked_nodes = [grid.start]
116 |
117 | visited_nodes: int = 0
118 |
119 | # Initialize the distance values in the distance matrix
120 | for i in range(V):
121 | for j in range(V):
122 | visited_nodes += 1
123 |
124 | # If the nodes are neighbors, set the distance to 1
125 | if nodes[i] in nodes[j].neighbors:
126 | distance[i][j] = 1
127 |
128 | # If the two nodes are the same, set the distance to 0
129 | for i in range(V):
130 | visited_nodes += 1
131 | distance[i][i] = 0
132 |
133 |
134 | # Iterate through all nodes in the list
135 | for k in range(V):
136 | # Iterate through all pairs of nodes in the list
137 | for i in range(V):
138 | for j in range(V):
139 | visited_nodes += 1
140 | # If the distance between the two nodes is currently longer than
141 | # the path through the k-th node, set it to the new shorter distance
142 | if distance[i][j] > distance[i][k] + distance[k][j]:
143 | distance[i][j] = distance[i][k] + distance[k][j]
144 |
145 | # If none of the nodes are the start or end nodes, check them again
146 | if (
147 | not nodes[i].is_start()
148 | and not nodes[i].is_end()
149 | and not nodes[j].is_start()
150 | and not nodes[j].is_end()
151 | and not nodes[k].is_start()
152 | and not nodes[k].is_end()
153 | ):
154 | checked_nodes.append(nodes[i])
155 | checked_nodes.append(nodes[j])
156 | checked_nodes.append(nodes[k])
157 |
158 | # Adding end if it is connected to start
159 | try:
160 | test = nodes.index(grid.end)
161 | checked_nodes.append(grid.end)
162 | except ValueError:
163 | pass
164 |
165 | return visited_nodes, count_path(
166 | distance, V, grid.start, grid.end, nodes, checked_nodes
167 | )
168 |
--------------------------------------------------------------------------------
/main/testing/Fringe.py:
--------------------------------------------------------------------------------
1 | from .RP import heuristic
2 |
3 | def count_path(cache: dict, current: object):
4 | """
5 | Counts the path size.
6 |
7 | Args:
8 | cache (dict): the cache of nodes in the grid that were visited
9 | current (Node): the ending node
10 |
11 | Returns:
12 | path (int): path size
13 | """
14 | path = -1
15 | while not current.is_start():
16 | path += 1
17 | _, current = cache[current]
18 | return path
19 |
20 | def fringe_search(grid: object):
21 | """
22 | Performs a Fringe Search over the grid.
23 |
24 | Args:
25 | grid (Grid): An object representing the current grid
26 |
27 | Returns:
28 | visited_nodes (int): Count of the number of nodes visited.
29 | path_size (int): Length of the path found.
30 | """
31 | fringe = []
32 | cache = {}
33 | cache[grid.start] = (0, None)
34 | fringe.append(grid.start)
35 |
36 | f_limit = heuristic("manhattan", grid.start, grid.end)
37 |
38 | found = False
39 |
40 | visited_nodes: int = 0
41 | path_size: int = 0
42 |
43 | while not found and fringe:
44 | f_min = float("inf")
45 | # Check every node in the fringe
46 | for node in fringe:
47 | g, parent = cache[node]
48 | # Get f-score for current node
49 | f = g + heuristic("manhattan", node, grid.end)
50 | # Check if the f-score is greater than the allowed limit
51 | if f > f_limit:
52 | f_min = min(f, f_min)
53 | continue
54 |
55 | visited_nodes += 1
56 |
57 | if node.is_end():
58 | path_size = count_path(cache, node)
59 | found = True
60 | break
61 |
62 | for child in node.neighbors:
63 | g_child = g + 1
64 | # If the child node has already been seen
65 | if child in cache:
66 | g_cached, c_parent = cache[child]
67 | if g_child >= g_cached:
68 | continue
69 | if child in fringe:
70 | fringe.remove(child)
71 | fringe.append(child)
72 | cache[child] = (g_child, node)
73 | fringe.remove(node)
74 | f_limit = f_min
75 | return visited_nodes, path_size
--------------------------------------------------------------------------------
/main/testing/GBFS.py:
--------------------------------------------------------------------------------
1 | from queue import PriorityQueue
2 | from .RP import heuristic, check, count_path
3 |
4 |
5 | def gbfs(grid: object):
6 | """
7 | Perform a greedy best-first search from start to end.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid.
11 |
12 | Returns:
13 | visited_nodes (int): Count of the number of nodes visited.
14 | count_path (int): Length of the path found.
15 | """
16 |
17 | # Initialize priority queue with the start node
18 | Q = PriorityQueue()
19 | Q.put((heuristic("manhattan", grid.start, grid.end), 0, grid.start))
20 |
21 | # Initialize counters and flags
22 | counter = 0
23 | found = False
24 |
25 | # Initialize a dictionary to store the previous nodes for each node
26 | previous = {}
27 |
28 | visited_nodes: int = 0
29 |
30 | # Perform the search
31 | while not Q.empty() and not found:
32 | # Get the current node from the queue
33 | _, _, current = Q.get()
34 |
35 | # Skip if the node has already been checked
36 | if current.is_checked():
37 | continue
38 |
39 | visited_nodes += 1
40 |
41 | # Draw the current node
42 | check(current)
43 |
44 | # Check the neighbors of the current node
45 | for neighbor in current.neighbors:
46 | # Skip if the neighbor has already been checked
47 | if not neighbor.is_checked():
48 | # End the search if the neighbor is the end node
49 | if neighbor.is_end():
50 | previous[neighbor] = current
51 | visited_nodes += 1
52 | found = True
53 | break
54 |
55 | # Add the neighbor to the queue
56 | counter += 1
57 | distance = heuristic("manhattan", neighbor, grid.end)
58 | previous[neighbor] = current
59 | Q.put((distance, counter, neighbor))
60 | return visited_nodes, count_path(previous, grid.end)
61 |
--------------------------------------------------------------------------------
/main/testing/GBLS.py:
--------------------------------------------------------------------------------
1 | from queue import PriorityQueue
2 | from .RP import heuristic, check, count_path
3 |
4 |
5 | def gbls(grid: object):
6 | """
7 | Modified version of the greedy best-first search algorithm that explores neighbors
8 | in the direction of the last chosen node as long as the estimated distance to the
9 | goal is shorter than the current node.
10 |
11 | Args:
12 | grid (Grid): An object representing the current grid.
13 |
14 | Returns:
15 | visited_nodes (int): Count of the number of nodes visited.
16 | count_path (int): Length of the path found.
17 | """
18 |
19 | # Initialize the priority queue with the starting node
20 | Q = PriorityQueue()
21 | Q.put((heuristic("manhattan", grid.start, grid.end), 0, grid.start))
22 |
23 | # Initialize the counter and flags
24 | counter = 0
25 | found = False
26 |
27 | previous = {}
28 | last_direction = None
29 |
30 | visited_nodes: int = 0
31 |
32 | # Continue the search as long as there are nodes in the queue or the goal has
33 | # not been found
34 | while not Q.empty() and not found:
35 | _, _, current = Q.get()
36 |
37 | if current.is_checked():
38 | continue
39 |
40 | visited_nodes += 1
41 |
42 | check(current)
43 |
44 | # Choose the neighbors in the last direction first,
45 | # if a direction has been chosen
46 | if last_direction:
47 | neighbors = [
48 | n
49 | for n in current.neighbors
50 | if n.get_pos()[0] - current.get_pos()[0] == last_direction[0]
51 | and n.get_pos()[1] - current.get_pos()[1] == last_direction[1]
52 | ]
53 |
54 | # Add the other neighbors to the list
55 | neighbors += [n for n in current.neighbors if n not in neighbors]
56 | # If no direction has been chosen, explore all neighbors
57 | else:
58 | neighbors = current.neighbors
59 |
60 | for neighbor in neighbors:
61 | if not neighbor.is_checked():
62 | if neighbor.is_end():
63 | previous[neighbor] = current
64 | visited_nodes += 1
65 | found = True
66 | break
67 |
68 | counter += 1
69 | distance = heuristic("manhattan", neighbor, grid.end)
70 |
71 | # Add the neighbor to the queue with the estimated
72 | # distance as the priority
73 | previous[neighbor] = current
74 | Q.put((distance, counter, neighbor))
75 |
76 | # Save the direction to the neighbor
77 | last_direction = (
78 | neighbor.get_pos()[0] - current.get_pos()[0],
79 | neighbor.get_pos()[1] - current.get_pos()[1],
80 | )
81 | return visited_nodes, count_path(previous, grid.end)
82 |
--------------------------------------------------------------------------------
/main/testing/IDAStar.py:
--------------------------------------------------------------------------------
1 | from .RP import heuristic
2 |
3 |
4 | def ida_star(grid: object):
5 | """
6 | Implements the IDA* search algorithm.
7 |
8 | Args:
9 | grid (Grid): An object representing the current grid.
10 |
11 | Returns:
12 | visited_nodes (int): Count of the number of nodes visited.
13 | path_size (int): Length of the path found.
14 | """
15 | bound = heuristic("manhattan", grid.start, grid.end)
16 |
17 | # Stack for path being searched
18 | path = [grid.start]
19 |
20 | visited_nodes: int = 0
21 |
22 | while True:
23 | result, temp_v_n = search(visited_nodes, path, 0, bound, grid.end)
24 |
25 | visited_nodes = temp_v_n
26 |
27 | if result is True:
28 | break
29 | if result == float("inf"):
30 | break
31 |
32 | bound = result
33 |
34 | return visited_nodes, len(path) - 2
35 |
36 |
37 | def search(visited_nodes: int, path: list, g: int, bound: float, end: object):
38 | """
39 | Recursive search function used by the IDA* algorithm.
40 |
41 | Args:
42 | visited_nodes (int): Count of the number of nodes visited.
43 | path (list): The list of nodes representing the path taken by the
44 | search algorithm.
45 | g (int): The cost of the path represented by the list of nodes.
46 | bound (float): The bound on the cost of the path.
47 | end (Node): The ending node.
48 | Returns:
49 | True or f (boolean or float):
50 | end was reached;
51 | g-score + heuristic is greater than current depth (bound)
52 | min_val (float): minimum value for the path
53 | """
54 | # Get last node in path
55 | node = path[-1]
56 |
57 | visited_nodes += 1
58 |
59 | # Calculate the f-score
60 | f = g + heuristic("manhattan", node, end)
61 |
62 | if f > bound:
63 | return f, visited_nodes
64 | if node.is_end():
65 | return True, visited_nodes
66 |
67 | min_val = float("inf")
68 | for neighbor in node.neighbors:
69 | if neighbor not in path:
70 | path.append(neighbor)
71 | result, temp_v_n = search(visited_nodes, path, g + 1, bound, end)
72 | visited_nodes = temp_v_n
73 | if result is True:
74 | return True, visited_nodes
75 | if result < min_val:
76 | min_val = result
77 |
78 | path.pop()
79 | return min_val, visited_nodes
80 |
--------------------------------------------------------------------------------
/main/testing/IDDFS.py:
--------------------------------------------------------------------------------
1 | from .RP import count_path
2 |
3 |
4 | def depth_limit(
5 | visited_nodes: int,
6 | current: object,
7 | depth: int,
8 | visited: set,
9 | path: dict,
10 | ):
11 | """
12 | Recursive DFS with a depth limit.
13 |
14 | Args:
15 | visited_nodes (int): Count of the number of nodes visited.
16 | current (Node): Current node being visited.
17 | depth (int): Current depth of the search.
18 | visited (set): Set of nodes already visited.
19 | path (dict): Dictionary of parent nodes, used to reconstruct the path.
20 |
21 | Returns:
22 | dict: Dictionary of parent nodes, used to reconstruct the path.
23 | Returns an empty dictionary if no path is found.
24 | visited_nodes (int): Count of the number of nodes visited.
25 | """
26 | if depth < 0:
27 | return {}, visited_nodes
28 | if current.is_end():
29 | visited_nodes += 1
30 | return path, visited_nodes
31 |
32 | visited.add(current)
33 |
34 | visited_nodes += 1
35 |
36 | for neighbor in current.neighbors:
37 | if neighbor not in visited:
38 | path[neighbor] = current
39 |
40 | result, temp_v_n = depth_limit(
41 | visited_nodes, neighbor, depth - 1, visited, path
42 | )
43 |
44 | visited_nodes = temp_v_n
45 |
46 | if result:
47 | return result, visited_nodes
48 |
49 | return {}, visited_nodes
50 |
51 |
52 | def iddfs(grid: object, length: int):
53 | """
54 | Iterative deepening DFS.
55 |
56 | Args:
57 | grid (Grid): An object representing the current grid.
58 | length (int): Length of the grid.
59 |
60 | Returns:
61 | visited_nodes (int): Count of the number of nodes visited.
62 | path_size (int): Length of the path found.
63 | """
64 |
65 | visited_nodes: int = 0
66 | path_size: int = 0
67 |
68 | for depth in range(0, length):
69 | visited = set()
70 | path, temp_v_n = depth_limit(
71 | visited_nodes, grid.start, depth, visited, {grid.start: None}
72 | )
73 |
74 | visited_nodes = temp_v_n
75 |
76 | if path:
77 | path_size = count_path(path, grid.end)
78 | break
79 | return visited_nodes, path_size
80 |
--------------------------------------------------------------------------------
/main/testing/JPS.py:
--------------------------------------------------------------------------------
1 | from .RP import heuristic, count_path
2 |
3 |
4 | def jps(grid: object):
5 | """
6 | Perform an Jump Point Search from start to end.
7 |
8 | Args:
9 | grid (Grid): An object representing the current grid
10 |
11 | Returns:
12 | visited_nodes (int): Count of the number of nodes visited.
13 | path_size (int): Length of the path found.
14 | """
15 |
16 | open_set = set([grid.start])
17 | closed_set = set()
18 |
19 | came_from = {}
20 |
21 | g_values = {grid.start: 0}
22 | f_values = {grid.start: heuristic("manhattan", grid.start, grid.end)}
23 |
24 | visited_nodes: int = 0
25 | path_size: int = 0
26 |
27 | while open_set:
28 | # Find the node with the lowest f-value in the open set.
29 | current = min(open_set, key=lambda node: f_values[node])
30 |
31 | visited_nodes += 1
32 |
33 | if current.is_end():
34 | path_size = count_path(came_from, grid.end)
35 | break
36 |
37 | # Move current from open set to closed set
38 | open_set.remove(current)
39 | closed_set.add(current)
40 |
41 | for neighbor in current.neighbors:
42 | # Check if neighbor is already closed
43 | if neighbor in closed_set:
44 | continue
45 |
46 | if neighbor not in open_set:
47 | g_value = g_values[current] + heuristic("manhattan", current, neighbor)
48 |
49 | g_values[neighbor] = g_value
50 | f_values[neighbor] = g_value + heuristic(
51 | "manhattan", neighbor, grid.end
52 | )
53 |
54 | came_from[neighbor] = current
55 | open_set.add(neighbor)
56 | return visited_nodes, path_size
57 |
--------------------------------------------------------------------------------
/main/testing/LPAStar.py:
--------------------------------------------------------------------------------
1 | import heapq
2 | from .RP import heuristic
3 |
4 |
5 | def get_checked_neighbors(node: object):
6 | """
7 | Get the neighbors of a node that have been checked or are the start node.
8 |
9 | Args:
10 | node (Node): The node to check.
11 |
12 | Returns:
13 | List[Node]: The list of checked or start neighbors.
14 | """
15 |
16 | checked = []
17 | for neighbor in node.neighbors:
18 | if neighbor.is_checked() or neighbor.is_start():
19 | checked.append(neighbor)
20 | return checked
21 |
22 |
23 | def count_path(current: object, costs: dict, path_size: int):
24 | """
25 | Reconstruct the path from the end node to the start node.
26 |
27 | Args:
28 | current (Node): The current node in the search.
29 | costs (Dict[Node, int]): The cost of reaching each node from the start.
30 | path_size (int): Counter for path size found.
31 |
32 | Returns:
33 | None: The function updates the screen with the path.
34 | """
35 |
36 | # Continue until the current node is the start node
37 | while not current.is_start():
38 | # Find the neighbor with the lowest cost
39 | current = min(get_checked_neighbors(current), key=lambda x: costs[x])
40 |
41 | # Break if the current node is the start node
42 | if current.is_start():
43 | break
44 |
45 | path_size += 1
46 | return path_size
47 |
48 |
49 | def update_vertex(grid: object, node: object, rhs: dict, g: dict, open_list: list):
50 | """
51 | Updates rhs values, and open list
52 |
53 | Args:
54 | grid (Grid): An object representing the current grid.
55 | node (Node): Current node being checked.
56 | rhs (dict): Dictionary of the rhs values.
57 | g (dict): Dictionary of the g values.
58 | open_list (list): List of open nodes.
59 |
60 | Returns:
61 | open_list: List of open nodes.
62 | """
63 | if node != grid.start:
64 | rhs[node] = float("inf")
65 | for neighbor in node.neighbors:
66 | if (
67 | neighbor.is_checked()
68 | or neighbor.is_start()
69 | or neighbor.is_unchecked()
70 | or (neighbor.is_end() and neighbor.been_checked)
71 | ):
72 | rhs[node] = min(rhs[node], g[neighbor] + 1)
73 |
74 | open_list = [(key, value) for key, value in open_list if value != node]
75 | heapq.heapify(open_list)
76 |
77 | if g[node] != rhs[node]:
78 | heapq.heappush(open_list, (calc_key(grid.end, node, g, rhs), node))
79 | return open_list
80 |
81 |
82 | def calc_key(end: object, node: object, g: dict, rhs: dict):
83 | """
84 | Calculates the key for the priority queue
85 |
86 | Args:
87 | end (Node): The end node of the current grid.
88 | node (Node): Current node.
89 | g (dict): Dictionary of the g values.
90 | rhs (dict): Dictionary of the rhs values.
91 |
92 | Returns:
93 | tuple: (min of g & rhs + heuristic, min of g & rhs)
94 | """
95 | return (
96 | min(g[node], rhs[node]) + heuristic("manhattan", node, end),
97 | min(g[node], rhs[node]),
98 | )
99 |
100 |
101 | def lpa_star(grid: object):
102 | """
103 | Performs the LPA* algorithm from start to end.
104 |
105 | Args:
106 | grid (Grid): An object representing the current grid
107 |
108 | Returns:
109 | visited_nodes (int): Count of the number of nodes visited.
110 | path_size (int): Length of the path found.
111 | """
112 | rhs = {node: float("inf") for row in grid.grid for node in row}
113 | g = {node: float("inf") for row in grid.grid for node in row}
114 | open_list = []
115 |
116 | rhs[grid.start] = 0
117 | topKey = calc_key(grid.end, grid.start, g, rhs)
118 | open_list.append((topKey, grid.start))
119 |
120 | visited_nodes: int = 0
121 | path_size: int = 0
122 |
123 | while topKey < calc_key(grid.end, grid.end, g, rhs) or rhs[grid.end] != g[grid.end]:
124 | # Check for no path found
125 | if open_list:
126 | topKey, node = heapq.heappop(open_list)
127 | visited_nodes += 1
128 | else:
129 | break
130 |
131 | if g[node] > rhs[node]:
132 | g[node] = rhs[node]
133 | else:
134 | g[node] = float("inf")
135 | open_list = update_vertex(grid, node, rhs, g, open_list)
136 |
137 | # Update neighboring nodes
138 | for neighbor in node.neighbors:
139 | if (
140 | not neighbor.is_start()
141 | and not neighbor.is_end()
142 | and not neighbor.is_checked()
143 | ):
144 | neighbor.uncheck()
145 | open_list = update_vertex(grid, neighbor, rhs, g, open_list)
146 |
147 | # Check the current node if it is not the start node or end node
148 | if not node.is_start() and not node.is_end():
149 | node.check()
150 |
151 | # Reconstruct path if at end
152 | if node == grid.end:
153 | path_size = count_path(node, rhs, path_size)
154 | return visited_nodes, path_size
--------------------------------------------------------------------------------
/main/testing/LexicographicBFS.py:
--------------------------------------------------------------------------------
1 | from .RP import count_path
2 |
3 |
4 | def lex_bfs(visited_nodes: int, start: object):
5 | """
6 | Implements the Lexicographic Breadth-First Search (LexBFS) algorithm.
7 |
8 | Args:
9 | visited_nodes (int): Count of the number of nodes visited.
10 | start (start): The starting node.
11 |
12 | Returns:
13 | order (list): A list of nodes in the graph, in the order they were
14 | visited by LexBFS.
15 | levels (dict): A dictionary containing the level of each node in the
16 | breadth-first search tree.
17 | visited_nodes (int): Count of the number of nodes visited.
18 | parent (dict): A dictionary containing the parent of each node checked.
19 | """
20 |
21 | # Initialize the list to store the order of the vertices
22 | order = []
23 |
24 | # Initialize the dictionaries to store the levels and parents of
25 | # the vertices
26 | levels = {}
27 | parent = {}
28 |
29 | # Set the level of the starting vertex to 0 and its parent to None
30 | levels[start] = 0
31 | parent[start] = None
32 |
33 | # Initialize the counter and the queue
34 | level = 0
35 | Q = [start]
36 |
37 | # Run the algorithm until the queue is empty or the flag is set to False
38 | while Q:
39 | # Pop the first element from the queue
40 | current = Q.pop(0)
41 |
42 | visited_nodes += 1
43 |
44 | order.append(current)
45 |
46 | for neighbor in current.neighbors:
47 | # If neighbor has not been visited before
48 | if neighbor not in levels:
49 | # Set the level of neighbor to the current level + 1
50 | levels[neighbor] = level + 1
51 | parent[neighbor] = current
52 | Q.append(neighbor)
53 | level = level + 1
54 |
55 | return order, levels, visited_nodes, parent
56 |
57 |
58 | def lbfs(grid: object):
59 | """
60 | Solves the shortest path problem using LexBFS.
61 |
62 | Args:
63 | grid (Grid): An object representing the current grid.
64 |
65 | Returns:
66 | visited_nodes (int): Count of the number of nodes visited.
67 | count_path (int): Length of the path found.
68 | """
69 |
70 | visited_nodes: int = 0
71 |
72 | # Run LexBFS to get the order of the vertices
73 | order, levels, temp_v_n, parent = lex_bfs(visited_nodes, grid.start)
74 |
75 | visited_nodes = temp_v_n
76 |
77 | if order is not False:
78 | # Initialize the distances dictionary
79 | distances = {node: float("inf") for row in grid.grid for node in row}
80 | distances[grid.start] = 0
81 |
82 | # Iterate through the vertices in the order produced by LexBFS
83 | for current in order:
84 | visited_nodes += 1
85 | # Update the distances of the neighbors of current
86 | for neighbor in current.neighbors:
87 | if distances[current] + 1 < distances[neighbor]:
88 | distances[neighbor] = distances[current] + 1
89 | parent[neighbor] = current
90 |
91 | return visited_nodes, count_path(parent, grid.end)
92 |
--------------------------------------------------------------------------------
/main/testing/RandomLIFO.py:
--------------------------------------------------------------------------------
1 | import pygame
2 | import random
3 | from queue import LifoQueue
4 | from .RP import count_path
5 |
6 | def rand_lifo(grid: object):
7 | """
8 | Runs a Random search on the grid using a LIFO Queue
9 | to choose the next node to check.
10 |
11 | Args:
12 | grid (Grid): An object representing the current grid.
13 |
14 | Returns:
15 | visited_nodes (int): Count of the number of nodes visited.
16 | path_size (int): Length of the path found.
17 | """
18 | # Initialize path and queue
19 | came_from = {}
20 | queue = LifoQueue()
21 | queue.put(grid.start)
22 |
23 | visited_nodes: int = 0
24 | path_size: int = 0
25 |
26 | # Run search till queue is empty
27 | while not queue.empty():
28 | current = queue.get()
29 |
30 |
31 | # End search if target is found
32 | if current.is_end():
33 | path_size = count_path(came_from, current)
34 | break
35 |
36 | visited_nodes += 1
37 |
38 | if not current.is_start():
39 | current.check()
40 |
41 | # Choose a random neighbor of the current node
42 | next_node = random.randint(0, len(current.neighbors) - 1)
43 |
44 | # Add every possible neighbor to the queue in the correct order
45 | for index, neighbor in enumerate(current.neighbors):
46 | if neighbor.is_checked():
47 | continue
48 |
49 | came_from[neighbor] = current
50 | if index != next_node:
51 | queue.put(neighbor)
52 |
53 | if not current.neighbors[next_node].is_checked():
54 | queue.put(current.neighbors[next_node])
55 | return visited_nodes, path_size
--------------------------------------------------------------------------------
/main/testing/RandomWalk.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 |
4 | def rand_walk(grid: object):
5 | """
6 | Generates a random walk from the starting node to the goal node.
7 |
8 | Parameters:
9 | grid (Grid): An object representing the current grid.
10 |
11 | Returns:
12 | visited_nodes (int): Count of the number of nodes visited.
13 | path_size (int): Length of the path found.
14 | """
15 |
16 | # Initialize the list of previous nodes and the current node
17 | came_from = []
18 | current = grid.start
19 |
20 | visited_nodes: int = 0
21 |
22 | # Continue the search until the goal is reached
23 | while True:
24 | # Choose a random neighbor of the current node
25 | neighbor = random.randint(0, len(current.neighbors) - 1)
26 |
27 | # Add the current node to the list of previous nodes if it
28 | # has not already been added
29 | if current not in came_from:
30 | came_from.append(current)
31 |
32 | # Move to the chosen neighbor
33 | current = current.neighbors[neighbor]
34 |
35 | visited_nodes += 1
36 |
37 | if current.is_end():
38 | break
39 | return visited_nodes, len(came_from) - 1
40 |
--------------------------------------------------------------------------------
/main/testing/SPFA.py:
--------------------------------------------------------------------------------
1 | from queue import Queue
2 | from .RP import count_path
3 |
4 | def spfa(grid: object):
5 | """
6 | Performs the Shortest Path Faster Algorithm for the given grid.
7 |
8 | Args:
9 | grid (Grid): An object representing the current grid.
10 |
11 | Returns:
12 | visited_nodes (int): Count of the number of nodes visited.
13 | count_path (int): Length of the path found.
14 | """
15 | d = {node: float("inf") for row in grid.grid for node in row}
16 | d[grid.start] = 0
17 |
18 | queue = Queue()
19 | queue.put(grid.start)
20 |
21 | path = {}
22 |
23 | visited_nodes: int = 0
24 |
25 | while not queue.empty():
26 | u = queue.get()
27 |
28 | visited_nodes += 1
29 |
30 | for v in u.neighbors:
31 | if d[u] + 1 < d[v]:
32 | # Update the distance and predecessor for the neighbor if the
33 | # current distance is shorter
34 | d[v] = d[u] + 1
35 | path[v] = u
36 |
37 | if v not in queue.queue:
38 | queue.put(v)
39 |
40 | return visited_nodes, count_path(path, grid.end)
--------------------------------------------------------------------------------
/main/testing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/main/testing/__init__.py
--------------------------------------------------------------------------------
/main/testing/bidirectional/AStar.py:
--------------------------------------------------------------------------------
1 | from queue import PriorityQueue
2 | from ..RP import heuristic, thread_construct
3 |
4 |
5 | def bi_a_star(grid: object):
6 | """
7 | Performs a bidirectional search to find the shortest path between the
8 | start and end nodes. The search is visualized using the given draw object.
9 |
10 | Args:
11 | grid (Grid): An object representing the current grid
12 |
13 | Returns:
14 | None: The function updates the screen with the search progress and path.
15 | """
16 |
17 | # Initialize two queues, one for each direction of the search
18 | count_start = 0
19 | count_end = 0
20 |
21 | open_set_start = PriorityQueue()
22 | open_set_end = PriorityQueue()
23 |
24 | open_set_start.put((0, 0, grid.start))
25 | open_set_end.put((0, 0, grid.end))
26 |
27 | came_from_start = {}
28 | came_from_end = {}
29 |
30 | g_score_start = {node: float("inf") for row in grid.grid for node in row}
31 | g_score_start[grid.start] = 0
32 |
33 | g_score_end = {node: float("inf") for row in grid.grid for node in row}
34 | g_score_end[grid.end] = 0
35 |
36 | # Initialize two sets to keep track of visited nodes, one for
37 | # each direction of the search
38 | visited_start = set()
39 | visited_end = set()
40 |
41 | # Add the start and end nodes to the path dictionaries
42 | came_from_start[grid.start] = grid.start
43 | came_from_end[grid.end] = grid.end
44 |
45 | visited_nodes: int = 0
46 | path_size: int = 0
47 |
48 | # Loop until one of the queues is empty
49 | while not open_set_start.empty() and not open_set_end.empty():
50 | # Dequeue a node from each queue and process it
51 | start_score, start_count, start_node = open_set_start.get()
52 |
53 | # Check for redundant checks
54 | if start_node in visited_start:
55 | continue
56 |
57 | _, _, end_node = open_set_end.get()
58 | # Check for redundant checks
59 | if end_node in visited_end:
60 | open_set_start.put((start_score, start_count, start_node))
61 | continue
62 |
63 | # Check if the nodes have already been visited from the other direction
64 | if start_node in visited_end:
65 | # Construct two threads to reconstruct the path from the start and
66 | # end directions
67 | path_size = thread_construct(
68 | (came_from_start, start_node, grid.start),
69 | (came_from_end, start_node, grid.end),
70 | )
71 | break
72 | if end_node in visited_start:
73 | # Construct two threads to reconstruct the path from the start and
74 | # end directions
75 | path_size = thread_construct(
76 | (came_from_start, end_node, grid.start),
77 | (came_from_end, end_node, grid.end),
78 | )
79 | break
80 |
81 | visited_nodes += 2
82 |
83 | # Add the neighbors of the dequeued nodes to their respective queues
84 | for neighbor in start_node.neighbors:
85 | temp_g_score = g_score_start[start_node] + 1
86 |
87 | if temp_g_score < g_score_start[neighbor]:
88 | came_from_start[neighbor] = start_node
89 | g_score_start[neighbor] = temp_g_score
90 | if neighbor not in visited_start:
91 | f_score = temp_g_score + heuristic("manhattan", neighbor, grid.end)
92 | count_start += 1
93 | open_set_start.put((f_score, count_start, neighbor))
94 | visited_start.add(start_node)
95 |
96 | for neighbor in end_node.neighbors:
97 | temp_g_score = g_score_end[end_node] + 1
98 |
99 | if temp_g_score < g_score_end[neighbor]:
100 | came_from_end[neighbor] = end_node
101 | g_score_end[neighbor] = temp_g_score
102 | if neighbor not in visited_end:
103 | f_score = temp_g_score + heuristic("manhattan", neighbor, grid.start)
104 | count_end += 1
105 | open_set_end.put((f_score, count_end, neighbor))
106 | visited_end.add(end_node)
107 |
108 | return visited_nodes, path_size
109 |
--------------------------------------------------------------------------------
/main/testing/bidirectional/BFS.py:
--------------------------------------------------------------------------------
1 | from ..RP import thread_construct
2 |
3 |
4 | def bi_search(grid: object):
5 | """
6 | Performs a bidirectional search to find the shortest path between the
7 | start and end nodes.
8 |
9 | Args:
10 | grid (Grid): An object representing the current grid.
11 |
12 | Returns:
13 | visited_nodes (int): Count of the number of nodes visited.
14 | path_size (int): Length of the path found.
15 | """
16 |
17 | # Initialize two queues, one for each direction of the search
18 | queue_start = [grid.start]
19 | queue_end = [grid.end]
20 |
21 | # Initialize two sets to keep track of visited nodes, one for
22 | # each direction of the search
23 | visited_start = set()
24 | visited_end = set()
25 |
26 | # Initialize dictionaries to store the paths taken by each search direction
27 | start_path = {}
28 | end_path = {}
29 |
30 | # Add the start and end nodes to the path dictionaries
31 | start_path[grid.start] = grid.start
32 | end_path[grid.end] = grid.end
33 |
34 | visited_nodes: int = 0
35 | path_size: int = 0
36 |
37 | # Loop until one of the queues is empty
38 | while queue_start and queue_end:
39 | # Dequeue a node from each queue and process it
40 | start_node = queue_start.pop(0)
41 | # Check for redundant checks
42 | if start_node in visited_start:
43 | continue
44 |
45 | end_node = queue_end.pop(0)
46 | # Check for redundant checks
47 | if end_node in visited_end:
48 | queue_start.insert(0, start_node)
49 | continue
50 |
51 | # Check if the nodes have already been visited from the other direction
52 | if start_node in visited_end:
53 | # Construct two threads to reconstruct the path from the start and
54 | # end directions
55 | path_size = thread_construct(
56 | (start_path, start_node, grid.start), (end_path, start_node, grid.end)
57 | )
58 | break
59 |
60 | if end_node in visited_start:
61 | # Construct two threads to reconstruct the path from the start and
62 | # end directions
63 | path_size = thread_construct(
64 | (start_path, end_node, grid.start), (end_path, end_node, grid.end)
65 | )
66 | break
67 |
68 | visited_nodes += 2
69 |
70 | # Mark the nodes as visited
71 | visited_start.add(start_node)
72 | visited_end.add(end_node)
73 |
74 | # Add the neighbors of the dequeued nodes to their respective queues
75 | loop_helper(start_node, visited_start, queue_start, start_path)
76 | loop_helper(end_node, visited_end, queue_end, end_path)
77 |
78 | return visited_nodes, path_size
79 |
80 |
81 | def loop_helper(node: object, visited: set, queue: list, path: dict):
82 | """
83 | Does the neighbor search.
84 |
85 | Args:
86 | node (Node): The node being checked.
87 | visited (set): The set of visited nodes from the side the node is being
88 | checked from.
89 | queue (list): The list of node to visit.
90 | path (dict): The path taken from the side the node is being checked.
91 |
92 | Returns:
93 | None
94 | """
95 | for neighbor in node.neighbors:
96 | if neighbor not in visited:
97 | queue.append(neighbor)
98 | path[neighbor] = node
--------------------------------------------------------------------------------
/main/testing/bidirectional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/main/testing/bidirectional/__init__.py
--------------------------------------------------------------------------------
/main/testing/results/Generated_Maze-Example.csv:
--------------------------------------------------------------------------------
1 | Algorithm,Local Run Time,Times Nodes Checked,Path Length,General Data
2 | ,,,,Total Open Nodes: 1165
3 | astar,0.0009820179993766942,157,65
4 | beam,0.0002225520001957193,72,65
5 | bellford,0.4267947219996131,1357225,65
6 | bestfs,0.0007002390002526226,157,65
7 | bfs,0.0004396410004119389,445,65
8 | biastar,0.0016367050002372707,198,65
9 | bisearch,0.00041756000064196996,312,65
10 | bstar,0.0009466930005146423,157,65
11 | dfs,0.001031709999551822,1146,65
12 | dijkstra,0.0018467629997758195,468,65
13 | flood,0.001915261000249302,969,65
14 | floyd,203.65243170800022,1582525515,65
15 | fmm,0.09329834500022116,480,65
16 | fringe,0.0021174599996811594,668,65
17 | gbfs,0.0004050409997944371,72,65
18 | gbls,0.0005712299998776871,75,65
19 | ida,0.002999525000632275,654,65
20 | iddfs,0.005324234999534383,9923,65
21 | jps,0.0005544420000660466,157,65
22 | lbfs,0.0013342349993763492,2330,65
23 | lpa,0.002990240999679372,157,65
24 | rand,1.682881677000296,42184,867
25 | rand_lifo,0.002002788000027067,485,65
26 | spfa,0.004598628999701759,1165,65
27 | theta,0.001139300000431831,157,65,Turn points: 28
28 |
--------------------------------------------------------------------------------
/resources/Best-First-Search.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/resources/Best-First-Search.gif
--------------------------------------------------------------------------------
/resources/OtherAlgorithms.md:
--------------------------------------------------------------------------------
1 | # Algorithms that could be used for path finding
2 |
3 | ```
4 | Algorithms to look at:
5 |
6 | - Hill climbing
7 | - Genetic algorithms
8 | - Ant colony optimization
9 | - Particle swarm optimization
10 | - Simulated annealing
11 |
12 | - D*
13 | - D* Lite
14 | - Landmark Label Setting
15 | - Hierarchical A*
16 | - Contraction Hierarchies
17 | - Lazy Theta*
18 | - Anytime Repairing A*
19 | - Weighted A*
20 | - Bounded A*
21 |
22 | - Optimal Reciprocal Collision Avoidance (ORCA)
23 | - Potential Field Path Planning (PFPP)
24 | - Rapidly-Exploring Random Tree (RRT)
25 | - Kinodynamic RRT (Kino-RRT)
26 | - Visibility Graph
27 | - Probabilistic Roadmap (PRM)
28 | - Elastic Band (EB)
29 | - A* with Elastic Band (AEB)
30 |
31 | - Cooperative Path Planning (CPP)
32 | - Optimal Cooperative Path Planning (OCPP)
33 | - Path Planning in Complex Environments (PPCP)
34 | - Path Planning for Multiple Robots (PPMR)
35 | - Multi-Agent Path Finding (MAPF)
36 | - Conflict-Based Search (CBS)
37 | - Incremental Search (ICS)
38 | - Conflict-Based Multi-Agent Path Finding (CB-MAPF)
39 |
40 | - Multi-Objective Evolutionary Algorithms (MOEA)
41 | - Constraint-Based Path Planning (CBPP)
42 | - Anytime Constraint-Based Path Planning (ACBPP)
43 | - Constraint-Based Multi-Agent Path Finding (CB-MAPF)
44 | - Sampling-Based Motion Planning (SMP)
45 | - Probabilistic Roadmap with Sampling (PRM-S)
46 | - Kinodynamic Sampling-Based Motion Planning (Kino-SMP)
47 | - Kinodynamic Probabilistic Roadmap with Sampling (Kino-PRM-S)
48 | ```
49 |
--------------------------------------------------------------------------------
/resources/Thoughts.md:
--------------------------------------------------------------------------------
1 | ## Reason for writing
2 |
3 | While programming this project, I have learned a lot about graph search and path finding algorithms, and I wanted to share some of my opinions and thoughts about them, in relation to this project.
4 |
5 | ## Process
6 |
7 | A lot of what went in to this project was researching what algorithms there are and how to translate them into something that can be used in a grid-based graph.
8 |
9 | I used resources like Wikipedia, and ChatGPT for a lot of the algorithms, and explainations. Most algorithm pages on Wikipedia have a pseudocode section, which is helpful when the mathematical notation doesn't quite make sense. ChatGPT, while currently looked at with suspicion by some, is a great resource for learning and as a tool for assisting in development. The only gripe I have to say about ChatGPT is that you can't be 100% certain what it spits out is correct without validating it against another source, like Wikipedia, or primary sources like the papers where the algorithms were published.
10 |
11 | Another resource that I tried to use is YouTube. However, most of the algorithms on YouTube (pathfinding algorithms) are either A*, BFS, DFS, or Dijkstra's algorithm. I could scarely find the other algorithms I implemented here, and ones I'm going to implement, on YouTube. There were some videos that went over the concept of the algorithm, like Bellman-Ford, and Floyd-Warshall, but they didn't provide either usable psuedocode or actual code for the algorithm. This is a personal opinion of mine, but if you want to do a deep dive on pathfinding algorithms, I would suggest using other resources than YouTube. However, if you're just looking to get started or just a cursory overview of pathfinding algorithms, then YouTube is a good resource.
12 |
13 | ## Algorithm Discussion
14 |
15 | While implementing these algorithms, I relized there is an issue with the project environment. By this I mean, not every algorithm can display its own strenths. The biggest losers of this simulation are the algorithms that are ment to be run once and the results used multiple times. For example, the Floyd-Warshall algorithm. If you look at the example test result in the `main/testing/results` folder, you will see that there are > one billion node checks. In the environment that I have set up, all data from the previous run is deleted, so the distance matrix that is created isn't reused. So, every run, on a default generated maze, the Floyd-Warshall algorithm will have to run > one billion checks. However, if data were saved from a previous run, the Floyd-Warshall algorithm would only have to do those checks once and then the shortest path could be found in linear time (O(|V|)). On the other end of the spectrum, the algorithms that are most suited to this environment are ones that are meant to be run without any preprocessing, like A*, BFS, and Dijkstra.
16 |
17 | If I were to reimplement this, or modify this program, I would add something like a caching function to store precomputed information that would speed up algorithm runtime.
18 |
19 | ---
20 |
21 | This program is mainly designed as a learning tool, rather than anything practical. However, most of the algorithms have practical applications.
22 |
23 | I think the easiest demonstration would be a GPS application. Many of the concepts in the algorithms implemented here are used for GPS applications. If we look at Theta*, we can see it gives turn points. This is congruent to what many GPS applications do where it tells you to turn at X street.
24 |
25 | --TBC
--------------------------------------------------------------------------------
/resources/astar_maze.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/resources/astar_maze.gif
--------------------------------------------------------------------------------
/resources/b_star.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/resources/b_star.pdf
--------------------------------------------------------------------------------
/resources/d-star.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/resources/d-star.pdf
--------------------------------------------------------------------------------
/resources/fast_marching.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/resources/fast_marching.pdf
--------------------------------------------------------------------------------
/resources/gabows.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/resources/gabows.pdf
--------------------------------------------------------------------------------
/resources/interesting_ideas.md:
--------------------------------------------------------------------------------
1 | One example of an interesting and unorthodox pathfinding algorithm is the "ants on a graph" algorithm, which is based on the behavior of real ants as they search for food. In this algorithm, virtual ants are placed on a graph and are allowed to move randomly, with the probability of moving to a particular node being based on the attractiveness of that node. Over time, the ants will tend to move towards nodes that are closer to the goal, resulting in the formation of a "pheromone" trail that other ants can follow.
2 |
3 | Another interesting and unorthodox pathfinding algorithm is the "firefly algorithm", which is based on the behavior of fireflies as they search for mates. In this algorithm, virtual fireflies are placed on a graph and are allowed to move randomly, with the probability of moving to a particular node being based on the brightness of that node. Over time, the fireflies will tend to move towards nodes that are brighter, resulting in the formation of a "light" trail that other fireflies can follow.
4 |
5 | In the case of the Japanese train routes, the researchers used a type of slime mold called Physarum polycephalum, which is known for its ability to find the shortest path between multiple food sources. By creating a model of the train network and providing the slime mold with "food" at the locations of different cities, the researchers were able to observe the growth of the slime mold and use it to determine the optimal routes between cities.
6 |
7 | This type of nature-inspired algorithm, known as "biological computing", has the potential to provide new solutions to complex problems that may be difficult to solve using traditional methods. While it is not always practical or feasible to use biological systems directly, studying and understanding the behavior of these systems can provide valuable insights and inspiration for developing new algorithms and approaches to problem-solving.
8 |
9 | *****
10 |
11 | Particle swarm optimization (PSO) is a heuristic optimization algorithm that is based on the behavior of bird flocks or fish schools. It is often used for solving problems that involve finding the optimal solution in a complex system.
12 |
13 | In the context of pathfinding, PSO can be used to find the shortest path between two nodes in a graph or network. It does this by simulating the movement of a group of particles, or agents, in the graph. Each particle represents a potential solution to the problem, and the particles move through the graph according to a set of rules that are designed to guide them towards the optimal solution.
14 |
15 | Here's an overview of how PSO might be used for pathfinding:
16 |
17 | Initialize the position and velocity of each particle in the graph.
18 | For each particle, calculate its fitness, or the quality of the solution it represents.
19 | Update the position and velocity of each particle based on its fitness and the positions and velocities of the other particles.
20 | Repeat steps 2 and 3 until the particles converge on the optimal solution.
21 |
22 | Once the particles have converged on the optimal solution, the shortest path between the two nodes can be reconstructed from the positions of the particles. PSO is a heuristic algorithm, so it doesn't guarantee that the solution it finds will be the global optimum, but it is often effective at finding good solutions in complex systems.
23 |
24 | *****
25 |
26 | The basic idea behind hill climbing is to start with an initial solution to the problem, and then iteratively improve the solution by making small, local changes. In the case of pathfinding, the initial solution might be a random path between the two nodes, and the local changes might involve modifying the path by adding or removing edges.
27 |
28 | Here's an overview of how hill climbing might be applied to pathfinding:
29 |
30 | Start with an initial path between the two nodes.
31 | Iteratively modify the path by adding or removing edges.
32 | Calculate the fitness, or the quality, of the modified path.
33 | If the modified path is better than the current path, update the current path with the modified path.
34 | Repeat steps 2-4 until the path cannot be improved any further.
35 |
36 | Once the path cannot be improved any further, the algorithm has converged on the local optimum, which is the shortest path between the two nodes according to the local search rules. Like other heuristic algorithms, hill climbing doesn't guarantee that the solution it finds will be the global optimum, but it is often effective at finding good solutions in complex systems.
37 |
38 | *****
--------------------------------------------------------------------------------
/resources/links.md:
--------------------------------------------------------------------------------
1 | Theta*: https://en.wikipedia.org/wiki/Theta*#Pseudocode
2 | Best First Search: https://en.wikipedia.org/wiki/Best-first_search
3 | Graph Algorithms: https://en.wikipedia.org/wiki/Category:Graph_algorithms
4 | SSS*: https://en.wikipedia.org/wiki/SSS*
5 | Shortest Path Problem: https://en.wikipedia.org/wiki/Shortest_path_problem
6 | AAPP: https://en.wikipedia.org/wiki/Any-angle_path_planning
7 | Johnson's: https://en.wikipedia.org/wiki/Johnson%27s_algorithm
8 | Fast Marching: https://en.wikipedia.org/wiki/Fast_marching_method
9 | https://math.berkeley.edu/~sethian/
10 | Fast Sweeping: https://en.wikipedia.org/wiki/Fast_sweeping_method
--------------------------------------------------------------------------------
/resources/theta_star.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/resources/theta_star.pdf
--------------------------------------------------------------------------------
/upload.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Check for missing commit message
4 | if [ -z "$1" ]; then
5 | echo "Error: Please provide a commit message."
6 | exit 1
7 | fi
8 |
9 | # Enable extended globbing.
10 | shopt -s extglob
11 |
12 | dir="main/testing/results"
13 |
14 | # Check if directory exists
15 | if [ -d "$dir" ]; then
16 | # Check if there are more than one file in the directory
17 | if [ "$(ls -1 "$dir" | wc -l)" -gt 1 ]; then
18 | # Navigate to the subfolder containing the CSV files.
19 | cd "$dir"
20 | rm !(Generated_Maze-Example.csv)
21 |
22 | echo "Deleting CSV files from ./main/testing/results"
23 | else
24 | echo "Directory contains only one file. Nothing deleted."
25 | fi
26 | else
27 | echo "Directory does not exist."
28 | fi
29 |
30 | # Upload to repository
31 | git add -A
32 | git commit -m "$1"
33 | git push
34 |
--------------------------------------------------------------------------------
/website/TODO:
--------------------------------------------------------------------------------
1 | - Bidirectional A*
2 | - Bidirectional search
3 | - Fringe search
4 | - Iterative Deepening (IDA*)
5 | - Iterative Deepening DFS (IDDFS)
6 | - Jump Point Search (JPS)
7 | - Lexicographic BFS (LBFS)
8 | - Lifelong Planning A* (LPA*)
9 | - Random Walk with LIFO Queue
10 | - Shortest Path Faster Algorithm (SPFA)
11 | - Theta*
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pathfinding",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "https://gurknathe.github.io/Pathfinding-Algorithms",
6 | "dependencies": {
7 | "@emotion/react": "^11.11.0",
8 | "@emotion/styled": "^11.11.0",
9 | "@mui/icons-material": "^5.11.16",
10 | "@mui/material": "^5.13.3",
11 | "@testing-library/jest-dom": "^5.16.5",
12 | "@testing-library/react": "^13.4.0",
13 | "@testing-library/user-event": "^13.5.0",
14 | "@types/jest": "^27.5.2",
15 | "@types/node": "^16.18.34",
16 | "@types/react": "^18.2.8",
17 | "@types/react-dom": "^18.2.4",
18 | "gh-pages": "^5.0.0",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "react-router-dom": "^6.11.2",
22 | "three": "^0.151.3",
23 | "typescript": "^4.9.5",
24 | "web-vitals": "^2.1.4"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "predeploy": "npm run build",
30 | "deploy": "gh-pages -d build",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | },
52 | "devDependencies": {
53 | "react-scripts": "5.0.1"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/website/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/website/public/favicon.ico
--------------------------------------------------------------------------------
/website/public/favicon.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/website/public/favicon.xcf
--------------------------------------------------------------------------------
/website/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Pathfinding Algorithms
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/website/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/website/public/logo192.png
--------------------------------------------------------------------------------
/website/public/logo192.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GurkNathe/Pathfinding-Algorithms/917dbbbc9fb075d1d9020403ecb6ca2426e0bb83/website/public/logo192.xcf
--------------------------------------------------------------------------------
/website/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Ethan Krug",
3 | "name": "Ethan Krug Personal Website",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | }
15 | ],
16 | "start_url": ".",
17 | "display": "standalone",
18 | "theme_color": "#000000",
19 | "background_color": "#ffffff"
20 | }
--------------------------------------------------------------------------------
/website/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/website/src/App.tsx:
--------------------------------------------------------------------------------
1 | import Panel from "./components/Panel"
2 |
3 | export default function App() {
4 | return(
5 |