-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalcat.py
More file actions
511 lines (431 loc) · 18.5 KB
/
Copy pathalcat.py
File metadata and controls
511 lines (431 loc) · 18.5 KB
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
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
import turtle
import random
import time
import pygame.mixer as mixer
# Game code created in this class
class Game:
def __init__(self):
"""Initialize game settings, assets, and state variables.
Sets up screen dimensions, game constants, loads sounds and images,
and initializes the game state."""
# Game constants
self.SCREEN_SIZE = 1024
self.MAP_SIZE = 25
self.TILE_SIZE = 32
self.NUM_ENEMIES = 5
self.EXTRA_PATH_CHANCE = 0.8
self.PACMAN_SPEED = self.TILE_SIZE
self.ANIMATION_DELAY = 100 # milliseconds
self.SCREEN_UPDATE_DELAY = 1/10
# Game state
self.game_running = True
self.intro_displayed = True
self.current_frame = 0
self.enemy_move_counter = 0
self.enemy_move_delay = 3
# Initialize map_layout
self.map_layout = [[1] * self.MAP_SIZE for _ in range(self.MAP_SIZE)]
# Setup screen
self.screen = self.setup_screen()
self.load_assets()
# Game objects
self.walls = []
self.dots = []
self.enemies = []
self.pacman = None
# Initialize pygame mixer
mixer.init()
# Update sound file handling
self.pop_sound = mixer.Sound('popsfx.mp3')
self.death_sound = mixer.Sound('deathsfx.mp3')
self.bg_music = 'bgmusic.mp3'
self.win_sound = mixer.Sound('winsfx.mp3')
# Load and start background music
mixer.music.load(self.bg_music)
mixer.music.play(-1) # -1 means loop indefinitely
def setup_screen(self):
"""Configure the game window and basic display settings.
Returns: turtle.Screen - The configured game screen"""
screen = turtle.Screen()
screen.setup(width=self.SCREEN_SIZE, height=self.SCREEN_SIZE)
screen.title("PYTHON EXHIBITION GAME")
screen.bgpic("bgimg.gif")
screen.tracer(0)
return screen
def load_assets(self):
"""Load all game images (GIFs) into the turtle screen.
Includes background, characters, dots, and UI elements."""
self.bgpic = ["bgimg.gif"]
self.pacman_frames = ["popcat1.gif", "popcat2.gif"]
self.dot_frames = ["dot1.gif", "dot2.gif"]
self.enemy_frames = ["whitedog1.gif", "blackdog1.gif", "browndog1.gif", "yellowdog1.gif","graydog1.gif"]
self.end_screen_assets = ["tryagain1.gif", "youwin.gif", "gameover.gif"]
for frame in self.pacman_frames:
self.screen.addshape(frame)
for frame in self.dot_frames:
self.screen.addshape(frame)
for frame in self.bgpic:
self.screen.addshape(frame)
for frame in self.enemy_frames:
self.screen.addshape(frame)
for frame in self.end_screen_assets:
self.screen.addshape(frame)
def setup_controls(self):
"""Bind keyboard controls for Pacman movement.
Arrow keys control direction: Up, Down, Left, Right"""
self.screen.listen()
self.screen.onkeypress(lambda: self.set_pacman_direction("left"), "Left")
self.screen.onkeypress(lambda: self.set_pacman_direction("right"), "Right")
self.screen.onkeypress(lambda: self.set_pacman_direction("up"), "Up")
self.screen.onkeypress(lambda: self.set_pacman_direction("down"), "Down")
def set_pacman_direction(self, direction):
"""Update Pacman's temporary direction based on key press.
Args:
direction (str): The direction to move ('up', 'down', 'left', 'right')"""
if self.game_running:
self.pacman.tempdirection = direction
def show_intro(self):
"""Display the game's intro screen with logo and start button.
Waits for player input (click/keypress) to start the game."""
intro = turtle.Turtle()
intro.hideturtle()
intro.penup()
intro.goto(0,0)
self.screen.addshape("logo.gif")
intro.shape("logo.gif")
intro.showturtle()
start_btn = turtle.Turtle()
start_btn.hideturtle()
start_btn.penup()
start_btn.goto(0, -150)
self.screen.addshape("clicktostart.gif")
start_btn.shape("clicktostart.gif")
start_btn.showturtle()
def start_game(*args):
intro.hideturtle()
start_btn.hideturtle()
self.screen.bgpic("nopic")
self.screen.bgcolor("#D1EFFF")
self.intro_displayed = False
self.screen.onclick(start_game)
self.screen.onkey(start_game, "Up")
self.screen.onkey(start_game, "space")
self.screen.listen()
def generate_maze(self):
"""Generate a random maze using a depth-first search algorithm with a stack.
The algorithm works as follows:
1. Start with a grid full of walls (1's)
2. Begin at cell (1,1) and mark it as path (0)
3. Using a stack to track the current path:
- Look for unvisited neighbors 2 cells away (to create corridors)
- If found, knock down walls between cells and move to new cell
- If no unvisited neighbors, backtrack using the stack
4. Add random additional paths for better connectivity
5. Place dots (2's) in some of the open spaces
The stack implementation ensures a fully connected maze by keeping
track of the path taken and allowing backtracking when needed."""
# Initialize maze with walls
self.map_layout = [[1] * self.MAP_SIZE for _ in range(self.MAP_SIZE)]
# Possible movements: right, down, left, up (moving 2 cells at a time)
directions = [(0, 2), (2, 0), (0, -2), (-2, 0)]
# Stack stores (x,y) coordinates of the current path
stack = [(1, 1)]
visited = set(stack)
while stack:
x, y = stack[-1] # Get current position from top of stack
self.map_layout[y][x] = 0 # Mark current cell as path
# Randomize direction choices for maze variety
random.shuffle(directions)
found_unvisited = False
# Try each direction to find unvisited cells
for dx, dy in directions:
nx, ny = x + dx, y + dy # Calculate new position
if (1 <= nx < self.MAP_SIZE - 1 and
1 <= ny < self.MAP_SIZE - 1 and
(nx, ny) not in visited):
# Create path by removing walls
self.map_layout[y + dy // 2][x + dx // 2] = 0 # Remove wall
self.map_layout[ny][nx] = 0 # Mark new cell as path
visited.add((nx, ny))
stack.append((nx, ny)) # Add new position to stack
found_unvisited = True
break
# Backtrack if no unvisited neighbors found
if not found_unvisited:
stack.pop()
# Randomly add extra paths for better connectivity
if random.random() < self.EXTRA_PATH_CHANCE:
for dx, dy in directions:
nx, ny = x + dx, y + dy
if (1 <= nx < self.MAP_SIZE - 1 and
1 <= ny < self.MAP_SIZE - 1 and
self.map_layout[ny][nx] == 1):
# Create additional path
self.map_layout[y + dy // 2][x + dx // 2] = 0
self.map_layout[ny][nx] = 0
break
# Add collectible dots to the maze
for y in range(self.MAP_SIZE):
for x in range(self.MAP_SIZE):
if (self.map_layout[y][x] == 0 and
x > 1 and y > 1 and
random.random() < 0.6):
self.map_layout[y][x] = 2
def create_enemy(self, n):
"""Create and position an enemy character.
Args:
n (int): Index for enemy sprite selection
Returns:
turtle.Turtle: Configured enemy sprite"""
ghost = turtle.Turtle()
ghost.shape(self.enemy_frames[n])
ghost.shapesize(0.5, 0.5)
ghost.color("#8E6BE6")
ghost.penup()
ghost.speed(0)
while True:
x = random.randint(4, self.MAP_SIZE - 2)
y = random.randint(4, self.MAP_SIZE - 2)
if self.map_layout[y][x] == 0:
screen_x = -384 + (x * self.TILE_SIZE)
screen_y = 384 - (y * self.TILE_SIZE)
ghost.goto(screen_x, screen_y)
break
return ghost
def create_pacman(self):
"""Create and configure the player character (Pacman).
Returns:
turtle.Turtle: Configured Pacman sprite"""
pacman = turtle.Turtle()
pacman.shape(self.pacman_frames[0])
pacman.penup()
pacman.speed(0)
pacman.goto(-352, 352)
pacman.direction = "Stop"
pacman.tempdirection = "Stop"
return pacman
def draw_map(self):
"""Convert the maze array into visible walls and dots.
Creates turtle objects for each maze element."""
for y in range(self.MAP_SIZE):
for x in range(self.MAP_SIZE):
screen_x = -384 + (x * self.TILE_SIZE)
screen_y = 384 - (y * self.TILE_SIZE)
if self.map_layout[y][x] == 1: # Wall
wall = turtle.Turtle()
wall.shape("square")
wall.color("#93c1ff")
wall.shapesize(stretch_wid=(self.TILE_SIZE-4)/20,
stretch_len=(self.TILE_SIZE-4)/20)
wall.penup()
wall.goto(screen_x, screen_y)
self.walls.append(wall)
elif self.map_layout[y][x] == 2: # Dot
dot = turtle.Turtle()
dot.shape(random.choice(["dot1.gif", "dot2.gif"]))
dot.color("#FE696C")
dot.shapesize(0.5, 0.5)
dot.penup()
dot.goto(screen_x, screen_y)
self.dots.append(dot)
def collision_with_wall(self, x, y):
"""Check if a position collides with any wall.
Args:
x (float): X coordinate to check
y (float): Y coordinate to check
Returns:
bool: True if collision detected, False otherwise"""
for wall in self.walls:
if wall.distance(x, y) < self.TILE_SIZE:
return True
return False
def play_sound(self, sound):
"""Play a given sound effect.
Args:
sound (pygame.mixer.Sound): Sound effect to play"""
sound.play()
def check_dot_collisions(self):
"""Check and handle Pacman collisions with dots.
Removes collected dots and plays sound effect."""
for dot in self.dots[:]:
if self.pacman.distance(dot) < 22:
dot.hideturtle()
self.play_sound(self.pop_sound)
self.dots.remove(dot)
def move_enemies(self):
"""Update enemy positions using random movement patterns.
Includes movement delay for game balance."""
self.enemy_move_counter += 1
if self.enemy_move_counter < self.enemy_move_delay:
return
self.enemy_move_counter = 0
for ghost in self.enemies:
possible_directions = [
(ghost.xcor() + self.TILE_SIZE, ghost.ycor()),
(ghost.xcor() - self.TILE_SIZE, ghost.ycor()),
(ghost.xcor(), ghost.ycor() + self.TILE_SIZE),
(ghost.xcor(), ghost.ycor() - self.TILE_SIZE)
]
random.shuffle(possible_directions)
for x, y in possible_directions:
if not self.collision_with_wall(x, y):
ghost.goto(x, y)
break
def move_pacman(self):
"""Update Pacman's position based on current direction.
Handles wall collisions and movement constraints."""
if not self.game_running:
return
x, y = self.pacman.xcor(), self.pacman.ycor()
if self.pacman.tempdirection == "left":
x -= self.PACMAN_SPEED
elif self.pacman.tempdirection == "right":
x += self.PACMAN_SPEED
elif self.pacman.tempdirection == "up":
y += self.PACMAN_SPEED
elif self.pacman.tempdirection == "down":
y -= self.PACMAN_SPEED
elif self.pacman.tempdirection == "Stop":
self.pacman.direction = "Stop"
return
if not self.collision_with_wall(x, y):
self.pacman.goto(x, y)
self.pacman.direction = self.pacman.tempdirection
else:
self.pacman.direction = "Stop" # Stop if we hit a wall
def animate_pacman(self):
"""Handle Pacman's sprite animation.
Changes frames based on movement state."""
if self.game_running:
# Only animate if Pacman is moving
if self.pacman.direction != "Stop":
self.current_frame = (self.current_frame + 1) % len(self.pacman_frames)
else:
self.current_frame = 0 # Reset to first frame when stopped
self.pacman.shape(self.pacman_frames[self.current_frame])
self.screen.ontimer(self.animate_pacman, self.ANIMATION_DELAY)
def check_game_over(self):
"""Check for win/lose conditions.
Returns:
bool: True if game is over, False if still playing"""
for ghost in self.enemies:
if self.pacman.distance(ghost) < 22:
self.play_sound(self.death_sound)
time.sleep(1.7)
self.show_end_screen("Game Over!")
return True
if len(self.dots) == 0:
time.sleep(1.7)
self.play_sound(self.win_sound)
self.show_end_screen("You Win!")
return True
return False
def show_end_screen(self, message):
"""Display game over screen with win/lose message.
Args:
message (str): "You Win!" or "Game Over!" message to display"""
self.game_running = False
# Clear existing objects but don't clear screen
for dot in self.dots:
dot.hideturtle()
for wall in self.walls:
wall.hideturtle()
for enemy in self.enemies:
enemy.hideturtle()
self.pacman.hideturtle()
self.screen.bgpic("bgimg.gif")
# Create end message turtle
end_message = turtle.Turtle()
end_message.hideturtle()
end_message.penup()
# Show appropriate GIF based on message
if message == "You Win!":
end_message.goto(40, 0)
end_message.shape("youwin.gif")
elif message == "Game Over!":
end_message.goto(25, -100)
end_message.shape("gameover.gif")
end_message.showturtle()
# Create restart button with GIF
restart_button = turtle.Turtle()
restart_button.penup()
restart_button.goto(10, -150)
restart_button.shape("tryagain1.gif")
restart_button.showturtle()
def restart_game(x,y):
if self.game_running == False:
end_message.hideturtle()
restart_button.hideturtle()
self.reset_game()
self.screen.onclick(restart_game)
self.screen.onkey(restart_game, "space")
self.screen.listen()
def reset_game(self):
"""Reset all game elements to start a new game.
Clears current game state and regenerates maze."""
# Reset game state
self.game_running = True
self.screen.bgpic("nopic")
# Clear existing objects
for dot in self.dots:
dot.hideturtle()
for wall in self.walls:
wall.hideturtle()
for enemy in self.enemies:
enemy.hideturtle()
if self.pacman:
self.pacman.hideturtle()
# Reset lists
self.walls.clear()
self.dots.clear()
self.enemies.clear()
# Regenerate maze and game objects
self.generate_maze()
self.draw_map()
self.pacman = self.create_pacman()
self.setup_controls()
# Create new enemies
for _ in range(self.NUM_ENEMIES):
n = _ - 1
self.enemies.append(self.create_enemy(n))
# Reset movement-related counters
self.enemy_move_counter = 0
self.current_frame = 0
# Start animation before setting game state
self.animate_pacman()
# Reset screen update rate
self.screen.delay(0) # Ensure screen delay is consistent
self.screen.tracer(0) # Ensure tracer is off
def run(self):
"""Main game loop. Handles intro sequence and game execution."""
self.show_intro()
while self.intro_displayed:
self.screen.update()
time.sleep(0.01)
self.generate_maze()
self.draw_map()
self.pacman = self.create_pacman()
self.setup_controls()
for _ in range(self.NUM_ENEMIES):
n = _ - 1
self.enemies.append(self.create_enemy(n))
self.animate_pacman()
# Main game loop
while True:
try:
if self.game_running:
self.move_enemies()
self.move_pacman()
self.check_dot_collisions()
if self.check_game_over():
continue
self.screen.update()
# Move screen update outside the game_running check
# to maintain consistent frame rate even during transitions
self.screen.update()
time.sleep(self.SCREEN_UPDATE_DELAY)
except turtle.Terminator:
break
if __name__ == "__main__":
game = Game()
game.run()