added TurtleDFS.py

This commit is contained in:
2026-03-30 08:39:15 -04:00
parent 73c6a283c6
commit be479d2c63
7 changed files with 266 additions and 8 deletions

256
Project 5/TurtleDFS.py Normal file
View File

@@ -0,0 +1,256 @@
######################################
# DCS 229 -- Turtle DFS
# Non-recursive depth first search for solving turtle mazes
# Date: 03/29/2026
# Name: Benjamin Adovasio
# Resources Used: Project 5 handout and TurtleMaze.py
##########################################
from __future__ import annotations
from pathlib import Path
import sys
from types import SimpleNamespace
from Stack import Stack
try:
import turtle
TURTLE_AVAILABLE = True
except ModuleNotFoundError:
TURTLE_AVAILABLE = False
class _StubTerminator(Exception):
"""Fallback replacement for turtle.Terminator."""
class _StubScreen:
def setworldcoordinates(self, *args) -> None:
pass
def tracer(self, *args) -> None:
pass
def update(self) -> None:
pass
def mainloop(self) -> None:
pass
class _StubTurtle:
def __init__(self):
self.screen = _StubScreen()
def shape(self, *args) -> None:
pass
def speed(self, *args) -> None:
pass
def up(self) -> None:
pass
def goto(self, *args) -> None:
pass
def color(self, *args) -> None:
pass
def fillcolor(self, *args) -> None:
pass
def setheading(self, *args) -> None:
pass
def down(self) -> None:
pass
def begin_fill(self) -> None:
pass
def forward(self, *args) -> None:
pass
def right(self, *args) -> None:
pass
def end_fill(self) -> None:
pass
def dot(self, *args) -> None:
pass
def towards(self, *args) -> int:
return 0
turtle = SimpleNamespace(
Turtle=_StubTurtle,
Screen=_StubScreen,
Terminator=_StubTerminator,
)
sys.modules["turtle"] = turtle
from TurtleMaze import Cell, Contents, TurtleMaze
class TestMaze:
"""Lightweight maze implementation used for non-GUI testing."""
def __init__(self, maze_file_name: str | Path):
self.maze_grid: list[list[Cell]] = []
self._path_log: list[tuple[int, int, Contents | None]] = []
maze_path = Path(maze_file_name)
for row_index, line in enumerate(maze_path.read_text().splitlines()):
row_list: list[Cell] = []
for col_index, char in enumerate(line):
cell = Cell(row_index, col_index, Contents(char))
row_list.append(cell)
if cell._contents == Contents.START:
self._start = cell
if cell._contents == Contents.GOAL:
self._goal = cell
self.maze_grid.append(row_list)
def updatePosition(self, cell: Cell, val: Contents | None = None) -> None:
"""Track visited positions during tests without opening a turtle window."""
if val is not None:
cell._contents = val
self._path_log.append((cell.x, cell.y, val))
def getStart(self) -> Cell:
return self._start
def getGoal(self) -> Cell:
return self._goal
def __getitem__(self, idx: int) -> list[Cell]:
return self.maze_grid[idx]
def get_neighbors(maze: TurtleMaze, cell: Cell) -> list[Cell]:
"""Return each non-obstacle neighbor in north, south, east, west order."""
neighbors: list[Cell] = []
candidate_positions = (
(cell.y - 1, cell.x), # north
(cell.y + 1, cell.x), # south
(cell.y, cell.x + 1), # east
(cell.y, cell.x - 1), # west
)
for row, col in candidate_positions:
if 0 <= row < len(maze.maze_grid) and 0 <= col < len(maze[row]):
neighbor = maze[row][col]
if not neighbor.isBlocked():
neighbors.append(neighbor)
return neighbors
def _mark_solution_path(maze: TurtleMaze, goal: Cell) -> None:
"""Walk the successful route without teleporting and color it green."""
goal_to_start: list[Cell] = []
current: Cell | None = goal
while current is not None:
goal_to_start.append(current)
current = current.getParent()
for cell in goal_to_start:
maze.updatePosition(cell, Contents.PART_OF_PATH)
for cell in reversed(goal_to_start[:-1]):
maze.updatePosition(cell, Contents.PART_OF_PATH)
def dfs(maze: TurtleMaze) -> bool:
"""Solve a maze using a non-recursive depth first search."""
start = maze.getStart()
stack: Stack[Cell] = Stack()
visited: set[tuple[int, int]] = set()
stack.push(start)
visited.add((start.x, start.y))
maze.updatePosition(start, Contents.TRIED)
while not stack.is_empty():
current = stack.peek()
if current.isGoal():
_mark_solution_path(maze, current)
return True
next_cell = None
for neighbor in get_neighbors(maze, current):
coordinates = (neighbor.x, neighbor.y)
if coordinates not in visited:
neighbor.setParent(current)
next_cell = neighbor
break
if next_cell is not None:
visited.add((next_cell.x, next_cell.y))
stack.push(next_cell)
if next_cell.isGoal():
maze.updatePosition(next_cell)
else:
maze.updatePosition(next_cell, Contents.TRIED)
continue
dead_end = stack.pop()
if dead_end != start and not dead_end.isGoal():
maze.updatePosition(dead_end, Contents.DEAD_END)
if not stack.is_empty():
maze.updatePosition(stack.peek())
return False
def _project_path(file_name: str) -> Path:
"""Return a path inside the Project 5 directory."""
return Path(__file__).resolve().with_name(file_name)
def _test_get_neighbors() -> None:
maze = TestMaze(_project_path("maze_1.txt"))
start = maze.getStart()
neighbor_coordinates = [(cell.x, cell.y) for cell in get_neighbors(maze, start)]
assert neighbor_coordinates == [(5, 1), (3, 1)]
def _test_dfs() -> None:
for maze_name in ("maze_1.txt", "maze_2.txt", "maze_3.txt"):
maze = TestMaze(_project_path(maze_name))
solved = dfs(maze)
assert solved is True
assert maze.getStart()._contents == Contents.PART_OF_PATH
assert maze.getGoal()._contents == Contents.PART_OF_PATH
def main() -> None:
_test_get_neighbors()
_test_dfs()
print("All TurtleDFS tests passed.")
if not TURTLE_AVAILABLE:
print("Skipping turtle visualization: turtle graphics are unavailable in this Python environment.")
return
maze_path = _project_path("maze_2.txt")
try:
maze = TurtleMaze(str(maze_path))
maze.drawMaze()
solved = dfs(maze)
print(f"Visual DFS demo on {maze_path.name}: {solved}")
maze.t.screen.mainloop()
except Exception as err:
print(f"Skipping turtle visualization: {err}")
if __name__ == "__main__":
main()