Maze Runner 3D

Tags: huge, artistic, game, maze

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()

https://inventwithpython.com/bigbookpython/project45.html