250 lines
7.7 KiB
Python
250 lines
7.7 KiB
Python
######################################
|
|
# DCS 229 -- TurtleMaze
|
|
# Utility file with the TurtleMaze class.
|
|
# Date: March 13, 2026
|
|
# Name: Prof. Andy Ricci
|
|
# Resources Used: https://runestone.academy/ns/books/published/pythonds/Recursion/ExploringaMaze.html. I also worked with Pat Cohen on the project.
|
|
##########################################
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
import turtle
|
|
|
|
|
|
|
|
################################################################################
|
|
class Contents(str, Enum):
|
|
''' create an enumeration to define what the visual contents of a Cell are;
|
|
using str as a "mixin" (multiple inheritance) forces all the entries to
|
|
be strings; using an Enum means no cell entry can be anything other
|
|
than the options here
|
|
'''
|
|
|
|
EMPTY = ' '
|
|
START = 'S'
|
|
GOAL = 'G'
|
|
PART_OF_PATH = 'O'
|
|
TRIED = '.'
|
|
OBSTACLE = '+'
|
|
DEAD_END = '-'
|
|
|
|
def __str__(self) -> str:
|
|
return self.value
|
|
|
|
|
|
################################################################################
|
|
class Cell:
|
|
''' class that allows us to use Cell as a data type -- an ordered triple
|
|
of row, column, & cell contents
|
|
(see Contents class enumeration above)
|
|
'''
|
|
|
|
def __init__(self, row: int, col: int, contents: Contents):
|
|
self._contents: Contents = contents
|
|
self._parent: Cell = None # parent of this Cell during exploration
|
|
self.x = col
|
|
self.y = row
|
|
|
|
def getParent(self) -> Cell:
|
|
''' method to return the parent of this Cell object as determined during
|
|
maze exploration
|
|
Returns:
|
|
a Cell object corresponding to the cell that considered this cell
|
|
during the exploration process
|
|
'''
|
|
return self._parent
|
|
|
|
def setParent(self, parent: Cell) -> None:
|
|
''' setter method to update this Cell's parent
|
|
Parameters:
|
|
parent: a different Cell object
|
|
Raises:
|
|
ValueError if self == parent
|
|
'''
|
|
if self == parent: raise ValueError(f"a Cell cannot be its own parent")
|
|
self._parent = parent
|
|
|
|
def markOnPath(self) -> None:
|
|
''' method to identify this cell as being on the path from source
|
|
to goal
|
|
'''
|
|
self._contents = Contents.PART_OF_PATH
|
|
|
|
def isBlocked(self) -> bool:
|
|
''' Boolean method to indicate whether this cell contains a block
|
|
Returns:
|
|
True if the cell is blocked (cannot be explored), False o/w
|
|
'''
|
|
return self._contents == Contents.OBSTACLE
|
|
|
|
def isGoal(self) -> bool:
|
|
''' Boolean method to indicate whether this cell is the goal
|
|
Returns:
|
|
True if the cell is the maze goal, False o/w
|
|
'''
|
|
return self._contents == Contents.GOAL
|
|
|
|
def __str__(self) -> str:
|
|
''' creates and returns a string representation of this cell
|
|
Returns:
|
|
a string identifying the cell's row, col, and cell contents
|
|
'''
|
|
string = f"({self.x}, {self.y}, {self._contents}) "
|
|
if self._parent is not None:
|
|
string += f"Parent is: ({self._parent.x}, {self._parent.y})"
|
|
return string
|
|
|
|
def __repr__(self) -> str:
|
|
''' overriding __repr__ so that printing, e.g., a list of Cell objects
|
|
(which will call __repr__ for each) will call __str__ for each, printing
|
|
nicely
|
|
Returns:
|
|
a string identifying the cell's row, col, and cell contents
|
|
'''
|
|
return self.__str__()
|
|
|
|
def __eq__(self, other: Cell) -> bool:
|
|
''' indicates whether a given other Cell is equal to this Cell
|
|
Returns:
|
|
True if this Cell and the other Cell are the same, False o/w
|
|
'''
|
|
return self.x == other.x and \
|
|
self.y == other.y
|
|
|
|
|
|
################################################################################
|
|
class TurtleMaze:
|
|
def __init__(self, mazeFileName):
|
|
rowsInMaze = 0
|
|
columnsInMaze = 0
|
|
self.maze_grid = []
|
|
mazeFile = open(mazeFileName, 'r')
|
|
rowsInMaze = 0
|
|
for line in mazeFile:
|
|
rowList = []
|
|
col = 0
|
|
for ch in line[:-1]: #
|
|
# make new cell: (row, col, contents)
|
|
cell = Cell(rowsInMaze, col, ch)
|
|
rowList.append(cell)
|
|
if ch == Contents.START:
|
|
self.startRow = rowsInMaze
|
|
self.startCol = col
|
|
self._start = cell
|
|
if ch == Contents.GOAL:
|
|
self._goal = cell
|
|
|
|
col = col + 1
|
|
rowsInMaze = rowsInMaze + 1
|
|
self.maze_grid.append(rowList)
|
|
columnsInMaze = len(rowList)
|
|
|
|
self._num_rows = rowsInMaze
|
|
self._num_cols = columnsInMaze
|
|
self.xTranslate = -columnsInMaze / 2
|
|
self.yTranslate = rowsInMaze / 2
|
|
self.t = turtle.Turtle()
|
|
self.t.shape('turtle')
|
|
self.wn = turtle.Screen()
|
|
self.wn.setworldcoordinates(-(columnsInMaze - 1) / 2 - .5, -(rowsInMaze - 1) / 2 - .5,
|
|
(columnsInMaze - 1) / 2 + .5, (rowsInMaze - 1) / 2 + .5)
|
|
|
|
|
|
def drawMaze(self):
|
|
self.t.speed(10)
|
|
self.wn.tracer(0)
|
|
for y in range(self._num_rows):
|
|
for x in range(self._num_cols):
|
|
cell = self.maze_grid[y][x]
|
|
if cell._contents == Contents.OBSTACLE:
|
|
self.drawCenteredBox(x + self.xTranslate, -y + self.yTranslate, 'orange')
|
|
self.updatePosition(self.getStart())
|
|
self.t.color('black')
|
|
self.t.fillcolor('blue')
|
|
self.wn.update()
|
|
self.wn.tracer(1)
|
|
|
|
def drawCenteredBox(self, x, y, color):
|
|
self.t.up()
|
|
self.t.goto(x - .5, y - .5)
|
|
self.t.color(color)
|
|
self.t.fillcolor(color)
|
|
self.t.setheading(90)
|
|
self.t.down()
|
|
self.t.begin_fill()
|
|
for i in range(4):
|
|
self.t.forward(1)
|
|
self.t.right(90)
|
|
self.t.end_fill()
|
|
|
|
def moveTurtle(self, x, y):
|
|
self.t.up()
|
|
self.t.setheading(self.t.towards(x + self.xTranslate, -y + self.yTranslate))
|
|
self.t.goto(x + self.xTranslate, -y + self.yTranslate)
|
|
|
|
def dropBreadcrumb(self, color):
|
|
self.t.dot(10, color)
|
|
|
|
def updatePosition(self, cell, val=None):
|
|
if val:
|
|
cell._contents = val
|
|
self.moveTurtle(cell.x, cell.y)
|
|
|
|
if val == Contents.PART_OF_PATH:
|
|
color = 'green'
|
|
elif val == Contents.TRIED:
|
|
color = 'black'
|
|
elif val == Contents.DEAD_END:
|
|
color = 'red'
|
|
else:
|
|
color = None
|
|
|
|
if color:
|
|
self.dropBreadcrumb(color)
|
|
|
|
|
|
def getStart(self) -> Cell:
|
|
''' accessor method to return the Cell object corresponding to the Maze start
|
|
Returns:
|
|
the Cell object at the Maze start location
|
|
'''
|
|
return self._start
|
|
|
|
def getGoal(self) -> Cell:
|
|
''' accessor method to return the Cell object corresponding to the Maze goal
|
|
Returns:
|
|
the Cell object at the Maze goal location
|
|
'''
|
|
return self._goal
|
|
|
|
def __getitem__(self, idx):
|
|
"""
|
|
Magic method for accessing element with square brackets []
|
|
|
|
Example usage:
|
|
|
|
maze = TurtleMaze(file)
|
|
maze[4] # the row at index 4 (the 5th row in the maze)
|
|
maze[4][0] # the first cell in the row at index 4
|
|
"""
|
|
|
|
return self.maze_grid[idx]
|
|
|
|
|
|
|
|
def main():
|
|
|
|
maze = TurtleMaze('maze_3.txt')
|
|
maze.drawMaze()
|
|
# print("row at index 4", maze[4])
|
|
# print("cell at index 4, 4", maze[4][4])
|
|
|
|
maze.updatePosition(maze.getStart())
|
|
print(maze.getStart())
|
|
|
|
maze.t.screen.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |