forked from assamidanov/python_programming_class
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmanager.py
More file actions
543 lines (445 loc) · 18.8 KB
/
manager.py
File metadata and controls
543 lines (445 loc) · 18.8 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
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
from cannon import MovingCannon, ArtificialCannon
from targets import TargetMaster
from color import Color
from artist import Artist
import threading
import pygame
import time
import random
class ScoreTable:
"""
A class that keeps track of the score the user manages to get
The score is determined by the difference of targets destroyed
and projectiles used (which are both maintained by this class as well).
This class also handles the font we're using to render text
Attributes
----------
targets_destroyed : int
The number of targets destroyed
projectiles_used : int
The number of projectiles used
font_name : str
The name of the font we're using (default dejavusansmono)
font_size : int
The font size (default 25)
font : pygame.font.Font
The font we're using
score: int
The number of targets destroyed
"""
def __init__(
self,
targets_destroyed: int = 0,
projectiles_used: int = 0,
font_name: str = "dejavusansmono",
font_size: int = 25):
"""Initializes the score table"""
self.targets_destroyed = targets_destroyed
self.projectiles_used = projectiles_used
self.font = pygame.font.SysFont(font_name, font_size)
@property
def score(self) -> int:
"""A property that calculates and returns the score"""
return self.targets_destroyed - self.projectiles_used
def draw(
self,
surface: pygame.Surface,
chosen_type: str = None,
health : int = None) -> None:
"""
Draws the score table by delegating to the Artist draw_score method
Parameters
----------
surface : pygame.Surface
The surface to draw the table onto
chosen_type : str
The currently chosen projectile type
health : int
The user's health
"""
Artist.draw_score(
surface,
self.font,
self.targets_destroyed,
self.projectiles_used,
self.score,
chosen_type,
health,
Color.RED,
Color.WHITE
)
def draw_game_over_screen(
self,
surface: pygame.Surface,
game_over_text: str) -> None:
"""
Draws the game over screen
Parameters
----------
surface : pygame.Surface
The surface to draw the game over screen onto
game_over_text : str
The text to display (Either "You Lose" or "You Win", for example)
"""
Artist.draw_death_screen(
surface,
self.font,
game_over_text,
self.score,
Color.RED
)
class Manager:
"""
The main Manager class that runs the entire project
Keeps track of the screen, pygame initialization, the game loop, and every
object used
Parameters
----------
num_targets : int
The initial number of targets to spawn (default 10)
num_cannons : int
The initial number of artifical cannons to spawn (default 3)
screen : pygame.Surface
The screen surface we draw everything onto
clock : pygame.Clock
The clock (keeps track of in-game ticks)
refresh_rate : int
The refresh rate of the game (default 15)
user_cannon : MovingCannon
The player object
artficial_cannons : list[ArtificialCannon]
A list of the artificial enemy cannons
target_master : TargetMaster
The controller of all the targets on the screen
bomb_spawning_thread : threading.Thread
A thread that handles periodic bomb spawning for all targets
"""
def __init__(
self,
num_targets: int = 10,
num_cannons: int = 3) -> None:
"""Initializes the Manager"""
self.screen_size = (800, 600)
self.init_pygame()
self.init_clock()
self.done = False
self.num_cannons = num_cannons
self.init_cannons()
self.score_t = ScoreTable()
self.num_targets = num_targets
self.bomb_spawning_thread = None
self.start_bomb_thread()
self.update_display()
def init_pygame(self) -> None:
"""Initalizes the Pygame module and the screen"""
pygame.display.init()
pygame.font.init()
self.screen = pygame.display.set_mode(self.screen_size)
pygame.display.set_caption("The Gun of Khiryanov II")
def update_display(self) -> None:
"""Updates the Pygame screen by calling display.flip()"""
pygame.display.flip()
def init_clock(self) -> None:
"""Initializes the Pygame clock and refresh rate"""
self.clock = pygame.time.Clock()
self.refresh_rate = 15
def init_cannons(self) -> None:
"""Intializes the user cannon, artificial cannons, and target_master"""
# Create the user cannon
self.user_cannon = MovingCannon(
x = 30,
y = self.screen_size[1]//2,
color = Color.LIGHT_BLUE
)
# Initializes an artifical cannon for each cannon we should have
self.artificial_cannons: list[ArtificialCannon] = []
for _ in range(self.num_cannons):
self.artificial_cannons.append(ArtificialCannon(
x = random.randint(self.screen_size[0]//2, self.screen_size[0] - 30),
y = random.randint(0, self.screen_size[1]),
v_x = random.randint(2, 5),
v_y = random.randint(2, 5),
color = Color.RED
))
self.target_master = TargetMaster()
def process_states(self) -> None:
"""Processes the entire game - an aspect of the main game loop"""
# Handle any inputs by the player
self.handle_events()
# Set the user and artificial cannon angles
self.handle_angles()
# Handle movement
self.handle_cannon_movement() # cannon
self.handle_target_movement() # targets
self.handle_projectile_movement() # projectiles
self.handle_bomb_movement() # bombs
# Handle collisions
self.handle_collisions()
# Handle dead objects
self.handle_exploded_bombs() # bombs
self.handle_dead_projectiles() # projectiles
# Handles new sets of target spawns
self.handle_new_missions()
# Draw everything to the screen
self.handle_drawing()
self.update_display()
def handle_angles(self) -> None:
"""
Handles the angle of the user and artificial cannon
Sets the user cannon's angle to the mouse position, and the artificial
cannon angle to the user position
"""
# Get mouse position and set angle
if pygame.mouse.get_focused():
mouse_pos = pygame.mouse.get_pos()
self.user_cannon.set_angle(*mouse_pos)
# Set each artificial cannon's angle to the user
for artificial_cannon in self.artificial_cannons:
artificial_cannon.set_angle(self.user_cannon.x, self.user_cannon.y)
def handle_cannon_movement(self) -> None:
"""Handle artificial and user cannon movement and type switching"""
# Which key corresponds to what movement
key_to_move = {
pygame.K_LEFT: self.user_cannon.move_left,
pygame.K_RIGHT: self.user_cannon.move_right,
pygame.K_UP: self.user_cannon.move_up,
pygame.K_DOWN: self.user_cannon.move_down,
pygame.K_a: self.user_cannon.move_left,
pygame.K_d: self.user_cannon.move_right,
pygame.K_w: self.user_cannon.move_up,
pygame.K_s: self.user_cannon.move_down
}
# Which key corresponds to what type of projectile
type_switcher = {
pygame.K_1: 's',
pygame.K_2: 'c',
pygame.K_3: 't'
}
# Move depending on the move key
keys_pressed = pygame.key.get_pressed()
for key, move_func in key_to_move.items():
if keys_pressed[key]:
move_func(self.screen_size)
# Switch depending on the switch key
for key, chosen_type in type_switcher.items():
if keys_pressed[key]:
self.user_cannon.change_chosen(chosen_type)
# Check if the user cannon should be gaining power
self.user_cannon.gain()
# Starts or ends the thread depending on if the artificial cannon
# is within range (or spawn targets if it isn't)
for artificial_cannon in self.artificial_cannons:
if artificial_cannon.determine_move(
self.user_cannon,
self.screen_size
):
artificial_cannon.end_thread()
else:
artificial_cannon.start_thread()
artificial_cannon.determine_target_spawning(
self.target_master, self.score_t.score, 0.01
)
def handle_target_movement(self) -> None:
"""Handles the movement of all the targets"""
self.target_master.move_all(self.screen_size)
def handle_projectile_movement(self) -> None:
"""Handles the movement of all the projectiles"""
self.user_cannon.projectile_master.move_all(self.screen_size)
for artificial_cannon in self.artificial_cannons:
artificial_cannon.projectile_master.move_all(self.screen_size)
def handle_dead_projectiles(self) -> None:
"""Removes dead projectiles from the screen"""
self.user_cannon.projectile_master.remove_dead()
for artificial_cannon in self.artificial_cannons:
artificial_cannon.projectile_master.remove_dead()
def handle_bomb_movement(self) -> None:
"""Handles the movement of all the bombs"""
for target in self.target_master.target_list:
target.bomb_master.move_all()
def handle_exploded_bombs(self) -> None:
"""Removes dead bombs from the screen"""
for target in self.target_master.target_list:
target.bomb_master.remove_exploded(
self.screen_size[1],
self.user_cannon
)
def handle_collisions(self) -> None:
"""Handles target and user collisions by delagating to the respective function"""
self.handle_target_collisions()
self.handle_user_collision()
self.handle_artificial_collision()
def handle_target_collisions(self) -> None:
"""
Handles target collisions by checking if any projectile collided
with any target. The objects must agree on shape type
"""
for projectile in self.user_cannon.projectile_master.projectile_list:
for target in self.target_master.target_list:
if target.check_collision(projectile):
if target.shape == projectile.shape:
self.target_master.target_list.remove(target)
self.score_t.targets_destroyed += 1
def handle_user_collision(self) -> None:
"""
Handles user collisions by checking if any artificial projectile
collided with the user
"""
for artificial_cannon in self.artificial_cannons:
for projectile in artificial_cannon.projectile_master.projectile_list:
if self.user_cannon.check_collision(projectile):
self.user_cannon.deal()
artificial_cannon.projectile_master.projectile_list.remove(projectile)
def handle_artificial_collision(self) -> None:
"""
Handles artificial cannon collisions by checking if any user
projectiles collided with the artificial cannon
"""
for artificial_cannon in self.artificial_cannons:
for projectile in self.user_cannon.projectile_master.projectile_list:
if artificial_cannon.check_collision(projectile):
# If a projectile hits an enemy cannon, don't count it
self.score_t.projectiles_used -= 1
artificial_cannon.deal()
if not artificial_cannon.is_alive:
# ac counts as 5 targets
self.score_t.targets_destroyed += 5
self.artificial_cannons.remove(artificial_cannon)
self.user_cannon.projectile_master.projectile_list.remove(projectile)
def handle_drawing(self) -> None:
"""Handles drawing all the objects"""
# Fills the background color
self.screen.fill(Color.BLACK)
self.draw_projectiles()
self.draw_targets()
self.draw_cannons()
self.draw_bombs()
self.draw_score()
def draw_projectiles(self) -> None:
"""Draws every projectile"""
self.user_cannon.projectile_master.draw_all(self.screen)
for artificial_cannon in self.artificial_cannons:
artificial_cannon.projectile_master.draw_all(self.screen)
def draw_targets(self) -> None:
"""Draws every target"""
self.target_master.draw_all(self.screen)
def draw_cannons(self) -> None:
"""Draws every cannon"""
self.user_cannon.draw(self.screen)
for artificial_cannon in self.artificial_cannons:
artificial_cannon.draw(self.screen)
def draw_bombs(self) -> None:
"""Draws every bomb"""
for target in self.target_master.target_list:
target.bomb_master.draw_all(self.screen)
def draw_score(self) -> None:
"""Draws the score table"""
self.score_t.draw(
self.screen,
self.user_cannon.chosen_type,
self.user_cannon.health
)
def handle_events(self) -> None:
"""Handles Pygame events"""
for event in pygame.event.get():
# If the user quits
if event.type == pygame.QUIT:
self.done = True
# If they press the left mouse button
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
self.user_cannon.activate()
# If they stop pressing the left mouse button
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
self.user_cannon.strike()
self.score_t.projectiles_used += 1
def handle_new_missions(self) -> None:
"""Creates a new set of targets if the user killed all of them"""
if not self.target_master.target_list:
if not self.user_cannon.projectile_master.projectile_list:
self.create_mission()
def create_mission(self) -> None:
"""Creates a num_targets amount of random targets"""
for _ in range(self.num_targets):
self.target_master.create_random_target(
self.screen_size,
self.target_master.calculate_target_size(self.score_t.score),
)
def start_bomb_thread(self) -> None:
"""Starts the bomb_spawning_thread"""
if not self.bomb_spawning_thread:
self.bomb_spawning_thread = threading.Thread(
target=self.spawn_bombs, daemon=True
)
self.bomb_spawning_thread.start()
def spawn_bombs(self, delay = 0.5, stagger = 0.1, chance = 0.8):
"""
Spawn bombs depending on the delay, stagger, and chance
Parameters
----------
delay : float
The delay to wait between bomb dropping checks (default 0.5)
stagger : float
The delay to wait between each target dropping their bombs
This is to prevent all the bombs from getting dropped at the
same time (default 0.1)
chance : float
The decimal chance of a target dropping a bomb on a given tick
"""
# While the bomb_spawning_thread is active
while self.bomb_spawning_thread:
# Wait for the delay
time.sleep(delay)
# If it is still active
if self.bomb_spawning_thread:
# Randomize which target we're dropping bombs from
random.shuffle(self.target_master.target_list)
for target in self.target_master.target_list:
# Stagger bomb drops so they don't all come out at the
# same time
time.sleep(stagger)
# Create a bomb with the given chance
target.bomb_master.create_bomb(
target.x, target.y + target.size, 1, chance
)
def end_bomb_thread(self):
"""Ends the bomb_spawning_thread by resetting it to None"""
self.bomb_spawning_thread = None
def game_loop(self):
"""Keep playing until the user ends the game"""
while not self.done:
self.clock.tick(self.refresh_rate)
self.process_states()
self.check_game_over()
self.game_over_loop()
def check_game_over(self) -> None:
"""Check if the game should be over and set self.done respectively"""
if not self.user_cannon.is_alive or self.check_ac_death():
self.done = True
def check_ac_death(self) -> bool:
"""
Check if all the artificial cannons are dead
Returns
-------
ac_death : bool
Whether or not all the artificial tanks are dead
"""
return all([not ac.is_alive for ac in self.artificial_cannons])
def game_over_loop(self) -> None:
"""Continuously the game over screen"""
show_game_over = True
while show_game_over:
# If the user died, display You Died
if not self.user_cannon.is_alive:
self.score_t.draw_game_over_screen(self.screen, "You Died!")
# If the user won, display You Won
elif self.check_ac_death():
self.score_t.draw_game_over_screen(self.screen, "You Won!")
else:
show_game_over = False
# Check for key press to exit
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
show_game_over = False
self.update_display()
pygame.quit()