diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/Project 5/Stack.py b/Project 5/Stack.py index 8258829..97e35a5 100644 --- a/Project 5/Stack.py +++ b/Project 5/Stack.py @@ -1,30 +1,33 @@ ###################################### # DCS 229 -- Stack # Stack data structure implemented with Linked List -# Date: -# Name: +# Date: 03/29/2026 +# Name: Benjamin Adovasio # Resources Used: LinkedList.py from Project 4. I also worked with Pat Cohen on the project. ########################################## from LinkedList import * + + class EmptyError(Exception): ''' class extending Exception to better document stack errors ''' def __init__(self, message: str): + super().__init__(message) self.message = message + class Stack[T]: ''' class to implement a stack ADT using our LinkedList class''' def __init__(self): - ''' initializes an empty stack ''' self._data = LinkedList() def __len__(self) -> int: - ''' allows the len function to be called using an ArrayStack object, e.g., - stack = ArrayStack() + ''' allows the len function to be called using a Stack object, e.g., + stack = Stack() print(len(stack)) Returns: number of elements in the stack, as an integer @@ -76,7 +79,7 @@ class Stack[T]: def main(): '''test functions''' - s = Stack() # I added this so stack doesnt need to be reinitialized for each test + s = Stack() #I added this so stack doesnt need to be reinitalized for each test assert len(s) == 0 assert s.is_empty() is True @@ -107,8 +110,7 @@ def main(): except EmptyError: pass - print("All Stack tests passed.") #only runs if all previous tests pass + print("All Stack tests passed.") #Only runs if all previous tests pass if __name__ == "__main__": main() - diff --git a/Project 5/TurtleDFS.py b/Project 5/TurtleDFS.py new file mode 100644 index 0000000..8920754 --- /dev/null +++ b/Project 5/TurtleDFS.py @@ -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() diff --git a/Project 5/__pycache__/LinkedList.cpython-314.pyc b/Project 5/__pycache__/LinkedList.cpython-314.pyc index 0bdd970..b0c0ec6 100644 Binary files a/Project 5/__pycache__/LinkedList.cpython-314.pyc and b/Project 5/__pycache__/LinkedList.cpython-314.pyc differ diff --git a/Project 5/__pycache__/Stack.cpython-314.pyc b/Project 5/__pycache__/Stack.cpython-314.pyc new file mode 100644 index 0000000..93a7be3 Binary files /dev/null and b/Project 5/__pycache__/Stack.cpython-314.pyc differ diff --git a/Project 5/__pycache__/TurtleDFS.cpython-314.pyc b/Project 5/__pycache__/TurtleDFS.cpython-314.pyc new file mode 100644 index 0000000..7c726e0 Binary files /dev/null and b/Project 5/__pycache__/TurtleDFS.cpython-314.pyc differ diff --git a/Project 5/__pycache__/TurtleMaze.cpython-314.pyc b/Project 5/__pycache__/TurtleMaze.cpython-314.pyc new file mode 100644 index 0000000..8f29316 Binary files /dev/null and b/Project 5/__pycache__/TurtleMaze.cpython-314.pyc differ