###################################### # 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()