Build a 2-player maze game with Python Part 5

by Dan Duda

Requirements

  • Python 3.7
  • PyCharm Community (optional)
  • Basic Python knowledge

Goto Part 4

Introduction

In part 4 of this tutorial series we were able to draw our maze centered on our display. Now it’s time to start making it into a playable game.

Player sprites

PyGame includes a Sprite object that we can use for our players. Sprites represent a discrete image on the screen that can move around, etc. Let’s create a new class that uses the Sprite object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Player(pg.sprite.Sprite):
def __init__(self, color, x, y, radius):
# Call the parent class (Sprite) constructor
super().__init__()

# Save the start position
self.start_x = x
self.start_y = y

# Create the rectangular image, fill and set background to transparent
self.image = pg.Surface([radius * 2, radius * 2])
self.image.fill(MAZE_COLOR)
self.image.set_colorkey(MAZE_COLOR)

# Draw our player onto the transparent rectangle
pg.draw.circle(self.image, color, (radius, radius), radius)

self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y

def reset(self):
self.rect.x = self.start_x
self.rect.y = self.start_y

In line 1 we define a new class called “Player” that inherits from “pg.sprite.Sprite”. In the “init” method we will require a color, starting x and y coordinates, and a radius. Our players will be represented by colored circles. Since a Sprite is actually a rectangle behind the scenes we create a rectangle that will fit the player cirlce and make it transparent on lines 11-13. On line 16 we draw our player circle onto the transparent rectangle. We then get the image and set its starting x and y position. On line 22 we create a “reset” method that we can call after a game ends to put the player back in their starting position.

Let’s add a couple color constants for our players. Player 1 will be red and player 2 will be blue.

1
2
PLAYER1_COLOR = (255, 0, 0)
PLAYER2_COLOR = (0, 0, 255)

In our MazeGenerator “init” method we will create instances of the Player class to represent our players. We will position player 1 in the upper left corner of the maze and player 2 in the bottom right corner. We create instances of the Player class on lines 2 and 6. Lines 4 and 10 setup some placeholder variables for the players’ sprite objects used for drawing later on.

1
2
3
4
5
6
7
8
9
10
# Create players
self.player1 = Player(PLAYER1_COLOR, MAZE_TOP_LEFT_CORNER[0] + BLOCK_SIZE,
MAZE_TOP_LEFT_CORNER[1] + BLOCK_SIZE, (BLOCK_SIZE * 3) // 2)
self.player1_sprite = None

self.player2 = Player(PLAYER2_COLOR,
MAZE_TOP_LEFT_CORNER[0] + MAZE_WIDTH_PX - CELL_SIZE,
MAZE_TOP_LEFT_CORNER[1] + MAZE_HEIGHT_PX - CELL_SIZE,
(BLOCK_SIZE * 3) // 2)
self.player2_sprite = None

We will initialize our players’ sprites in the “initialize” method we added in part 4.

1
2
3
4
5
6
7
8
9
def initialize(self):
self.player1_sprite = None
self.player1.reset()
self.player2_sprite = None
self.player2.reset()

self.generate_maze()
self.player1_sprite = pg.sprite.RenderPlain(self.player1)
self.player2_sprite = pg.sprite.RenderPlain(self.player2)

Let’s also add a new method called “draw_screen” that will handle drawing everything that needs to be drawn.

1
2
3
4
5
6
def draw_screen(self):
self.draw_maze()
self.player1_sprite.draw(self.screen)
self.player2_sprite.draw(self.screen)

pg.display.update()

And we’ll need to call “draw_screen” from our “run_game” method. We’ll call this inside the main game loop so that the screen is updated every frame.

1
2
3
4
5
6
7
8
9
10
11
12
13
def run_game(self):
self.initialize()

# Main game loop
run = True
while run:
for event in pg.event.get():
if event.type == pg.QUIT:
run = False

self.draw_screen()

pg.quit()

Also since it can take a long time to display the maze being generated we can set out SHOW_DRAW constant to False.

1
SHOW_DRAW = False  # Show the maze being created

If we run the app now we see our two players positioned at their starting points.


Game goals

Our players should have a goal. We will mark exits for each player at the opposing player’s starting point. We will just color the wall next to the player with the opposing player’s color.

In the “draw_maze” method let’s add the following code. This will go at the bottom of the method outside of the loop.

1
2
3
4
5
6
7
8
# Color the player exits
pg.draw.rect(self.screen, PLAYER2_COLOR, (MAZE_TOP_LEFT_CORNER[0],
MAZE_TOP_LEFT_CORNER[1] + BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE * 3))

pg.draw.rect(self.screen, PLAYER1_COLOR,
(MAZE_TOP_LEFT_CORNER[0] + MAZE_WIDTH_PX - BLOCK_SIZE,
MAZE_TOP_LEFT_CORNER[1] + MAZE_HEIGHT_PX - BLOCK_SIZE * 4,
BLOCK_SIZE, BLOCK_SIZE * 3))

Now we should see the colored exits.


Moving the player

To be able to move the players on the screen we now have to deal with keyboard events. We also need to figure out how to have two players use a single keyboard to play at the same time. An obvious choice for moving a player would be the arrow keys. Since we’ll have a second player I chose the WASD keys as the other control keys as they’re popular in 1st person shooters and also because they’re closer to the other side of the keyboard from the arrow keys.

One way of detecting key presses in PyGame is to use the “pg.key.get_pressed” method and that is what we will use. In our main game loop for each frame we will check if any keys are pressed and then check if they were our control keys and if so call a method to process those actions. Our “run_game” method should now look like this:

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
def run_game(self):
self.initialize()

# Main game loop
run = True
while run:
for event in pg.event.get():
if event.type == pg.QUIT:
run = False

keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.move_left(self.player2)
if keys[pg.K_RIGHT]:
self.move_right(self.player2)
if keys[pg.K_UP]:
self.move_up(self.player2)
if keys[pg.K_DOWN]:
self.move_down(self.player2)
if keys[pg.K_a]:
self.move_left(self.player1)
if keys[pg.K_d]:
self.move_right(self.player1)
if keys[pg.K_w]:
self.move_up(self.player1)
if keys[pg.K_s]:
self.move_down(self.player1)

self.draw_screen()

pg.quit()

We first check if any keys were pressed and store them in “keys”. Then we check for each control key and if pressed call an action method passing in the player. We pass in the player so that we only need to create 4 action methods, one for each direction.

Now we create the actions methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def move_up(self, player):
if self.can_move(Direction.North, player):
player.rect.y -= 1

def move_right(self, player):
if self.can_move(Direction.East, player):
player.rect.x += 1

def move_down(self, player):
if self.can_move(Direction.South, player):
player.rect.y += 1

def move_left(self, player):
if self.can_move(Direction.West, player):
player.rect.x -= 1

We need to verify that the player can actually move in the direction they are trying to move so we first call a method called “can_move” that we will need to create. If the player is able to move then we move the player’s sprite in that direction by 1 pixel.

And now for the “can_move” method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def can_move(self, direction, player):
# Top left corner of first cell
corner_offset_x = MAZE_TOP_LEFT_CORNER[0] + BLOCK_SIZE
corner_offset_y = MAZE_TOP_LEFT_CORNER[1] + BLOCK_SIZE

# Calculate which cells the player occupies
square = BLOCK_SIZE * 4
p1 = (player.rect.x - corner_offset_x, player.rect.y - corner_offset_y)
p2 = (p1[0] + square - 1, p1[1] + square - 1)
player_pos1 = (p1[0] // square, p1[1] // square)
player_pos2 = (p2[0] // square, p2[1] // square)
cell_index1 = self.get_cell_index((player_pos1[0], player_pos1[1]))
cell_index2 = self.get_cell_index((player_pos2[0], player_pos2[1]))

functions = {
Direction.North: self.can_move_up,
Direction.East: self.can_move_right,
Direction.South: self.can_move_down,
Direction.West: self.can_move_left
}

return functions[direction](cell_index1, cell_index2)

This is probably the most complicated method in the game so let’s break it down. A player’s sprite can occupy either a single cell or 2 cells (for when the player is moving from one cell to another). We get the top-left corner of the first maze cell to use as an offset on lines 3 and 4. We know that each cell is a block size * 4 (3 for path and one for wall) so we store that in square. Then using the player’s rectangle position we calculate the top-left corner of the player’s position and store in p1 and the bottom-right corner in p2. We then calculate the player’s x and y maze coordinate (not pixel but grid). From the grid x and y position we use our helper method on lines 12 and 13 to get the indexes into our maze list. If the player is occupying a single cell the indexes will be the same.

On lines 15-20 we create a dictionary look up some helper methods that we will write. This is a shortcut to writing a long if,elif clause. Since we passed in the direction we can then call the helper method with a single statement on line 22. We pass the two cell indexes to whichever helper method will be called.

And the 4 new helper methods are defined as.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def can_move_up(self, index1, index2):
if index1 == index2:
return self.maze[index1] & CellProp.Path_N.value
else:
return index2 == index1 + MAZE_WIDTH

def can_move_right(self, index1, index2):
if index1 == index2:
return self.maze[index1] & CellProp.Path_E.value
else:
return index2 == index1 + 1

def can_move_down(self, index1, index2):
if index1 == index2:
return self.maze[index1] & CellProp.Path_S.value
else:
return index2 == index1 + MAZE_WIDTH

def can_move_left(self, index1, index2):
if index1 == index2:
return self.maze[index1] & CellProp.Path_W.value
else:
return index2 == index1 + 1

Based on the direction we use our bitwise operator to check if there is a pathway in that direction. If the player is between two cells then we can assume there is a pathway. If the player is trying to move between two cells we verify that it’s in the correct direction.

Run the game now and you be able to move both players around. Notice though they move very slowly. Recall that in our main game loop we are calling “draw_screen” in every iteration of the loop, every frame. Our “draw_screen” method in turn calls our “draw_maze” method. The “draw_maze” method is fairly intensive with lots of nested loops so can use a lot of CPU cycles to run.

Optimizing our drawing

To see the problem more clearly let’s check what our FPS (frames per second) are when we play our game. PyGame includes a way to easily do this. At the very top of our “run_game” method add the following line.

1
clock = pg.time.Clock()

Then right after we call “draw_screen” add the following lines so it looks like this.

1
2
3
4
self.draw_screen()

pg.display.set_caption(f'PyMaze ({str(int(clock.get_fps()))} FPS)')
clock.tick()

This will update the window caption every frame, adding on the current FPS.

Run the app again and look at the window caption. On my system I get about 20 FPS which is pretty slow especially since our game doesn’t do much.

So how can we fix this? Note that once we generate the maze for a game it doesn’t change. What we can do is save the finished maze as an image and then just redraw the image to the screen each frame instead processing the maze list every time.

We’ll need a variable to store the image of the maze in so in our “init” method we’ll add the following after we call “random.seed()”.

1
2
# Store maze as image after we create it so that we just have to redraw the image on update
self.maze_image = None

And in our “generate_maze” method at the bottom outside of the loop add:

1
2
3
4
# save image of completed maze
self.draw_maze()
pg.display.update()
self.maze_image = self.screen.copy()

We first draw the completed maze to the display and then use the “screen.copy()” method to save it to our “maze_image” variable.

In the “draw_screen” method replace the call to “draw_maze()” with the following.

1
self.screen.blit(self.maze_image, (0, 0))

This will draw the image we stored to the display at position 0,0.

Now run the game again and note the FPS. On my system I’m hovering around 400 FPS. Quite a jump from 20 FPS.

Winning condition

We can move our players around the maze and we’ve optimized the drawing. Now how does a player win? We just need to check if the player’s position is at the opposing player’s starting position. We can do this in the “can_move” method.

First let’s add a couple of boolean flags to our “init” method so we can trigger a win condition. Add these after we create the player and player sprite variables.

1
2
self.win1_flag = False
self.win2_flag = False

Then in the “can_move” method before our return statement add.

1
2
3
4
5
6
# Check for maze exit/win
# Check if player is at opposing player's start x,y
if self.player1.rect.x == self.player2.start_x and self.player1.rect.y == self.player2.start_y:
self.win1_flag = True
elif self.player2.rect.x == self.player1.start_x and self.player2.rect.y == self.player1.start_y:
self.win2_flag = True

And change our “run_game” to not process key events if a player has won as well as call a method to display a win message.

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
def run_game(self):
clock = pg.time.Clock()
self.initialize()

# Main game loop
run = True
while run:
for event in pg.event.get():
if event.type == pg.QUIT:
run = False

if not self.win1_flag and not self.win2_flag:
keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.move_left(self.player2)
if keys[pg.K_RIGHT]:
self.move_right(self.player2)
if keys[pg.K_UP]:
self.move_up(self.player2)
if keys[pg.K_DOWN]:
self.move_down(self.player2)
if keys[pg.K_a]:
self.move_left(self.player1)
if keys[pg.K_d]:
self.move_right(self.player1)
if keys[pg.K_w]:
self.move_up(self.player1)
if keys[pg.K_s]:
self.move_down(self.player1)

if self.win1_flag or self.win2_flag:
self.display_win()
self.initialize()
self.win1_flag = self.win2_flag = False

self.draw_screen()

pg.display.set_caption(f'PyMaze ({str(int(clock.get_fps()))} FPS)')
clock.tick()

pg.quit()

Add a new method called “display_win”.

1
2
3
4
5
6
7
8
9
def display_win(self):
msg = 'Player 1 Wins!!!' if self.win1_flag else 'Player 2 Wins!!!'

font = pg.font.SysFont('Arial', 72, True)
size = font.size(msg)
s = font.render(msg, True, MESSAGE_COLOR, (0, 0, 0))
self.screen.blit(s, (SCREEN_WIDTH // 2 - size[0] // 2, SCREEN_HEIGHT // 2 - size[1] // 2))
pg.display.update()
pg.time.wait(3000)

We’ll use PyGame’s font system to display a message in the center of the maze when a player wins and then wait 3 seconds. After the message displays we call “initialize” again to generate a new maze and then we can play again.

Let’s also add a color constant for our MESSAGE_COLOR. We’ll make it green.

1
MESSAGE_COLOR = (0, 255, 0)

You should be able to play it and see the win message appear when you reach an exit.

Finishing touches

Let’s finish up by adding some polish to our game. It would be nice to keep score and to also display the command keys for each player. Also you may notice that player 1 has a little easier time with the maze most of the time. This is because the maze_generator starts processing from the top-left corner so is usually far into the maze before it does any back-tracking. So in general player 2 will have more choices to make early on than player 1. We can alleviate this by alternating which corner the maze generator starts at each round.

Starting in our “init” method let’s add variables to store the current round and player scores.

1
2
3
4
self.player1_score = 0
self.player2_score = 0

self.round = 1

In the “generate_maze” method replace the following code:

1
2
process_stack = [(0, 0)]
self.maze[0] |= CellProp.Visited.value

with this code:

1
2
3
4
5
6
7
8
# Start at alternating corners to be more fair
process_stack = [(0, 0)]
if self.round % 2 == 0:
process_stack = [(MAZE_WIDTH - 1, MAZE_HEIGHT - 1)]
self.maze[CELL_COUNT - 1] |= CellProp.Visited.value
else:
process_stack = [(0, 0)]
self.maze[0] |= CellProp.Visited.value

In the “draw_screen” method, before the “display_update()” call add the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
font = pg.font.SysFont('Arial', 18, True)

# Display Scores
p1_msg = f'PLAYER 1: {self.player1_score}'
p2_msg = f'PLAYER 2: {self.player2_score}'
p1_size = font.size(p1_msg)
p2_size = font.size(p2_msg)
p1 = font.render(p1_msg, True, PLAYER1_COLOR)
p2 = font.render(p2_msg, True, PLAYER2_COLOR)
self.screen.blit(p1, (MAZE_TOP_LEFT_CORNER[0], MAZE_TOP_LEFT_CORNER[1] - p1_size[1]))
self.screen.blit(p2, (MAZE_TOP_LEFT_CORNER[0] + MAZE_WIDTH_PX - p2_size[0],
MAZE_TOP_LEFT_CORNER[1] - p1_size[1]))

# Display instructions
p1_msg = 'a w s d to move'
p2_msg = '← ↑ ↓ → to move'
p2_size = font.size(p2_msg)
p1 = font.render(p1_msg, True, PLAYER1_COLOR)
p2 = font.render(p2_msg, True, PLAYER2_COLOR)
self.screen.blit(p1, (MAZE_TOP_LEFT_CORNER[0], MAZE_TOP_LEFT_CORNER[1] + MAZE_HEIGHT_PX + 2))
self.screen.blit(p2, (MAZE_TOP_LEFT_CORNER[0] + MAZE_WIDTH_PX - p2_size[0],
MAZE_TOP_LEFT_CORNER[1] + MAZE_HEIGHT_PX + 2))

And finally in the “display_win” method right after we set the “msg” variable add the following:

1
2
3
4
5
self.round += 1
if self.win1_flag:
self.player1_score += 1
else:
self.player2_score += 1

Checkout more advanced topics in the next part:
Goto Part 6

Final code and code download

Download code: maze.zip

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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# Simple Maze Game Using The Recursive Back-Tracker Algorithm
from enum import Enum
import random
import pygame as pg


# Global Settings
SHOW_DRAW = False # Show the maze being created
SHOW_FPS = False # Show frames per second in caption
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 1024
# Maze Size: 36 X 30 is max size for screen of 1200 X 1024
MAZE_WIDTH = 36 # in cells
MAZE_HEIGHT = 30 # in cells
CELL_COUNT = MAZE_WIDTH * MAZE_HEIGHT
BLOCK_SIZE = 8 # Pixel size/Wall thickness
PATH_WIDTH = 3 # Width of pathway in blocks
CELL_SIZE = BLOCK_SIZE * PATH_WIDTH + BLOCK_SIZE # extra BLOCK_SIZE to include wall to east and south of cell
MAZE_WIDTH_PX = CELL_SIZE * MAZE_WIDTH + BLOCK_SIZE # extra BLOCK_SIZE to include left edge wall
MAZE_HEIGHT_PX = CELL_SIZE * MAZE_HEIGHT + BLOCK_SIZE # extra BLOCK_SIZE to include top edge wall
MAZE_TOP_LEFT_CORNER = (SCREEN_WIDTH // 2 - MAZE_WIDTH_PX // 2, SCREEN_HEIGHT // 2 - MAZE_HEIGHT_PX // 2)

# Define the colors we'll need
BACK_COLOR = (100, 100, 100)
WALL_COLOR = (18, 94, 32)
MAZE_COLOR = (255, 255, 255)
UNVISITED_COLOR = (0, 0, 0)
PLAYER1_COLOR = (255, 0, 0)
PLAYER2_COLOR = (0, 0, 255)
MESSAGE_COLOR = (0, 255, 0)


class CellProp(Enum):
Path_N = 1
Path_E = 2
Path_S = 4
Path_W = 8
Visited = 16


class Direction(Enum):
North = (0, -1)
East = (1, 0)
South = (0, 1)
West = (-1, 0)


class Player(pg.sprite.Sprite):
def __init__(self, color, x, y, radius):
# Call the parent class (Sprite) constructor
super().__init__()

# Save the start position
self.start_x = x
self.start_y = y

# Create the rectangular image, fill and set background to transparent
self.image = pg.Surface([radius * 2, radius * 2])
self.image.fill(MAZE_COLOR)
self.image.set_colorkey(MAZE_COLOR)

# Draw our player onto the transparent rectangle
pg.draw.circle(self.image, color, (radius, radius), radius)

self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y

def reset(self):
self.rect.x = self.start_x
self.rect.y = self.start_y


class MazeGenerator:
direction_to_flag = {
Direction.North: CellProp.Path_N,
Direction.East: CellProp.Path_E,
Direction.South: CellProp.Path_S,
Direction.West: CellProp.Path_W
}

opposite_direction = {
Direction.North: Direction.South,
Direction.East: Direction.West,
Direction.South: Direction.North,
Direction.West: Direction.East
}

def __init__(self):
# Need to initialize pygame before using it
pg.init()

# Create a display surface to draw our game on
self.screen = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

# Set the title on the window
pg.display.set_caption('PyMaze')

# Use a single list to store 2D array
self.maze = []

random.seed()

# Store maze as image after we create it so that we just have to redraw the image on update
self.maze_image = None

# Create players
self.player1 = Player(PLAYER1_COLOR, MAZE_TOP_LEFT_CORNER[0] + BLOCK_SIZE,
MAZE_TOP_LEFT_CORNER[1] + BLOCK_SIZE, (BLOCK_SIZE * 3) // 2)
self.player1_sprite = None

self.player2 = Player(PLAYER2_COLOR,
MAZE_TOP_LEFT_CORNER[0] + MAZE_WIDTH_PX - CELL_SIZE,
MAZE_TOP_LEFT_CORNER[1] + MAZE_HEIGHT_PX - CELL_SIZE,
(BLOCK_SIZE * 3) // 2)
self.player2_sprite = None

self.player1_score = 0
self.player2_score = 0

self.round = 1

self.win1_flag = False
self.win2_flag = False

def get_cell_index(self, position):
x, y = position
return y * MAZE_WIDTH + x

def generate_maze(self):
# Initialize maze with zero values
self.maze = [0] * CELL_COUNT
visited_count = 0

# Start at alternating corners to be more fair
process_stack = [(0, 0)]
if self.round % 2 == 0:
process_stack = [(MAZE_WIDTH - 1, MAZE_HEIGHT - 1)]
self.maze[CELL_COUNT - 1] |= CellProp.Visited.value
else:
process_stack = [(0, 0)]
self.maze[0] |= CellProp.Visited.value

visited_count += 1
while visited_count < CELL_COUNT:
# Step 1 - Create a list of the unvisited neighbors
x, y = process_stack[-1] # get position of top item on stack
current_cell_index = self.get_cell_index((x, y))

# Find all unvisited neighbors
neighbors = []
for direction in Direction:
dir = direction.value
new_x, new_y = (x + dir[0], y + dir[1])
if 0 <= new_x < MAZE_WIDTH and 0 <= new_y < MAZE_HEIGHT:
index = self.get_cell_index((new_x, new_y))
if not self.maze[index] & CellProp.Visited.value:
# Cell was not already visited so add to neighbors list with the direction
neighbors.append((new_x, new_y, direction))

if len(neighbors) > 0:
# Choose a random neighboring cell
cell = neighbors[random.randrange(len(neighbors))]
cell_x, cell_y, cell_direction = cell
cell_position = (cell_x, cell_y)
cell_index = self.get_cell_index(cell_position)

# Create a path between the neighbor and the current cell by setting the direction property flag
flag_to = MazeGenerator.direction_to_flag[cell_direction]
flag_from = MazeGenerator.direction_to_flag[MazeGenerator.opposite_direction[cell_direction]]

self.maze[current_cell_index] |= flag_to.value
self.maze[cell_index] |= flag_from.value | CellProp.Visited.value

process_stack.append(cell_position)
visited_count += 1
else:
# Backtrack since there were no unvisited neighbors
process_stack.pop()

if SHOW_DRAW:
self.draw_maze()
pg.display.update()
#pg.time.wait(500)
pg.event.pump()

# save image of completed maze
self.draw_maze()
pg.display.update()
self.maze_image = self.screen.copy()

def draw_maze(self):
self.screen.fill(BACK_COLOR)
pg.draw.rect(self.screen, WALL_COLOR, (MAZE_TOP_LEFT_CORNER[0], MAZE_TOP_LEFT_CORNER[1],
MAZE_WIDTH_PX, MAZE_HEIGHT_PX))

for x in range(MAZE_WIDTH):
for y in range(MAZE_HEIGHT):
for py in range(PATH_WIDTH):
for px in range(PATH_WIDTH):
cell_index = self.get_cell_index((x, y))
if self.maze[cell_index] & CellProp.Visited.value:
self.draw(MAZE_COLOR, x * (PATH_WIDTH + 1) + px, y * (PATH_WIDTH + 1) + py)
else:
self.draw(UNVISITED_COLOR, x * (PATH_WIDTH + 1) + px, y * (PATH_WIDTH + 1) + py)

for p in range(PATH_WIDTH):
if self.maze[y * MAZE_WIDTH + x] & CellProp.Path_S.value:
self.draw(MAZE_COLOR, x * (PATH_WIDTH + 1) + p, y * (PATH_WIDTH + 1) + PATH_WIDTH)

if self.maze[y * MAZE_WIDTH + x] & CellProp.Path_E.value:
self.draw(MAZE_COLOR, x * (PATH_WIDTH + 1) + PATH_WIDTH, y * (PATH_WIDTH + 1) + p)

# Color the player exits
pg.draw.rect(self.screen, PLAYER2_COLOR, (MAZE_TOP_LEFT_CORNER[0],
MAZE_TOP_LEFT_CORNER[1] + BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE * 3))

pg.draw.rect(self.screen, PLAYER1_COLOR,
(MAZE_TOP_LEFT_CORNER[0] + MAZE_WIDTH_PX - BLOCK_SIZE,
MAZE_TOP_LEFT_CORNER[1] + MAZE_HEIGHT_PX - BLOCK_SIZE * 4,
BLOCK_SIZE, BLOCK_SIZE * 3))

def draw(self, color, x, y):
x_offset = MAZE_TOP_LEFT_CORNER[0] + BLOCK_SIZE
y_offset = MAZE_TOP_LEFT_CORNER[1] + BLOCK_SIZE
pg.draw.rect(self.screen, color, (x * BLOCK_SIZE + x_offset,
y * BLOCK_SIZE + y_offset,
BLOCK_SIZE, BLOCK_SIZE))

def draw_screen(self):
self.screen.blit(self.maze_image, (0, 0))
self.player1_sprite.draw(self.screen)
self.player2_sprite.draw(self.screen)

font = pg.font.SysFont('Arial', 18, True)

# Display Scores
p1_msg = f'PLAYER 1: {self.player1_score}'
p2_msg = f'PLAYER 2: {self.player2_score}'
p1_size = font.size(p1_msg)
p2_size = font.size(p2_msg)
p1 = font.render(p1_msg, True, PLAYER1_COLOR)
p2 = font.render(p2_msg, True, PLAYER2_COLOR)
self.screen.blit(p1, (MAZE_TOP_LEFT_CORNER[0], MAZE_TOP_LEFT_CORNER[1] - p1_size[1]))
self.screen.blit(p2, (MAZE_TOP_LEFT_CORNER[0] + MAZE_WIDTH_PX - p2_size[0],
MAZE_TOP_LEFT_CORNER[1] - p1_size[1]))

# Display instructions
p1_msg = 'a w s d to move'
p2_msg = '← ↑ ↓ → to move'
p2_size = font.size(p2_msg)
p1 = font.render(p1_msg, True, PLAYER1_COLOR)
p2 = font.render(p2_msg, True, PLAYER2_COLOR)
self.screen.blit(p1, (MAZE_TOP_LEFT_CORNER[0], MAZE_TOP_LEFT_CORNER[1] + MAZE_HEIGHT_PX + 2))
self.screen.blit(p2, (MAZE_TOP_LEFT_CORNER[0] + MAZE_WIDTH_PX - p2_size[0],
MAZE_TOP_LEFT_CORNER[1] + MAZE_HEIGHT_PX + 2))

pg.display.update()

def display_win(self):
msg = 'Player 1 Wins!!!' if self.win1_flag else 'Player 2 Wins!!!'

self.round += 1
if self.win1_flag:
self.player1_score += 1
else:
self.player2_score += 1

font = pg.font.SysFont('Arial', 72, True)
size = font.size(msg)
s = font.render(msg, True, MESSAGE_COLOR, (0, 0, 0))
self.screen.blit(s, (SCREEN_WIDTH // 2 - size[0] // 2, SCREEN_HEIGHT // 2 - size[1] // 2))
pg.display.update()
pg.time.wait(3000)

def can_move(self, direction, player):
# Top left corner of first cell
corner_offset_x = MAZE_TOP_LEFT_CORNER[0] + BLOCK_SIZE
corner_offset_y = MAZE_TOP_LEFT_CORNER[1] + BLOCK_SIZE

# Calculate which cells the player occupies
square = BLOCK_SIZE * 4
p1 = (player.rect.x - corner_offset_x, player.rect.y - corner_offset_y)
p2 = (p1[0] + square - 1, p1[1] + square - 1)
player_pos1 = (p1[0] // square, p1[1] // square)
player_pos2 = (p2[0] // square, p2[1] // square)
cell_index1 = self.get_cell_index((player_pos1[0], player_pos1[1]))
cell_index2 = self.get_cell_index((player_pos2[0], player_pos2[1]))

functions = {
Direction.North: self.can_move_up,
Direction.East: self.can_move_right,
Direction.South: self.can_move_down,
Direction.West: self.can_move_left
}

# Check for maze exit/win
# Check if player is at opposing player's start x,y
if self.player1.rect.x == self.player2.start_x and self.player1.rect.y == self.player2.start_y:
self.win1_flag = True
elif self.player2.rect.x == self.player1.start_x and self.player2.rect.y == self.player1.start_y:
self.win2_flag = True

return functions[direction](cell_index1, cell_index2)

def can_move_up(self, index1, index2):
if index1 == index2:
return self.maze[index1] & CellProp.Path_N.value
else:
return index2 == index1 + MAZE_WIDTH

def can_move_right(self, index1, index2):
if index1 == index2:
return self.maze[index1] & CellProp.Path_E.value
else:
return index2 == index1 + 1

def can_move_down(self, index1, index2):
if index1 == index2:
return self.maze[index1] & CellProp.Path_S.value
else:
return index2 == index1 + MAZE_WIDTH

def can_move_left(self, index1, index2):
if index1 == index2:
return self.maze[index1] & CellProp.Path_W.value
else:
return index2 == index1 + 1

def move_up(self, player):
if self.can_move(Direction.North, player):
player.rect.y -= 1

def move_right(self, player):
if self.can_move(Direction.East, player):
player.rect.x += 1

def move_down(self, player):
if self.can_move(Direction.South, player):
player.rect.y += 1

def move_left(self, player):
if self.can_move(Direction.West, player):
player.rect.x -= 1

def initialize(self):
self.player1_sprite = None
self.player1.reset()
self.player2_sprite = None
self.player2.reset()

self.generate_maze()
self.player1_sprite = pg.sprite.RenderPlain(self.player1)
self.player2_sprite = pg.sprite.RenderPlain(self.player2)

def run_game(self):
clock = pg.time.Clock()
self.initialize()

# Main game loop
run = True
while run:
for event in pg.event.get():
if event.type == pg.QUIT:
run = False

if not self.win1_flag and not self.win2_flag:
keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.move_left(self.player2)
if keys[pg.K_RIGHT]:
self.move_right(self.player2)
if keys[pg.K_UP]:
self.move_up(self.player2)
if keys[pg.K_DOWN]:
self.move_down(self.player2)
if keys[pg.K_a]:
self.move_left(self.player1)
if keys[pg.K_d]:
self.move_right(self.player1)
if keys[pg.K_w]:
self.move_up(self.player1)
if keys[pg.K_s]:
self.move_down(self.player1)

if self.win1_flag or self.win2_flag:
self.display_win()
self.initialize()
self.win1_flag = self.win2_flag = False

self.draw_screen()

if SHOW_FPS:
pg.display.set_caption(f'PyMaze ({str(int(clock.get_fps()))} FPS)')
clock.tick()

pg.quit()


mg = MazeGenerator()
mg.run_game()