# Principled Programming / Tim Teitelbaum / Chapter 19. class MRP: """ Maze, Rat, and Path (MRP) Representations. ## Methods # Rat motions and path extension/retractions. turn_clockwise(cls) -> None turn_counter_clockwise(cls) -> None step_forward(cls) -> None step_backward(cls) -> None face_previous(cls) -> None # Maze inspection predicates. is_facing_wall(cls) -> bool is_at_cheese(cls) -> bool is_about_to_repeat(cls) -> bool is_facing_unvisited(cls) -> bool # Maze I/O. input(cls) -> None print_maze(cls) -> None # Tossed arrow abstraction. record_neighbor_and_direction(cls) -> None is_at_neighbor(cls) -> bool restore_direction(cls) -> None # Validity checking, testing, and debugging. is_valid(cls) -> bool is_valid_rat(cls) -> bool is_solution(cls) -> bool generate_input(cls, n: int, w: int) -> None: print_state(self, s: str) -> None """ # Maze. Cells of an N-by-N maze are represented by elements of # array M[2*N+1][2*N+1]. Maze cell ⟨r,c⟩ is represented by array # element M[2*r+1][2*c+1]. The possible walls ⟨top, right, bottom, # left⟩ of the maze cell corresponding to ⟨r,c⟩ are represented by # WALL or NO_WALL in ⟨M[r-1][c], M[r][c+1], M[r+1][c], M[r][c-1]⟩. # The remaining elements of M are unused. lo is 1, and hi is 2*N-1. _N: int = 0 """_N is size of the maze.""" _M: list[list[int]] = [] """_M is a 2-D array containing visit numbers and wall representations. M[r][c], for odd r and c, are int visit numbers (1 thru _move), or _UNVISITED. M[r][c], for r and c of opposite parity, are wall representations: _WALL or _NO_WALL. M[r][c], for even r and c, are unused. """ _WALL: int = -1 """_WALL (in elements _M[r][c] with r and c of opposite parity) represents the presence of a wall.""" _NO_WALL: int = 0 """_NO_WALL (in elements _M[r][c] with r and c of opposite parity) represents the absence of a wall.""" _lo: int = 1 # Left and right maze indices. """_lo is the row and column index of the upper-left cell of _M[][].""" _hi: int = 2 * _N - 1 # Top and right maze indices. """_lo is the row and column index of the lower-right cell of _M[][].""" # Rat. The rat is located in cell M[r][c] facing direction d, where # d=⟨0,1,2,3⟩ represents the orientation ⟨up,right,down,left⟩, # respectively. _r: int = _lo """_r is the row coordinate of the rat's current position.""" _c: int = _lo """_c is the column coordinate of the rat's current position.""" _d: int = 0 """_d is the index number in <up,right,down,left> of the rat's present direction faced.""" # Path. When the rat has traveled to cell ⟨r,c⟩ via a given path # through cells of the maze, the elements of M that correspond to # those cells will be 1, 2, 3, etc., and all other elements of M # that correspond to cells of the maze will be UNVISITED. The # number of the last step in the path is move. _UNVISITED: int = 0 """_UNVISITED represents a cell _M[r][c] that is not on the rat's current path.""" _move: int = _lo """_move is the visit number of the last cell of the current path. """ # Recorded state. _neighbor_number: int # Recorded visit #. """The visit number of a cell into which an arrow has been tossed.""" _neighbor_direction: int # Dir. at time of recording. """The direction faced by the rat at the time an arrow was tossed into the cell in front of it.""" # Unit vectors in direction d # d = 0, 1, 2, 3 # up, right, down, left _deltaR: list[int] = [ -1, 0, 1, 0 ] """_deltaR[d] is the row component of a unit vector in direction d.""" _deltaC: list[int] = [ 0, 1, 0, -1 ] """_deltaC[d] is the column component of a unit vector in direction d.""" # Public interface. # ================= # Rat motions and path extension / retractions. @classmethod def turn_clockwise(cls) -> None: """Turn the rat 90 degrees clockwise.""" MRP._d = (MRP._d + 1) % 4 @classmethod def turn_counter_clockwise(cls) -> None: """Turn the rat 90 degrees counter-clockwise.""" MRP._d = (MRP._d - 1) % 4 @classmethod def step_forward(cls) -> None: """Step one cell forward. Precondition: Not facing a wall.""" MRP._r += 2 * MRP._deltaR[MRP._d]; MRP._c += 2 * MRP._deltaC[MRP._d] MRP._move += 1; MRP._M[MRP._r][MRP._c] = MRP._move @classmethod def step_backward(cls) -> None: """Step one cell along the current path (but in the direction faced). Precondition: Not facing a wall.""" MRP._M[MRP._r][MRP._c] = MRP._UNVISITED MRP._move -= 1 MRP._r += 2 * MRP._deltaR[MRP._d] MRP._c += 2 * MRP._deltaC[MRP._d] @classmethod def face_previous(cls) -> None: """Turn in place to face the previous cell on the path.""" MRP._d = 0 while MRP.is_facing_wall() or (MRP._M[MRP._r][MRP._c] - 1 != MRP._M[MRP._r + 2 * MRP._deltaR[MRP._d] ][MRP._c + 2 * MRP._deltaC[MRP._d]] ): MRP._d += 1 # Maze inspection predicates. @classmethod def is_facing_wall(cls) -> bool: """True if there is a wall in the facing direction, False otherwise.""" return MRP._M[MRP._r + MRP._deltaR[MRP._d] ][MRP._c + MRP._deltaC[MRP._d]] == MRP._WALL @classmethod def is_at_cheese(cls) -> bool: """True if the present rat location is colocated with the cheese.""" return (MRP._r == MRP._hi) and (MRP._c == MRP._hi) @classmethod def is_about_to_repeat(cls) -> bool: """Rat is in the upper-left cell facing left, and with next turn will re-enter the initial state.""" return (MRP._r == MRP._lo) and (MRP._c == MRP._lo) and (MRP._d==3) @classmethod def is_facing_unvisited(cls) -> bool: """The cell the rat is facing is not on the current path. Precondition: no intervening wall.""" return MRP._M[MRP._r + 2 * MRP._deltaR[MRP._d] ][MRP._c + 2 * MRP._deltaC[MRP._d]] == MRP._UNVISITED #I/O @classmethod def input(cls) -> None: """Input an N-by-N maze.""" # Input file split into lines. # file = "1\nxxx\nx x\nxxx\nxxx" # 1x1. # file = "2\nxxxxx\nx x x\nx x\nx x\nxxxxx" # 2x2, with obstacle. # file = "3\nxxxxxxx\nx x\nx x x\nx xx x\nx xxxxx\nx x\nxxxxxxx" # 3x3, with room. file = "5\n###########\n# # #\n# ## # # #\n# # # #\n# # ## ###\n# # # #\n# # # ### #\n# # # # #\n# ### #####\n# # #\n###########\n" # 5x5, for Chapter 19. lines = file.split("\n") # Maze. As per representation invariant. MRP._N = int(lines[0]); del lines[0] MRP._lo = 1; MRP._hi = 2 * MRP._N - 1 MRP._M = [[0 for _ in range(2 * MRP._N + 1)] for _ in range(2 * MRP._N + 1)] for r in range(MRP._lo - 1, MRP._hi + 2): line = lines[0]; del lines[0] for c in range(MRP._lo - 1, MRP._hi + 2): if (r % 2 == 1) and (c % 2 == 1): MRP._M[r][c] = MRP._UNVISITED elif line[c:c+1] == " ": MRP._M[r][c] = MRP._NO_WALL else: MRP._M[r][c] = MRP._WALL # Rat. Place rat in upper-left cell facing up. MRP._r = MRP._lo; MRP._c = MRP._lo; MRP._d = 0 # Path. Establish the rat in the upper-left cell. MRP._move = 1; MRP._M[MRP._r][MRP._c] = MRP._move @classmethod def print_maze(cls) -> None: """Output N-by-N maze, with walls and path.""" for r in range(MRP._lo - 1, MRP._hi + 2): for c in range(MRP._lo - 1, MRP._hi + 2): if MRP._M[r][c] == MRP._WALL: s = "#" elif ((MRP._M[r][c] == MRP._NO_WALL) or (MRP._M[r][c] == MRP._UNVISITED)): s = " " else: s = str(MRP._M[r][c]) + "" print((s + " ")[0:3], end='') print() # Tossed arrow abstraction. @classmethod def record_neighbor_and_direction(cls) -> None: """ “Toss an arrow into a neighboring cell.” """ MRP._neighbor_number = MRP._M[MRP._r + 2 * MRP._deltaR[MRP._d] ][MRP._c + 2 * MRP._deltaC[MRP._d]] MRP._neighbor_direction = MRP._d @classmethod def is_at_neighbor(cls) -> bool: """ “Detect being in a cell into which an arrow was tossed.” """ return MRP._M[MRP._r][MRP._c] == MRP._neighbor_number @classmethod def restore_direction(cls) -> None: """ “Align rat's direction with the orientation of a tossed arrow.” """ MRP._d = MRP._neighbor_direction # Validity checking, testing, and debugging. @classmethod def _is_valid_path(cls, r: int, c: int) -> bool: """Return False iff the rat reached cell ⟨r,c⟩ via an invalid path.""" if MRP._M[r][c] == MRP._UNVISITED: return True # No claim if UNVISITED. else: while not((r == MRP._lo) and (c == MRP._lo)): # Go to any valid predecessor; return False if there is none. d = 0 while (d < 4) and ( ( MRP._M[r + MRP._deltaR[d] ][c + MRP._deltaC[d]] == MRP._WALL ) or (MRP._M[r + 2 * MRP._deltaR[d]][ c + 2 * MRP._deltaC[d]] != (MRP._M[r][c] - 1) ) ): d += 1 if d == 4: return False r += 2 * MRP._deltaR[d]; c += 2 * MRP._deltaC[d] return True # Reached upper-left cell. @classmethod def is_valid(cls) -> bool: """Return False on evidence that a representation invariant is violated.""" # return MRP._is_valid_path(MRP._r, MRP._c) and MRP._is_valid_rat() @classmethod def _is_valid_rat(cls) -> bool: """Return False iff rat’s representation invariant is violated.""" if (MRP._r < 0) or (MRP._r > MRP._hi) or ( (MRP._c < 0) or (MRP._c > MRP._hi)): return False elif (MRP._d < 0) or (MRP._d > 3): return False elif MRP._M[MRP._r][MRP._c] != MRP._move: return False else: return True @classmethod def is_solution(cls) -> bool: """Return False iff rat reached lower-right cell via an invalid path.""" return MRP._is_valid_path(MRP._hi, MRP._hi) @classmethod def generate_input(cls, n: int, w: int) -> None: """Create an N-by-N maze with walls given by the bits of w.""" # Maze. MRP._N = n;lo = MRP._lo = 1; hi = MRP._hi = 2 * n - 1 MRP._M = [[0 for _ in range(2 * n + 1)] for _ in range(2 * n + 1)] # Set boundary walls. for i in range(0, hi+2): MRP._M[lo - 1][i] = MRP._M[hi + 1][i] = MRP._WALL MRP._M[i][lo - 1] = MRP._M[i][hi + 1] = MRP._WALL # Set 2*n*(n-1) interior walls to the corresponding bits of w. for r in range(lo, hi + 1): for c in range(lo, hi + 1): if (r%2 == 0 and c%2 == 1) or (r%2 == 1 and c%2 == 0): if w%2 == 1: MRP._M[r][c] = MRP._WALL else: MRP._M[r][c] = MRP._NO_WALL w = w // 2 # Rat. MRP._r = lo; MRP._c = lo; MRP._d = 0 # Path. MRP._move = 1; MRP._M[lo][lo] = MRP._move @classmethod def print_state(cls, s: str) -> None: """Print state variables for debugging purposes.""" print(s, MRP._r, MRP._c, MRP._d, MRP._move)