-
Notifications
You must be signed in to change notification settings - Fork 18
/
maze.py
325 lines (279 loc) · 10.7 KB
/
maze.py
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# -*- coding: utf-8 -*-
import random
# Easy to read representation for each cardinal direction.
N, S, W, E = ('n', 's', 'w', 'e')
class Cell(object):
"""
Class for each individual cell. Knows only its position and which walls are
still standing.
"""
def __init__(self, x, y, walls):
self.x = x
self.y = y
self.walls = set(walls)
def __repr__(self):
# <15, 25 (es )>
return '<{}, {} ({:4})>'.format(self.x, self.y, ''.join(sorted(self.walls)))
def __contains__(self, item):
# N in cell
return item in self.walls
def is_full(self):
"""
Returns True if all walls are still standing.
"""
return len(self.walls) == 4
def _wall_to(self, other):
"""
Returns the direction to the given cell from the current one.
Must be one cell away only.
"""
assert abs(self.x - other.x) + abs(self.y - other.y) == 1, '{}, {}'.format(self, other)
if other.y < self.y:
return N
elif other.y > self.y:
return S
elif other.x < self.x:
return W
elif other.x > self.x:
return E
else:
assert False
def connect(self, other):
"""
Removes the wall between two adjacent cells.
"""
other.walls.remove(other._wall_to(self))
self.walls.remove(self._wall_to(other))
class Maze(object):
"""
Maze class containing full board and maze generation algorithms.
"""
# Unicode character for a wall with other walls in the given directions.
UNICODE_BY_CONNECTIONS = {'ensw': '┼',
'ens': '├',
'enw': '┴',
'esw': '┬',
'es': '┌',
'en': '└',
'ew': '─',
'e': '╶',
'nsw': '┤',
'ns': '│',
'nw': '┘',
'sw': '┐',
's': '╷',
'n': '╵',
'w': '╴'}
def __init__(self, width=20, height=10):
"""
Creates a new maze with the given sizes, with all walls standing.
"""
self.width = width
self.height = height
self.cells = []
for y in range(self.height):
for x in range(self.width):
self.cells.append(Cell(x, y, [N, S, E, W]))
def __getitem__(self, index):
"""
Returns the cell at index = (x, y).
"""
x, y = index
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[x + y * self.width]
else:
return None
def neighbors(self, cell):
"""
Returns the list of neighboring cells, not counting diagonals. Cells on
borders or corners may have less than 4 neighbors.
"""
x = cell.x
y = cell.y
for new_x, new_y in [(x, y - 1), (x, y + 1), (x - 1, y), (x + 1, y)]:
neighbor = self[new_x, new_y]
if neighbor is not None:
yield neighbor
def _to_str_matrix(self):
"""
Returns a matrix with a pretty printed visual representation of this
maze. Example 5x5:
OOOOOOOOOOO
O O O
OOO OOO O O
O O O O
O OOO OOO O
O O O O
OOO O O OOO
O O O O O
O OOO O O O
O O O
OOOOOOOOOOO
"""
str_matrix = [['O'] * (self.width * 2 + 1)
for i in range(self.height * 2 + 1)]
for cell in self.cells:
x = cell.x * 2 + 1
y = cell.y * 2 + 1
str_matrix[y][x] = ' '
if N not in cell and y > 0:
str_matrix[y - 1][x + 0] = ' '
if S not in cell and y + 1 < self.width:
str_matrix[y + 1][x + 0] = ' '
if W not in cell and x > 0:
str_matrix[y][x - 1] = ' '
if E not in cell and x + 1 < self.width:
str_matrix[y][x + 1] = ' '
return str_matrix
def __repr__(self):
"""
Returns an Unicode representation of the maze. Size is doubled
horizontally to avoid a stretched look. Example 5x5:
┌───┬───────┬───────┐
│ │ │ │
│ │ ╷ ╵ ╷ │
│ │ │ │ │
│ │ └───┬───┘ │
│ │ │ │
│ └───────┤ ┌───┤
│ │ │ │
│ ╷ ╶───┘ ╵ │
│ │ │
└───┴───────────────┘
"""
# Starts with regular representation. Looks stretched because chars are
# twice as high as they are wide (look at docs example in
# `Maze._to_str_matrix`).
skinny_matrix = self._to_str_matrix()
# Simply duplicate each character in each line.
double_wide_matrix = []
for line in skinny_matrix:
double_wide_matrix.append([])
for char in line:
double_wide_matrix[-1].append(char)
double_wide_matrix[-1].append(char)
# The last two chars of each line are walls, and we will need only one.
# So we remove the last char of each line.
matrix = [line[:-1] for line in double_wide_matrix]
def g(x, y):
"""
Returns True if there is a wall at (x, y). Values outside the valid
range always return false.
This is a temporary helper function.
"""
if 0 <= x < len(matrix[0]) and 0 <= y < len(matrix):
return matrix[y][x] != ' '
else:
return False
# Fix double wide walls, finally giving the impression of a symmetric
# maze.
for y, line in enumerate(matrix):
for x, char in enumerate(line):
if not g(x, y) and g(x - 1, y):
matrix[y][x - 1] = ' '
# Right now the maze has the correct aspect ratio, but is still using
# 'O' to represent walls.
# Finally we replace the walls with Unicode characters depending on
# their context.
for y, line in enumerate(matrix):
for x, char in enumerate(line):
if not g(x, y):
continue
connections = set((N, S, E, W))
if not g(x, y + 1): connections.remove(S)
if not g(x, y - 1): connections.remove(N)
if not g(x + 1, y): connections.remove(E)
if not g(x - 1, y): connections.remove(W)
str_connections = ''.join(sorted(connections))
# Note we are changing the matrix we are reading. We need to be
# careful as to not break the `g` function implementation.
matrix[y][x] = Maze.UNICODE_BY_CONNECTIONS[str_connections]
# Simple double join to transform list of lists into string.
return '\n'.join(''.join(line) for line in matrix) + '\n'
def randomize(self):
"""
Knocks down random walls to build a random perfect maze.
Algorithm from http://mazeworks.com/mazegen/mazetut/index.htm
"""
cell_stack = []
cell = random.choice(self.cells)
n_visited_cells = 1
while n_visited_cells < len(self.cells):
neighbors = [c for c in self.neighbors(cell) if c.is_full()]
if len(neighbors):
neighbor = random.choice(neighbors)
cell.connect(neighbor)
cell_stack.append(cell)
cell = neighbor
n_visited_cells += 1
else:
cell = cell_stack.pop()
@staticmethod
def generate(width=20, height=10):
"""
Returns a new random perfect maze with the given sizes.
"""
m = Maze(width, height)
m.randomize()
return m
class MazeGame(object):
"""
Class for interactively playing random maze games.
"""
def __init__(self, maze):
self.maze = maze or Maze.generate()
def _get_random_position(self):
"""
Returns a random position on the maze.
"""
return (random.randrange(0, self.maze.width),
random.randrange(0, self.maze.height))
def _display(self, pos, value):
"""
Displays a value on the screen from an x and y maze positions.
"""
x, y = pos
# Double x position because displayed maze is double-wide.
console.set_display(y * 2 + 1, x * 4 + 2, value)
def play(self):
"""
Starts an interactive game on this maze, with random starting and goal
positions. Returns True if the user won, or False if she quit the game
by pressing "q".
"""
player = self._get_random_position()
target = self._get_random_position()
while player != target:
console.display(str(self.maze))
self._display(player, '@')
self._display(target, '$')
key = console.get_valid_key(['up', 'down', 'left', 'right', 'q'])
if key == 'q':
return False
direction, difx, dify = {'up': (N, 0, -1),
'down': (S, 0, 1),
'left': (W, -1, 0),
'right': (E, 1, 0)}[key]
current_cell = self.maze[player]
if direction not in current_cell:
player = (player[0] + difx, player[1] + dify)
console.display('You win!')
console.get_key()
return True
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
width = int(sys.argv[1])
if len(sys.argv) > 2:
height = int(sys.argv[2])
else:
height = width
else:
width = 20
height = 10
import console
try:
while MazeGame(Maze.generate(width, height)).play(): pass
except:
import traceback
traceback.print_exc(file=open('error_log.txt', 'a'))