forked from Seitoh63/PySpaceInvaders
-
Notifications
You must be signed in to change notification settings - Fork 6
/
main.py
331 lines (245 loc) · 10.8 KB
/
main.py
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
import math
import random
import sys
import pygame
from alien import Aliens
from config import *
from decor import Ground, Barricades
from spaceship import Spaceship
from ui import Score, LifeCounter, HighScore, GameOver
class PySpaceInvaders:
def __init__(self):
# We create a surface in which sprites will be shown
self.window_surface = pygame.display.set_mode(WINDOW_SIZE)
# Variables for game loop
self.update_time_delay = 0
self.draw_time_delay = 0
# Game state variable
self.is_game_over = False
self.delay_since_game_over = 0
self.is_playing = True
# We create the game entities
self.spaceship = Spaceship()
self.aliens = Aliens()
self.ground = Ground()
self.barricades = Barricades()
self.score = Score()
self.high_score = HighScore()
self.life_counter = LifeCounter()
self.game_over = GameOver()
def play(self):
clock = pygame.time.Clock()
while True:
dt = clock.tick()
update_count = self._get_update_count(dt)
if update_count > 0:
# If game over for too long, reset the game
if self.is_game_over:
self.delay_since_game_over += update_count * UPDATE_PERIOD_MS
if self.delay_since_game_over > GAME_OVER_DURATION_S * 1000:
self._reset()
# This update the entities from the game
self._update(update_count * UPDATE_PERIOD_MS)
# Here, it's all the update that involves several entities, like collision
self._update_life_count()
self._collide()
frame_count = self._get_frame_count(dt)
if frame_count > 0:
self._draw()
def _update(self, dt):
# Getting input events
events = self._get_events()
# Updating each entity
self.spaceship.update(dt, events)
self.aliens.update(dt)
def _update_life_count(self):
if self.score.value // ONE_LIFE_SCORE > self.life_counter.life_gain_count:
self.life_counter.one_up()
if not self.spaceship.is_active:
if self.life_counter.life_count > 0:
self.life_counter.life_count -= 1
self.spaceship.reset()
else:
self._game_over()
def _get_update_count(self, dt):
# Incrementing the delay since previous update
self.update_time_delay += dt
# Count how many updates should be done. If more than one, a warning is shown
update_count = self.update_time_delay // UPDATE_PERIOD_MS
if update_count > 1:
print(str(update_count - 1) + " updates are late.")
self.update_time_delay = self.update_time_delay % UPDATE_PERIOD_MS
return update_count
def _get_events(self):
events = []
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
events.append(event)
return events
def _draw(self):
# First we clear everything from screen
self.window_surface.fill((0, 0, 0,))
# We draw each entity
self.ground.draw(self.window_surface)
self.barricades.draw(self.window_surface)
self.spaceship.draw(self.window_surface)
self.aliens.draw(self.window_surface)
self.score.draw(self.window_surface)
self.high_score.draw(self.window_surface)
self.life_counter.draw(self.window_surface)
if self.is_game_over:
self.game_over.draw(self.window_surface)
# We show the screen
pygame.display.flip()
def _get_frame_count(self, dt):
# Incrementing delay since previous frame
self.draw_time_delay += dt
# We count how many frames we should draw. If more than one, we show a warning.
frame_count = self.draw_time_delay // DRAW_PERIOD_MS
if frame_count > 1:
print("Skipping " + str(frame_count - 1) + " frames")
self.draw_time_delay = self.draw_time_delay % DRAW_PERIOD_MS
return frame_count
def _collide(self):
self._collide_missile_and_aliens()
self._collide_spaceship_and_aliens()
self._collide_spaceship_and_lasers()
self._collide_missile_and_lasers()
self._collide_missile_and_barricades()
self._collide_laser_and_barricades()
self._collide_alien_and_barricades()
self._collide_missile_and_saucer()
def _collide_missile_and_aliens(self):
# If no missile, no collision to check
if not self.spaceship.missile.is_active:
return
# Get rectangle from missile
missile_rect = self.spaceship.missile.rect
# Get each alien rectangle and check collision
for alien in self.aliens:
if missile_rect.colliderect(alien.rect):
# if collision, make the alien explode and remove missile
alien.explode()
self.spaceship.missile.is_active = False
# increase score
self.score.value += alien.type * 10
def _collide_missile_and_saucer(self):
# If no missile or no saucer
if not self.spaceship.missile.is_active or not self.aliens.saucer.is_active:
return
# Get rectangle from missile and saucer
missile_rect = self.spaceship.missile.rect
saucer_rect = self.aliens.saucer.rect
# if collision, make the saucer explode and remove missile
if missile_rect.colliderect(saucer_rect):
self.aliens.saucer.explode()
self.spaceship.missile.is_active = False
# increase score
self.score.value += 300
def _collide_spaceship_and_aliens(self):
# Get each alien rect and check collision with spaceship
for alien in self.aliens:
# If spaceship already destroyed, we stop.
if self.spaceship.is_destroyed:
return
if alien.rect.colliderect(self.spaceship.rect):
self.spaceship.destroy()
def _collide_spaceship_and_lasers(self):
# If spaceship already destroyed, we return
if self.spaceship.is_destroyed:
return
# Get each laser rectangle and spaceship rectangle
laser_rect_list = [laser.rect for laser in self.aliens.lasers]
spaceship_rect = self.spaceship.rect
if spaceship_rect.collidelist(laser_rect_list) != - 1:
self.spaceship.destroy()
def _collide_missile_and_lasers(self):
# If no missile, no collision to check
if not self.spaceship.missile.is_active:
return
# Get each laser rectangle and missile rectangle
laser_rect_list = [laser.rect for laser in self.aliens.lasers]
missile_rect = self.spaceship.missile.rect
# If collision, we remove both
laser_index = missile_rect.collidelist(laser_rect_list)
if laser_index != -1:
self.spaceship.missile.explode()
self.aliens.lasers[laser_index].explode()
def _collide_missile_and_barricades(self):
# If no missile, no collision to check
if not self.spaceship.missile.is_active:
return
# If collision, update barricade sprite and destroy missile
if self._collide_with_barricades(self.spaceship.missile, MISSILE_BARRICADE_EXPLOSION_RADIUS):
self.spaceship.missile.is_active = False
def _collide_laser_and_barricades(self):
# If collision, update barricade sprite and destroy laser
for laser in self.aliens.lasers:
if self._collide_with_barricades(laser, LASER_BARRICADE_EXPLOSION_RADIUS):
laser.explode()
def _collide_alien_and_barricades(self):
# If collision, update barricade sprite only, alien continue to live
for alien in self.aliens:
self._collide_with_barricades(alien, LASER_BARRICADE_EXPLOSION_RADIUS)
def _collide_with_barricades(self, shoot, radius):
for barricade in self.barricades:
# Find a colliding pixel
collision_point = self._find_colliding_pixel(shoot, barricade)
# Handle collision if there is one
if collision_point:
self._apply_explosion_on_mask(collision_point, radius, barricade)
self._build_sprite_from_mask(barricade)
return True
return False
def _find_colliding_pixel(self, shoot, barricade):
# get distance vector between top left of barricade and colliding entity
x, y = (shoot.rect.x, shoot.rect.y)
offset = (x - barricade.rect.x, y - barricade.rect.y)
# Using mask to get collision point
w, h = (shoot.rect.w, shoot.rect.h)
shoot_mask = pygame.Mask((w, h), fill=True)
return barricade.mask.overlap(shoot_mask, offset)
def _apply_explosion_on_mask(self, collision_point, radius, barricade):
# At collision point, remove pixels
cx, cy = collision_point
barricade.mask.set_at((cx, cy), 0)
# Loop on each pixel around collision point
for x in range(cx - radius, cx + radius + 1, 1):
for y in range(cy - radius, cy + radius + 1, 1):
# If not in barricade sprite, continue
if x < 0 or x >= barricade.rect.w or y < 0 or y >= barricade.rect.h:
continue
# if not in the circle around collision, continue
if math.sqrt((x - cx) ** 2 + (y - cy) ** 2) > radius:
continue
# We remove the pixel under a given probability
if random.random() < BARRICADE_DESTRUCTION_PROBABILITY:
barricade.mask.set_at((x, y), 0)
def _build_sprite_from_mask(self, barricade):
# create an surfarray and change pixel color according to mask
surf_array = pygame.surfarray.array3d(barricade.sprite)
for y in range(barricade.rect.h):
for x in range(barricade.rect.w):
if barricade.mask.get_at((x, y)) == 0:
surf_array[x, y] = (0, 0, 0)
# make sprite from surfarray.
barricade.sprite = pygame.surfarray.make_surface(surf_array)
def _game_over(self):
self.is_game_over = True
self.is_playing = False
if self.score.value > self.high_score.value:
self.high_score.value = self.score.value
def _reset(self):
self.spaceship = Spaceship()
self.aliens.reset()
self.barricades = Barricades()
self.score = Score()
self.life_counter = LifeCounter()
self.is_game_over = False
self.delay_since_game_over = 0
self.is_playing = True
if __name__ == "__main__":
pygame.init()
game = PySpaceInvaders()
game.play()