-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmaze.py
More file actions
217 lines (177 loc) · 7.26 KB
/
maze.py
File metadata and controls
217 lines (177 loc) · 7.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import pdb
import numpy as np
from heading import Heading
from rotation import Rotation
class Maze(object):
# Maps headings to the axial components.
HEADING_COMPONENTS_MAP = {
Heading.NORTH: np.array([0, 1]),
Heading.EAST: np.array([1, 0]),
Heading.SOUTH: np.array([0, -1]),
Heading.WEST: np.array([-1, 0])
}
# Maps heading to wall decimal.
HEADING_DECIMAL_MAP = {
Heading.NORTH: 1,
Heading.EAST: 2,
Heading.SOUTH: 4,
Heading.WEST: 8
}
def __init__(self, filename):
"""Reads in a maze file.
Maze objects have two main attributes:
- dim: mazes should be square, with sides of even length. (integer)
- walls: passages are coded as a 4-bit number, with a bit value taking
0 if there is a wall and 1 if there is no wall. The 1s register
corresponds with a square's top edge, 2s register the right edge,
4s register the bottom edge, and 8s register the left edge. (numpy
array)
The initialization function also performs some consistency checks for
wall positioning.
Arguments:
filename -- the path to the maze file.
"""
with open(filename, 'r') as f_in:
# Read lines of file.
lines = [line for line in f_in]
# First line should be an integer with the maze dimensions
self.dim = int(lines[0])
# Subsequent lines describe the permissability of walls
walls = []
for line in lines[1:]:
walls.append(list(map(int, line.strip().split(','))))
self.walls = np.array(walls)
# Perform validation on maze
# Maze dimensions
if self.dim % 2:
raise Exception('Maze dimensions must be even in length!')
if self.walls.shape != (self.dim, self.dim):
raise Exception('Maze shape does not match dimension attribute!')
# Wall permeability
wall_errors = []
# vertical walls
for x in range(self.dim-1):
for y in range(self.dim):
if (self.walls[x,y] & 2 != 0) != (self.walls[x+1,y] & 8 != 0):
wall_errors.append([(x,y), 'v'])
# horizontal walls
for y in range(self.dim-1):
for x in range(self.dim):
if (self.walls[x,y] & 1 != 0) != (self.walls[x,y+1] & 4 != 0):
wall_errors.append([(x,y), 'h'])
if wall_errors:
for cell, wall_type in wall_errors:
if wall_type == 'v':
cell2 = (cell[0]+1, cell[1])
print(f"Inconsistent vertical wall betweeen {cell} and {cell2}")
else:
cell2 = (cell[0], cell[1]+1)
print(f"Inconsistent horizontal wall betweeen {cell} and {cell2}")
raise Exception('Consistency errors found in wall specifications!')
def is_permissible(self, pos, heading):
"""Tells if we can move from a square in a heading.
Arguments:
pos -- the current position.
heading -- a Heading value, e.g. Heading.NORTH.
Returns:
True if we can move, False otherwise.
"""
# Check if there's a wall in that direction.
return self.walls[pos[0], pos[1]] & self.HEADING_DECIMAL_MAP[heading] != 0
def dist_to_wall(self, pos, heading):
"""Checks the distance to a wall in a particular heading.
Arguments:
pos -- the current position.
heading -- a Heading value, e.g. Heading.NORTH.
Return:
an integer distance. The number of moves that can be made in that direction.
"""
# Copy the pos as we're modifying it.
curr_pos = pos.copy()
distance = 0
while self.is_permissible(curr_pos, heading):
distance += 1
curr_pos[0] += self.HEADING_COMPONENTS_MAP[heading][0]
curr_pos[1] += self.HEADING_COMPONENTS_MAP[heading][1]
return distance
def new_pos(self, pos, heading, move):
"""Returns the new position after moving.
Arguments:
pos -- the current position.
heading -- a Heading value, e.g. Heading.NORTH.
move -- the desired move.
Returns:
A list of [x, y] int components, showing the new position.
"""
# Get x, y changes.
dx, dy = move * self.HEADING_COMPONENTS_MAP[heading]
# Update x, y co-ordinates.
x_new, y_new = pos[0] + dx, pos[1] + dy
return [int(x_new), int(y_new)]
def sensor_readings(self, pos, heading):
"""Calculate the mouse's sensor readings.
Arguments:
pos -- the mouse's current position.
heading -- a Heading value, e.g. Heading.NORTH.
Returns:
A tuple of sensor readings, each giving the distance to the wall in the (left, middle, right) directions.
"""
# Get left and right headings.
l_head = heading.rotate(Rotation.LEFT)
r_head = heading.rotate(Rotation.RIGHT)
# Get distances for each heading.
dist = self.dist_to_wall(pos, heading)
l_dist = self.dist_to_wall(pos, l_head)
r_dist = self.dist_to_wall(pos, r_head)
return (l_dist, dist, r_dist)
def reached_goal(self, pos):
"""Is the position within the goal?
Arguments:
pos -- the position.
Returns:
True if goal reached, else False.
"""
# Both axes will have the same goal co-ordinates.
goal_coords = [self.dim / 2 -1, self.dim / 2]
# Check if position in goal.
if not (pos[0] in goal_coords and pos[1] in goal_coords):
return False
return True
def valid_move(self, pos, heading, size):
"""Checks if the mouse's move is valid in the context of the maze.
Arguments:
pos -- the [x, y] co-ordinates of the mouse.
heading -- a Heading, e.g. Heading.NORTH.
size -- the size of the move. Positive or negative.
Returns:
True if valid move, False otherwise.
"""
# Check that starting pos is valid.
if not self.pos_exists(pos):
return False
# Get the move heading.
move_heading = heading
dir = (-1, 1)[size > 0]
if dir == -1:
move_heading = move_heading.opposite()
# Get distance to the wall.
dist = self.dist_to_wall(pos, move_heading)
# Is the move size larger than the distance to the wall?
if abs(size) > dist:
return False
return True
def pos_exists(self, pos):
"""Checks if this is a valid position in the maze.
Arguments:
pos -- a list containing the [x, y] co-ordinates of the mouse.
Returns:
True if position exists, False otherwise.
"""
# Check that position is integer.
if not isinstance(pos[0], np.int8) or not isinstance(pos[1], np.int8):
return False
# Check against maze dimensions.
if not (pos[0] >= 0 and pos[0] < self.dim) or not (pos[1] >= 0 and
pos[1] < self.dim):
return False
return True