Skip to content

Commit bcc9cba

Browse files
committed
Add RecursiveBacktracker algorithm
1 parent 41031dc commit bcc9cba

File tree

6 files changed

+73
-25
lines changed

6 files changed

+73
-25
lines changed

README.md

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A small remark: Code is not a 1:1 copy of the book's. For example I built render
1111
- `AldousBroder`
1212
- `BinaryTree`
1313
- `HuntAndKill`
14+
- `RecursiveBacktracker`
1415
- `Sidewinder`
1516
- `Wilson`
1617

@@ -96,7 +97,6 @@ Stats demo runs all available algorithms a certain number of times and gathers s
9697
Sample output:
9798
```
9899
PYTHONPATH=. python3 demos/stats_demo.py 25 25 --pathfinding
99-
100100
Rows: 25
101101
columns: 25
102102
Total cells: 625
@@ -105,29 +105,34 @@ Pathfinding: True
105105
> running AldousBroder
106106
> running BinaryTree
107107
> running HuntAndKill
108+
> running RecursiveBacktracker
108109
> running Sidewinder
109110
> running Wilson
110111
111-
Average dead-ends (desc):
112-
AldousBroder: 181/625 (29.01%)
113-
Wilson: 181/625 (28.97%)
114-
Sidewinder: 170/625 (27.12%)
115-
BinaryTree: 156/625 (24.93%)
116-
HuntAndKill: 062/625 (9.86%)
112+
Average dead-ends (deadends/total-cells, sorted by % desc):
113+
AldousBroder: 182/625 (29.12%)
114+
Wilson: 181/625 (28.93%)
115+
Sidewinder: 171/625 (27.29%)
116+
BinaryTree: 156/625 (24.97%)
117+
RecursiveBacktracker: 065/625 (10.47%)
118+
HuntAndKill: 061/625 (9.73%)
117119
118120
Generation speed benchmark (seconds, sorted by average desc):
119-
Wilson: avg: 0.65727073 min: 0.22471986 max: 2.18292433
120-
HuntAndKill: avg: 0.08903943 min: 0.07111554 max: 0.12254786
121-
AldousBroder: avg: 0.03225283 min: 0.01685291 max: 0.08700121
122-
Sidewinder: avg: 0.00232661 min: 0.00208164 max: 0.00273097
123-
BinaryTree: avg: 0.00217227 min: 0.00209896 max: 0.00251945
121+
Wilson: avg: 0.641611 min: 0.235594 max: 2.173624
122+
HuntAndKill: avg: 0.078919 min: 0.059095 max: 0.101278
123+
AldousBroder: avg: 0.038898 min: 0.015946 max: 0.180922
124+
RecursiveBacktracker: avg: 0.005492 min: 0.005383 max: 0.006105
125+
BinaryTree: avg: 0.002130 min: 0.002074 max: 0.002359
126+
Sidewinder: avg: 0.002105 min: 0.002039 max: 0.002320
124127
125128
Pathfinding speed benchmark (seconds, sorted by average desc):
126-
HuntAndKill: avg: 0.01336081 min: 0.01190879 max: 0.02201122
127-
Sidewinder: avg: 0.01306345 min: 0.01151913 max: 0.02009250
128-
Wilson: avg: 0.01259966 min: 0.01147151 max: 0.01936247
129-
AldousBroder: avg: 0.01224201 min: 0.01164555 max: 0.01713539
130-
BinaryTree: avg: 0.01185717 min: 0.01154452 max: 0.01265934
129+
AldousBroder: avg: 0.014295 min: 0.011494 max: 0.035487
130+
RecursiveBacktracker: avg: 0.012775 min: 0.012238 max: 0.014378
131+
HuntAndKill: avg: 0.012100 min: 0.011589 max: 0.013740
132+
Wilson: avg: 0.011712 min: 0.011262 max: 0.013362
133+
Sidewinder: avg: 0.011641 min: 0.011314 max: 0.013308
134+
BinaryTree: avg: 0.011561 min: 0.011267 max: 0.013016
135+
131136
```
132137

133138
## Testing
@@ -137,3 +142,9 @@ Note: Runs also some linter tests, to conform with both `mypy` and `flake8`.
137142
```
138143
pytest
139144
```
145+
146+
## Roadmap & TODOs
147+
148+
- Of course finish the book and implement all main code and algorithms
149+
- Dockerize the project -> Will allow to upgrade to Python 3.6 and perform additional mypy cleanups & improvements
150+
- Implement more pathfinders -> (e.g. recursive backtracking as a maze solve algorithm)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from random import choice
2+
3+
from typing import Optional
4+
5+
from base.grid import Grid
6+
from base.cell import Cell
7+
8+
"""
9+
Recursive Backtracker algorithm picks a random starting cell and randomly walks. It cannot walk on an already visited
10+
cell, and if finds at a dead-end (no more unvisited cells around current one), goes back in its steps until has a cell
11+
at least one visited neighbor; then starts walking again.
12+
"""
13+
14+
15+
class RecursiveBacktracker:
16+
17+
@staticmethod
18+
def on(grid: Grid, starting_cell: Optional[Cell] = None) -> Grid:
19+
if starting_cell is None:
20+
starting_cell = grid.random_cell()
21+
22+
# We'll use the list as a stack to do very easily any backtracking
23+
walked_path = []
24+
walked_path.append(starting_cell)
25+
26+
while len(walked_path) > 0:
27+
current_cell = walked_path[-1]
28+
unvisited_neighbors = [neighbor for neighbor in current_cell.neighbors if len(neighbor.links) == 0]
29+
30+
if len(unvisited_neighbors) == 0:
31+
walked_path.pop()
32+
else:
33+
neighbor = choice(unvisited_neighbors)
34+
current_cell.link(neighbor)
35+
walked_path.append(neighbor)
36+
37+
return grid

algorithms/wilson.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def on(grid: Grid) -> Grid:
3131
cell = choice(cell.neighbors)
3232
try:
3333
position = path.index(cell)
34-
# already walked, perform loop-erase
34+
# already walked, perform loop-erase. e.g. A -> B -> C -> D -> B becomes A -> B
3535
path = path[:position + 1]
3636
except ValueError:
3737
path.append(cell)

demos/demo_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
from algorithms.aldous_broder import AldousBroder
77
from algorithms.wilson import Wilson
88
from algorithms.hunt_and_kill import HuntAndKill
9+
from algorithms.recursive_backtracker import RecursiveBacktracker
910

1011
import renderers.ascii_renderer as ASCIIRenderer
1112
import renderers.unicode_renderer as UNICODERenderer
1213
import renderers.png_renderer as PNGRenderer
1314

14-
ALGORITHMS = [AldousBroder, BinaryTree, HuntAndKill, Sidewinder, Wilson]
15+
ALGORITHMS = [AldousBroder, BinaryTree, HuntAndKill, RecursiveBacktracker, Sidewinder, Wilson]
1516
ALL_RENDERERS = [UNICODERenderer, ASCIIRenderer, PNGRenderer]
1617

1718

demos/stats_demo.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,20 @@ def get_tries() -> int:
8888
}
8989

9090
sorted_averages = sorted(algorithm_averages.items(), key=lambda x: -x[1])
91-
print("\nAverage dead-ends (desc):")
91+
print("\nAverage dead-ends (deadends/total-cells, sorted by % desc):")
9292
for algorithm, average in sorted_averages:
9393
percentage = average * 100.0 / size
94-
print(" {:>16}: {:03.0f}/{:03d} ({:.2f}%)".format(algorithm.__name__, average, size, percentage))
94+
print(" {:>22}: {:03.0f}/{:03d} ({:.2f}%)".format(algorithm.__name__, average, size, percentage))
9595

9696
sorted_benchmarks = sorted(algorithm_benchmarks.items(), key=lambda x: -x[1]["average"])
9797
print("\nGeneration speed benchmark (seconds, sorted by average desc):")
9898
for algorithm, benchmark in sorted_benchmarks:
99-
print(" {:>16}: avg: {:03.8f} min: {:03.8f} max: {:03.8f}".format(algorithm.__name__, benchmark["average"],
99+
print(" {:>22}: avg: {:03.6f} min: {:03.6f} max: {:03.6f}".format(algorithm.__name__, benchmark["average"],
100100
benchmark["min"], benchmark["max"]))
101101

102102
if pathfinding:
103103
sorted_pathfinding_benchmarks = sorted(pathfinding_benchmarks.items(), key=lambda x: -x[1]["average"])
104104
print("\nPathfinding speed benchmark (seconds, sorted by average desc):")
105105
for algorithm, benchmark in sorted_pathfinding_benchmarks:
106-
print(" {:>16}: avg: {:03.8f} min: {:03.8f} max: {:03.8f}".format(algorithm.__name__, benchmark["average"],
106+
print(" {:>22}: avg: {:03.6f} min: {:03.6f} max: {:03.6f}".format(algorithm.__name__, benchmark["average"],
107107
benchmark["min"], benchmark["max"]))

renderers/unicode_renderer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
# N 1
88
# W E 2 4
99
# S 8
10-
1110
JUNCTIONS = [" ", " ", " ", "\u251b", " ", "\u2517", "\u2501", "\u253b", " ", "\u2503", "\u2513", "\u252b", "\u250f",
1211
"\u2523", "\u2533", "\u254b"]
1312

@@ -77,7 +76,7 @@ def get_topmost_junction(cell: Cell) -> str:
7776

7877

7978
def get_south_east_junction(cell: Cell) -> str:
80-
# Taking advantage that we always go forward east and south, just need to calculate available posibilities
79+
# Taking advantage that we always go forward east and south, just need to calculate available posibilities
8180
#
8281
# [ X ] [X-east]
8382
#

0 commit comments

Comments
 (0)