-
Notifications
You must be signed in to change notification settings - Fork 0
/
gameplay.py
324 lines (301 loc) · 13.2 KB
/
gameplay.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
from level import Level
from enemies import Enemies
from spaceship import Spaceship
from rocket import Rocket
from direction import Direction
from const import Const
from random import randint
from player import Player
from time import perf_counter as clock
import pygame
import enum
class Gameplay:
def __init__(self, space, screen):
self.space = space
self.level = 0
self.levels = []
self.screen = screen
self.level_initialized = False
self.spaceship = Spaceship(x=Const.INITIAL_X_POS, y=Const.INITIAL_Y_POS, screen=self.screen)
self.enemies = Enemies(self)
self.rocket_left = Rocket(spaceship=self.spaceship, side=Direction.left)
self.rocket_right = Rocket(spaceship=self.spaceship, side=Direction.right)
self.rockets = [self.rocket_left, self.rocket_right]
self.player = Player(self.screen)
self.invaders_in_place = False
self.num_of_levels = 10
self.__setup_levels()
self.initialize_level()
self.level_font = pygame.font.Font("res/PixelEmulator-xq08.ttf", 24)
self.notification_time = 0
self.blinking_period = 0
self.blinking_time = 0
self.blink = False
self.first_blink = False
self.level_notification = False
self.death_notification = False
self.spaceship_state = self.SpaceshipState.normal
class Probability(enum.Enum):
very_low = 100
low = 80
medium = 60
high = 40
very_high = 20
class SpaceshipState(enum.Enum):
normal = 0
hit = 1
blinking = 2
def __setup_levels(self):
"""
Prepare all levels with their data
"""
for i in range(self.num_of_levels):
self.levels.append(Level())
# Level one
self.levels[0].set_invader_locations(Gameplay.__layout_invaders(1, 5))
self.levels[0].set_asteroids_probability(self.Probability.very_low)
# Level two
self.levels[1].set_invader_locations(Gameplay.__layout_invaders(1, 6))
self.levels[1].set_asteroids_probability(self.Probability.very_low)
# Level three
self.levels[2].set_invader_locations(Gameplay.__layout_invaders(2, 5))
self.levels[2].set_asteroids_probability(self.Probability.low)
# Level four
self.levels[3].set_invader_locations(Gameplay.__layout_invaders(2, 6))
self.levels[3].set_asteroids_probability(self.Probability.low)
# Level five
self.levels[4].set_invader_locations(Gameplay.__layout_invaders(3, 5))
self.levels[4].set_asteroids_probability(self.Probability.medium)
# Level six
self.levels[5].set_invader_locations(Gameplay.__layout_invaders(3, 6))
self.levels[5].set_asteroids_probability(self.Probability.medium)
# Level seven
self.levels[6].set_invader_locations(Gameplay.__layout_invaders(4, 5))
self.levels[6].set_asteroids_probability(self.Probability.high)
# Level eight
self.levels[7].set_invader_locations(Gameplay.__layout_invaders(4, 6))
self.levels[7].set_asteroids_probability(self.Probability.high)
# Level nine
self.levels[8].set_invader_locations(Gameplay.__layout_invaders(5, 5))
self.levels[8].set_asteroids_probability(self.Probability.very_high)
# Level ten
self.levels[9].set_invader_locations(Gameplay.__layout_invaders(5, 6))
self.levels[9].set_asteroids_probability(self.Probability.very_high)
@staticmethod
def __layout_invaders(lines, columns):
"""
Arrange the invaders on the screen
"""
invaders_locations = []
nominal_shift = Const.INVADER_SIZE
if lines == 1:
additional_shift = 0
else:
additional_shift = Const.INVADER_SIZE
initial_shift = ((Const.INVADER_RIGHT_BORDER + Const.INVADER_SIZE - Const.INVADER_LEFT_BORDER) -
(Const.INVADER_SIZE * 1.7 * (columns - 1) + nominal_shift + additional_shift)) / 2
for line in range(lines):
shift = (line % 2) * nominal_shift
for column in range(columns):
invaders_locations.append((initial_shift + Const.INVADER_SIZE * 1.7 * column + shift,
-Const.INVADER_SIZE - Const.INVADER_SIZE * 1.2 * line))
return invaders_locations
def current_level(self):
"""
Return the current level object
"""
return self.levels[self.level]
def add_score(self, enemy):
"""
Add a score for hitting an enemy
"""
# The farther the enemy was from the player, the more score he gets for shooting it
self.player.add_score(enemy.score + int((Const.SCREEN_HEIGHT - enemy.get_xy()[1]) / 10))
def initialize_level(self):
"""
Initialize a new level
"""
if self.level == self.num_of_levels:
# TODO: this is a very sad ending...
self.space.quit()
if not self.level_initialized:
self.level_initialized = True
self.invaders_in_place = False
for invader_location in self.current_level().get_invader_locations():
self.enemies.add_invader(x=invader_location[0],
y=invader_location[1],
speed=1)
self.level_notification = True
self.notification_time = clock() + Const.NOTIFICATION_TIME
def end_level(self):
"""
Returns true if the current level ended (no invaders left alive)
"""
if self.enemies.current_number_of_invaders() > 0:
return False
return True
def next_level(self):
"""
Switch to the next level
"""
if self.level < self.num_of_levels:
self.level += 1
self.invaders_in_place = False
self.initialize_level()
def spaceship_was_hit(self):
self.spaceship.hit()
self.death_notification = True
self.notification_time = clock() + Const.NOTIFICATION_TIME
self.player.hit()
self.spaceship_state = self.SpaceshipState.hit
if self.player.get_lives() == 0:
# The game will end when there are 0 lives left
# TODO: this is a very sad ending...
self.space.quit()
def check_hits(self):
"""
Check if a rocket or a spaceship hit something
"""
for enemy in self.enemies.get_enemies():
# Spaceship
if self.spaceship_state == self.SpaceshipState.normal and \
pygame.Rect(self.spaceship.hitbox).colliderect(enemy.hitbox):
if not enemy.is_hit():
enemy.hit()
# Spaceship death. It can be hit by an exploding enemy as well.
self.spaceship_was_hit()
# Rocket
for rocket in self.rockets:
if rocket.is_launched():
if pygame.Rect(rocket.hitbox).colliderect(enemy.hitbox):
rocket.gone()
enemy.hit()
self.add_score(enemy)
def spaceship_post_explosion(self):
"""
This is what happens to the spaceship after it explodes
"""
self.spaceship_state = self.SpaceshipState.blinking
# Since during the explosion some basic parameters are changed, need to reinitialize
self.spaceship.reinitialize()
self.blinking_period = clock() + Const.BLINKING_PERIOD
self.blinking_time = clock() + Const.BLINKING_PERIOD / 3 # Just some longer outage for the first blink
# The first blink will be longer, i.e. the spaceship will disappear for a while after explosion
self.first_blink = True
self.blink = True
def run(self):
"""
Game running logic
"""
# Enemies
# Invaders may be still entering the screen
if not self.invaders_in_place:
if self.enemies.all_invaders_appeared():
self.invaders_in_place = True
self.enemies.invaders_arrived()
# Add a random asteroid
if randint(0, self.current_level().get_asteroids_probability().value) == 0:
self.enemies.add_asteroid()
# Move all enemies
self.enemies.move()
# Spaceship and rockets
# Calculate the next locations of everything
self.spaceship.move()
self.rocket_left.move()
self.rocket_right.move()
# Check if something was hit
self.check_hits()
if self.spaceship_state == self.SpaceshipState.hit and not self.spaceship.is_hit():
# The spaceship was hit and finished exploding
self.spaceship_post_explosion()
# Game
if self.end_level():
self.next_level()
def handle_events(self):
"""
Handle all game events, mostly keypress
"""
for event in pygame.event.get():
# Exit event
if event.type == pygame.QUIT:
self.space.quit()
# Respond to keys
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.space.quit()
if self.spaceship_state == self.SpaceshipState.normal:
# In any of the abnormal spaceship states, do not allow firing the rockets
if event.key == pygame.K_z:
self.rocket_left.launch()
if event.key == pygame.K_x:
self.rocket_right.launch()
if self.spaceship_state != self.SpaceshipState.hit and not self.first_blink:
# Allow movement of the spaceship if it is normal or blinking, but not if it is exploding
# Also, don't allow moving the ship during the first, long blink
if event.key == pygame.K_LEFT:
self.spaceship.set_direction(Direction.left)
if event.key == pygame.K_RIGHT:
self.spaceship.set_direction(Direction.right)
if event.type == pygame.KEYUP:
if event.key == pygame.K_LEFT:
# Check if a direction change is relevant
if self.spaceship.get_direction() == Direction.left:
self.spaceship.set_direction(Direction.none)
elif event.key == pygame.K_RIGHT:
# Check if a direction change is relevant
if self.spaceship.get_direction() == Direction.right:
self.spaceship.set_direction(Direction.none)
def draw(self):
"""
Draw everything, including static player-related data and notifications
"""
self.screen.draw()
self.enemies.draw()
self.player.draw()
# If any rocket has been launched, it is fully independent from
# the spaceship, and therefore will be drawn unrelated from its state.
for rocket in self.rockets:
if rocket.is_launched():
rocket.draw()
# After an explosion of the spaceship, and reducing of one life, the spaceship
# will not appear for a while, and then appear blinking for few seconds.
if clock() < self.blinking_period:
# Spaceship blinking was required
if clock() > self.blinking_time:
# Blinking duty cycle finished, switch the blink state
self.blink = not self.blink
self.first_blink = False
self.blinking_time = clock() + Const.BLINKING_TIME
if not self.blink:
# Only if the current blink state is False, draw the spaceship
self.rocket_left.draw()
self.rocket_right.draw()
self.spaceship.draw()
else:
# Spaceship blinking is not required or finished
if self.spaceship_state == self.SpaceshipState.blinking:
# Spaceship blinking has just finished
self.spaceship_state = self.SpaceshipState.normal
if self.spaceship_state != self.SpaceshipState.hit:
# Draw the rockets only if the spaceship is not currently exploding
for rocket in self.rockets:
if not rocket.is_launched():
# But not if the rockets have been launched - this was already handled above
rocket.draw()
self.spaceship.draw()
# Handle the notifications
if clock() < self.notification_time:
label = ""
if self.level_notification:
label = self.player.font.render("Level " + str(self.level + 1), True, Const.COLOR_WHITE)
elif self.death_notification:
label = self.player.font.render("Lives:" + str(self.player.get_lives()), True, Const.COLOR_RED)
width, height = label.get_rect().size
label = pygame.transform.scale(label, (width * 4, height * 4))
width, height = label.get_rect().size
x = (Const.SCREEN_WIDTH - width) / 2
y = (Const.SCREEN_HEIGHT - height) / 2
self.screen.window.blit(label, (x, y))
else:
self.level_notification = False
self.death_notification = False