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/.
When you run mazerunner3d.py, the output will look like this:
Maze Runner 3D, by Al Sweigart [email protected]
(Maze files are generated by mazemakerrec.py)
Enter the filename of the maze (or LIST or QUIT):
> maze75x11s1.txt
░░░░░░░░░░░░░░░░░░░
░ \ / ░
░ \_________/ ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ | | ░
░ |_______| ░
░ / \ ░
░ / \ ░
░░░░░░░░░░░░░░░░░░░
Location (1, 1) Direction: NORTH
(W)
Enter direction: (A) (D) or QUIT.
> d
░░░░░░░░░░░░░░░░░░░
░ \ ░
░ \_____________░
░ | ░
░ | ░
░ | ░
░ | ░
░ | ░
░ | ░
░ | ░
░ | ░
░ |____________░
░ / ░
░ / ░
░░░░░░░░░░░░░░░░░░░
Location (1, 1) Direction: EAST
--snip--
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.
Here is the source code for the 3D maze:
1. """Maze 3D, by Al Sweigart [email protected]
2. Move around a maze and try to escape... in 3D!
3. This code is available at https://nostarch.com/big-book-small-python-programming
4. Tags: extra-large, artistic, maze, game"""
5.
6. import copy, sys, os
7.
8. # Set up the constants:
9. WALL = '#'
10. EMPTY = ' '
11. START = 'S'
12. EXIT = 'E'
13. BLOCK = chr(9617) # Character 9617 is '░'
14. NORTH = 'NORTH'
15. SOUTH = 'SOUTH'
16. EAST = 'EAST'
17. WEST = 'WEST'
18.
19.
20. def 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.
39. EXIT_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.
48. ALL_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.
65. CLOSED = {}
66. CLOSED['A'] = wallStrToWallDict(r'''
67. _____
68. .....
69. .....
70. .....
71. _____'''.strip()) # Paste to 6, 4.
72.
73. CLOSED['B'] = wallStrToWallDict(r'''
74. .\.
75. ..\
76. ...
77. ...
78. ...
79. ../
80. ./.'''.strip()) # Paste to 4, 3.
81.
82. CLOSED['C'] = wallStrToWallDict(r'''
83. ___________
84. ...........
85. ...........
86. ...........
87. ...........
88. ...........
89. ...........
90. ...........
91. ...........
92. ___________'''.strip()) # Paste to 3, 1.
93.
94. CLOSED['D'] = wallStrToWallDict(r'''
95. ./.
96. /..
97. ...
98. ...
99. ...
100. \..
101. .\.'''.strip()) # Paste to 10, 3.
102.
103. CLOSED['E'] = wallStrToWallDict(r'''
104. ..\..
105. ...\_
106. ....|
107. ....|
108. ....|
109. ....|
110. ....|
111. ....|
112. ....|
113. ....|
114. ....|
115. .../.
116. ../..'''.strip()) # Paste to 0, 0.
117.
118. CLOSED['F'] = wallStrToWallDict(r'''
119. ../..
120. _/...
121. |....
122. |....
123. |....
124. |....
125. |....
126. |....
127. |....
128. |....
129. |....
130. .\...
131. ..\..'''.strip()) # Paste to 12, 0.
132.
133. def 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.
148. def 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.
158. def 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.
217. print('Maze Runner 3D, by Al Sweigart [email protected]')
218. print('(Maze files are generated by mazemakerrec.py)')
219.
220. # Get the maze file's filename from the user:
221. while 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:
242. mazeFile = open(filename)
243. maze = {}
244. lines = mazeFile.readlines()
245. px = None
246. py = None
247. exitx = None
248. exity = None
249. y = 0
250. for 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
263. HEIGHT = y
264.
265. assert px != None and py != None, 'No start point in file.'
266. assert exitx != None and exity != None, 'No exit point in file.'
267. pDir = NORTH
268.
269.
270. while 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()
Try to find the answers to the following questions. Experiment with some modifications to the code and rerun the program to see what effect the changes have.
move == 'QUIT'
on line 279 to move == 'quit'
?