Maze Runner 3D
This three-dimensional maze runner provides the player with a first-person view from inside a maze. Try to find your way out! You can generate maze files by following the instructions in Project 44, “Maze Runner 2D,” or by downloading maze files from https://invpy.com/mazes/.
This 3D-perspective ASCII art starts with the multiline string stored in ALL_OPEN. This string depicts a position in which no paths are closed off by walls. The program then draws the walls, stored in the CLOSED dictionary, on top of the ALL_OPEN string to generate the ASCII art for any possible combination of closed-off paths. For example, here’s how the program generates the view in which the wall is to the left of the player:
\ \
____ ____ \_ \_ ____
|\ /| | | /|
|| || | | ||
||__ __|| | |__ __||
|| |\ /| || | | |\ /| ||
|| | X | || + | = | | X | ||
|| |/ \| || | | |/ \| ||
||_/ \_|| | |_/ \_||
|| || | | ||
___|/ \|___ | | \|___
/ /
/ /
The periods in the ASCII art in the source code get removed before the strings are displayed; they only exist to make entering the code easier, so you don’t insert or leave out blank spaces.
maze_runner_3d.py
1"""Maze 3D, by Al Sweigart al@inventwithpython.com
2Move around a maze and try to escape... in 3D!
3This code is available at https://nostarch.com/big-book-small-python-programming
4Tags: extra-large, artistic, maze, game"""
5
6import copy, sys, os
7
8# Set up the constants:
9WALL = '#'
10EMPTY = ' '
11START = 'S'
12EXIT = 'E'
13BLOCK = chr(9617) # Character 9617 is '░'
14NORTH = 'NORTH'
15SOUTH = 'SOUTH'
16EAST = 'EAST'
17WEST = 'WEST'
18
19
20def wallStrToWallDict(wallStr):
21 """Takes a string representation of a wall drawing (like those in
22 ALL_OPEN or CLOSED) and returns a representation in a dictionary
23 with (x, y) tuples as keys and single-character strings of the
24 character to draw at that x, y location."""
25 wallDict = {}
26 height = 0
27 width = 0
28 for y, line in enumerate(wallStr.splitlines()):
29 if y > height:
30 height = y
31 for x, character in enumerate(line):
32 if x > width:
33 width = x
34 wallDict[(x, y)] = character
35 wallDict['height'] = height + 1
36 wallDict['width'] = width + 1
37 return wallDict
38
39EXIT_DICT = {(0, 0): 'E', (1, 0): 'X', (2, 0): 'I',
40 (3, 0): 'T', 'height': 1, 'width': 4}
41
42# The way we create the strings to display is by converting the pictures
43# in these multiline strings to dictionaries using wallStrToWallDict().
44# Then we compose the wall for the player's location and direction by
45# "pasting" the wall dictionaries in CLOSED on top of the wall dictionary
46# in ALL_OPEN.
47
48ALL_OPEN = wallStrToWallDict(r'''
49.................
50____.........____
51...|\......./|...
52...||.......||...
53...||__...__||...
54...||.|\./|.||...
55...||.|.X.|.||...
56...||.|/.\|.||...
57...||_/...\_||...
58...||.......||...
59___|/.......\|___
60.................
61.................'''.strip())
62# The strip() call is used to remove the newline
63# at the start of this multiline string.
64
65CLOSED = {}
66CLOSED['A'] = wallStrToWallDict(r'''
67_____
68.....
69.....
70.....
71_____'''.strip()) # Paste to 6, 4.
72
73CLOSED['B'] = wallStrToWallDict(r'''
74.\.
75..\
76...
77...
78...
79../
80./.'''.strip()) # Paste to 4, 3.
81
82CLOSED['C'] = wallStrToWallDict(r'''
83___________
84...........
85...........
86...........
87...........
88...........
89...........
90...........
91...........
92___________'''.strip()) # Paste to 3, 1.
93
94CLOSED['D'] = wallStrToWallDict(r'''
95./.
96/..
97...
98...
99...
100\..
101.\.'''.strip()) # Paste to 10, 3.
102
103CLOSED['E'] = wallStrToWallDict(r'''
104..\..
105...\_
106....|
107....|
108....|
109....|
110....|
111....|
112....|
113....|
114....|
115.../.
116../..'''.strip()) # Paste to 0, 0.
117
118CLOSED['F'] = wallStrToWallDict(r'''
119../..
120_/...
121|....
122|....
123|....
124|....
125|....
126|....
127|....
128|....
129|....
130.\...
131..\..'''.strip()) # Paste to 12, 0.
132
133def displayWallDict(wallDict):
134 """Display a wall dictionary, as returned by wallStrToWallDict(), on
135 the screen."""
136 print(BLOCK * (wallDict['width'] + 2))
137 for y in range(wallDict['height']):
138 print(BLOCK, end='')
139 for x in range(wallDict['width']):
140 wall = wallDict[(x, y)]
141 if wall == '.':
142 wall = ' '
143 print(wall, end='')
144 print(BLOCK) # Print block with a newline.
145 print(BLOCK * (wallDict['width'] + 2))
146
147
148def pasteWallDict(srcWallDict, dstWallDict, left, top):
149 """Copy the wall representation dictionary in srcWallDict on top of
150 the one in dstWallDict, offset to the position given by left, top."""
151 dstWallDict = copy.copy(dstWallDict)
152 for x in range(srcWallDict['width']):
153 for y in range(srcWallDict['height']):
154 dstWallDict[(x + left, y + top)] = srcWallDict[(x, y)]
155 return dstWallDict
156
157
158def makeWallDict(maze, playerx, playery, playerDirection, exitx, exity):
159 """From the player's position and direction in the maze (which has
160 an exit at exitx, exity), create the wall representation dictionary
161 by pasting wall dictionaries on top of ALL_OPEN, then return it."""
162
163 # The A-F "sections" (which are relative to the player's direction)
164 # determine which walls in the maze we check to see if we need to
165 # paste them over the wall representation dictionary we're creating.
166
167 if playerDirection == NORTH:
168 # Map of the sections, relative A
169 # to the player @: BCD (Player facing north)
170 # E@F
171 offsets = (('A', 0, -2), ('B', -1, -1), ('C', 0, -1),
172 ('D', 1, -1), ('E', -1, 0), ('F', 1, 0))
173 if playerDirection == SOUTH:
174 # Map of the sections, relative F@E
175 # to the player @: DCB (Player facing south)
176 # A
177 offsets = (('A', 0, 2), ('B', 1, 1), ('C', 0, 1),
178 ('D', -1, 1), ('E', 1, 0), ('F', -1, 0))
179 if playerDirection == EAST:
180 # Map of the sections, relative EB
181 # to the player @: @CA (Player facing east)
182 # FD
183 offsets = (('A', 2, 0), ('B', 1, -1), ('C', 1, 0),
184 ('D', 1, 1), ('E', 0, -1), ('F', 0, 1))
185 if playerDirection == WEST:
186 # Map of the sections, relative DF
187 # to the player @: AC@ (Player facing west)
188 # BE
189 offsets = (('A', -2, 0), ('B', -1, 1), ('C', -1, 0),
190 ('D', -1, -1), ('E', 0, 1), ('F', 0, -1))
191
192 section = {}
193 for sec, xOff, yOff in offsets:
194 section[sec] = maze.get((playerx + xOff, playery + yOff), WALL)
195 if (playerx + xOff, playery + yOff) == (exitx, exity):
196 section[sec] = EXIT
197
198 wallDict = copy.copy(ALL_OPEN)
199 PASTE_CLOSED_TO = {'A': (6, 4), 'B': (4, 3), 'C': (3, 1),
200 'D': (10, 3), 'E': (0, 0), 'F': (12, 0)}
201 for sec in 'ABDCEF':
202 if section[sec] == WALL:
203 wallDict = pasteWallDict(CLOSED[sec], wallDict,
204 PASTE_CLOSED_TO[sec][0], PASTE_CLOSED_TO[sec][1])
205
206 # Draw the EXIT sign if needed:
207 if section['C'] == EXIT:
208 wallDict = pasteWallDict(EXIT_DICT, wallDict, 7, 9)
209 if section['E'] == EXIT:
210 wallDict = pasteWallDict(EXIT_DICT, wallDict, 0, 11)
211 if section['F'] == EXIT:
212 wallDict = pasteWallDict(EXIT_DICT, wallDict, 13, 11)
213
214 return wallDict
215
216
217print('Maze Runner 3D, by Al Sweigart al@inventwithpython.com')
218print('(Maze files are generated by mazemakerrec.py)')
219
220# Get the maze file's filename from the user:
221while True:
222 print('Enter the filename of the maze (or LIST or QUIT):')
223 filename = input('> ')
224
225 # List all the maze files in the current folder:
226 if filename.upper() == 'LIST':
227 print('Maze files found in', os.getcwd())
228 for fileInCurrentFolder in os.listdir():
229 if (fileInCurrentFolder.startswith('maze')
230 and fileInCurrentFolder.endswith('.txt')):
231 print(' ', fileInCurrentFolder)
232 continue
233
234 if filename.upper() == 'QUIT':
235 sys.exit()
236
237 if os.path.exists(filename):
238 break
239 print('There is no file named', filename)
240
241# Load the maze from a file:
242mazeFile = open(filename)
243maze = {}
244lines = mazeFile.readlines()
245px = None
246py = None
247exitx = None
248exity = None
249y = 0
250for line in lines:
251 WIDTH = len(line.rstrip())
252 for x, character in enumerate(line.rstrip()):
253 assert character in (WALL, EMPTY, START, EXIT), 'Invalid character at column {}, line {}'.format(x + 1, y + 1)
254 if character in (WALL, EMPTY):
255 maze[(x, y)] = character
256 elif character == START:
257 px, py = x, y
258 maze[(x, y)] = EMPTY
259 elif character == EXIT:
260 exitx, exity = x, y
261 maze[(x, y)] = EMPTY
262 y += 1
263HEIGHT = y
264
265assert px != None and py != None, 'No start point in file.'
266assert exitx != None and exity != None, 'No exit point in file.'
267pDir = NORTH
268
269
270while True: # Main game loop.
271 displayWallDict(makeWallDict(maze, px, py, pDir, exitx, exity))
272
273 while True: # Get user move.
274 print('Location ({}, {}) Direction: {}'.format(px, py, pDir))
275 print(' (W)')
276 print('Enter direction: (A) (D) or QUIT.')
277 move = input('> ').upper()
278
279 if move == 'QUIT':
280 print('Thanks for playing!')
281 sys.exit()
282
283 if (move not in ['F', 'L', 'R', 'W', 'A', 'D']
284 and not move.startswith('T')):
285 print('Please enter one of F, L, or R (or W, A, D).')
286 continue
287
288 # Move the player according to their intended move:
289 if move == 'F' or move == 'W':
290 if pDir == NORTH and maze[(px, py - 1)] == EMPTY:
291 py -= 1
292 break
293 if pDir == SOUTH and maze[(px, py + 1)] == EMPTY:
294 py += 1
295 break
296 if pDir == EAST and maze[(px + 1, py)] == EMPTY:
297 px += 1
298 break
299 if pDir == WEST and maze[(px - 1, py)] == EMPTY:
300 px -= 1
301 break
302 elif move == 'L' or move == 'A':
303 pDir = {NORTH: WEST, WEST: SOUTH,
304 SOUTH: EAST, EAST: NORTH}[pDir]
305 break
306 elif move == 'R' or move == 'D':
307 pDir = {NORTH: EAST, EAST: SOUTH,
308 SOUTH: WEST, WEST: NORTH}[pDir]
309 break
310 elif move.startswith('T'): # Cheat code: 'T x,y'
311 px, py = move.split()[1].split(',')
312 px = int(px)
313 py = int(py)
314 break
315 else:
316 print('You cannot move in that direction.')
317
318 if (px, py) == (exitx, exity):
319 print('You have reached the exit! Good job!')
320 print('Thanks for playing!')
321 sys.exit()