Asteroid Smasher#
This is a sample asteroid smasher game made with the Arcade library.
1"""
2Asteroid Smasher
3
4Shoot space rocks in this demo program created with Python and the
5Arcade library.
6
7Artwork from https://kenney.nl
8
9For a fancier example of this game, see:
10https://github.com/pythonarcade/asteroids
11
12If Python and Arcade are installed, this example can be run from
13the command line with:
14python -m arcade.examples.asteroid_smasher
15"""
16from __future__ import annotations
17
18import random
19import math
20import arcade
21
22from typing import cast
23
24SCREEN_TITLE = "Asteroid Smasher"
25STARTING_ASTEROID_COUNT = 3
26SCALE = 0.5
27
28# Screen dimensions and limits
29SCREEN_WIDTH = 800
30SCREEN_HEIGHT = 600
31OFFSCREEN_SPACE = 300
32LEFT_LIMIT = -OFFSCREEN_SPACE
33RIGHT_LIMIT = SCREEN_WIDTH + OFFSCREEN_SPACE
34BOTTOM_LIMIT = -OFFSCREEN_SPACE
35TOP_LIMIT = SCREEN_HEIGHT + OFFSCREEN_SPACE
36
37# Control player speed
38TURN_SPEED = 3
39THRUST_AMOUNT = 0.2
40
41
42class TurningSprite(arcade.Sprite):
43 """ Sprite that sets its angle to the direction it is traveling in. """
44 def update(self):
45 """ Move the sprite """
46 super().update()
47 self.angle = -math.degrees(math.atan2(self.change_y, self.change_x))
48
49
50class ShipSprite(arcade.Sprite):
51 """ Sprite that represents our spaceship. """
52 def __init__(self, filename, scale):
53 """ Set up the spaceship. """
54
55 # Call the parent Sprite constructor
56 super().__init__(filename, scale=scale)
57
58 # Info on the space ship.
59 # Angle comes in automatically from the parent class.
60 self.thrust = 0
61 self.speed = 0
62 self.max_speed = 4
63 self.drag = 0.05
64 self.respawning = 0
65
66 # Mark that we are respawning.
67 self.respawn()
68
69 def respawn(self):
70 """
71 Called when we die and need to make a new ship.
72 'respawning' is an invulnerability timer.
73 """
74 # If we are in the middle of respawning, this is non-zero.
75 self.respawning = 1
76 self.alpha = 0
77 self.center_x = SCREEN_WIDTH / 2
78 self.center_y = SCREEN_HEIGHT / 2
79 self.angle = 0
80
81 def update(self):
82 """ Update our position and other particulars. """
83
84 # Is the user spawning
85 if self.respawning:
86 # Increase spawn counter, setting alpha to that amount
87 self.respawning += 1
88 self.alpha = self.respawning
89 # Once we are close enough, set alpha to 255 and clear
90 # respawning flag
91 if self.respawning > 230:
92 self.respawning = 0
93 self.alpha = 255
94
95 # Apply drag forward
96 if self.speed > 0:
97 self.speed -= self.drag
98 if self.speed < 0:
99 self.speed = 0
100 # Apply drag reverse
101 if self.speed < 0:
102 self.speed += self.drag
103 if self.speed > 0:
104 self.speed = 0
105
106 # Apply thrust
107 self.speed += self.thrust
108
109 # Enforce speed limit
110 if self.speed > self.max_speed:
111 self.speed = self.max_speed
112 if self.speed < -self.max_speed:
113 self.speed = -self.max_speed
114
115 # Calculate movement vector based on speed/angle
116 self.change_x = math.sin(math.radians(self.angle)) * self.speed
117 self.change_y = math.cos(math.radians(self.angle)) * self.speed
118
119 # Apply movement vector
120 self.center_x += self.change_x
121 self.center_y += self.change_y
122
123 # If the ship goes off-screen, move it to the other side of the window
124 if self.right < 0:
125 self.left = SCREEN_WIDTH
126 if self.left > SCREEN_WIDTH:
127 self.right = 0
128 if self.bottom < 0:
129 self.top = SCREEN_HEIGHT
130 if self.top > SCREEN_HEIGHT:
131 self.bottom = 0
132
133 """ Call the parent class. """
134 super().update()
135
136
137class AsteroidSprite(arcade.Sprite):
138 """ Sprite that represents an asteroid. """
139
140 def __init__(self, image_file_name, scale):
141 super().__init__(image_file_name, scale=scale)
142 self.size = 0
143
144 def update(self):
145 """ Move the asteroid around. """
146 super().update()
147 if self.center_x < LEFT_LIMIT:
148 self.center_x = RIGHT_LIMIT
149 if self.center_x > RIGHT_LIMIT:
150 self.center_x = LEFT_LIMIT
151 if self.center_y > TOP_LIMIT:
152 self.center_y = BOTTOM_LIMIT
153 if self.center_y < BOTTOM_LIMIT:
154 self.center_y = TOP_LIMIT
155
156
157class MyGame(arcade.Window):
158 """ Main application class. """
159
160 def __init__(self):
161 super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
162
163 self.game_over = False
164
165 # Create sprite lists
166 self.player_sprite_list = arcade.SpriteList()
167 self.asteroid_list = arcade.SpriteList()
168 self.bullet_list = arcade.SpriteList()
169 self.ship_life_list = arcade.SpriteList()
170
171 # Set up the player
172 self.score = 0
173 self.player_sprite = None
174 self.lives = 3
175
176 # Load sounds
177 self.laser_sound = arcade.load_sound(":resources:sounds/hurt5.wav")
178 self.hit_sound1 = arcade.load_sound(":resources:sounds/explosion1.wav")
179 self.hit_sound2 = arcade.load_sound(":resources:sounds/explosion2.wav")
180 self.hit_sound3 = arcade.load_sound(":resources:sounds/hit1.wav")
181 self.hit_sound4 = arcade.load_sound(":resources:sounds/hit2.wav")
182
183 # Text fields
184 self.text_score = arcade.Text(
185 f"Score: {self.score}",
186 start_x=10,
187 start_y=70,
188 font_size=13,
189 )
190 self.text_asteroid_count = arcade.Text(
191 f"Asteroid Count: {len(self.asteroid_list)}",
192 start_x=10,
193 start_y=50,
194 font_size=13,
195 )
196
197 def start_new_game(self):
198 """ Set up the game and initialize the variables. """
199
200 self.game_over = False
201
202 # Sprite lists
203 self.player_sprite_list = arcade.SpriteList()
204 self.asteroid_list = arcade.SpriteList()
205 self.bullet_list = arcade.SpriteList()
206 self.ship_life_list = arcade.SpriteList()
207
208 # Set up the player
209 self.score = 0
210 self.player_sprite = ShipSprite(":resources:images/space_shooter/playerShip1_orange.png",
211 scale=SCALE)
212 self.player_sprite_list.append(self.player_sprite)
213 self.lives = 3
214
215 # Set up the little icons that represent the player lives.
216 cur_pos = 10
217 for i in range(self.lives):
218 life = arcade.Sprite(":resources:images/space_shooter/playerLife1_orange.png",
219 scale=SCALE)
220 life.center_x = cur_pos + life.width
221 life.center_y = life.height
222 cur_pos += life.width
223 self.ship_life_list.append(life)
224
225 # Make the asteroids
226 image_list = (":resources:images/space_shooter/meteorGrey_big1.png",
227 ":resources:images/space_shooter/meteorGrey_big2.png",
228 ":resources:images/space_shooter/meteorGrey_big3.png",
229 ":resources:images/space_shooter/meteorGrey_big4.png")
230 for i in range(STARTING_ASTEROID_COUNT):
231 # Pick one of four random rock images
232 image_no = random.randrange(4)
233
234 enemy_sprite = AsteroidSprite(image_list[image_no], scale=SCALE)
235
236 # Set position
237 enemy_sprite.center_y = random.randrange(BOTTOM_LIMIT, TOP_LIMIT)
238 enemy_sprite.center_x = random.randrange(LEFT_LIMIT, RIGHT_LIMIT)
239
240 # Set speed / rotation
241 enemy_sprite.change_x = random.random() * 2 - 1
242 enemy_sprite.change_y = random.random() * 2 - 1
243 enemy_sprite.change_angle = (random.random() - 0.5) * 2
244
245 enemy_sprite.size = 4
246 self.asteroid_list.append(enemy_sprite)
247
248 self.text_score.text = f"Score: {self.score}"
249 self.text_asteroid_count.text = f"Asteroid Count: {len(self.asteroid_list)}"
250
251 def on_draw(self):
252 """ Render the screen """
253
254 # Clear the screen before we start drawing
255 self.clear()
256
257 # Draw all the sprites.
258 self.asteroid_list.draw()
259 self.ship_life_list.draw()
260 self.bullet_list.draw()
261 self.player_sprite_list.draw()
262
263 # Draw the text
264 self.text_score.draw()
265 self.text_asteroid_count.draw()
266
267 def on_key_press(self, symbol, modifiers):
268 """ Called whenever a key is pressed. """
269 # Shoot if the player hit the space bar and we aren't respawning.
270 if not self.player_sprite.respawning and symbol == arcade.key.SPACE:
271 bullet_sprite = TurningSprite(":resources:images/space_shooter/laserBlue01.png",
272 scale=SCALE)
273
274 # Set bullet vector
275 bullet_speed = 13
276 angle_radians = math.radians(self.player_sprite.angle)
277 bullet_sprite.change_y = math.cos(angle_radians) * bullet_speed
278 bullet_sprite.change_x = math.sin(angle_radians) * bullet_speed
279
280 # Set bullet position
281 bullet_sprite.center_x = self.player_sprite.center_x
282 bullet_sprite.center_y = self.player_sprite.center_y
283
284 # Add to our sprite list
285 self.bullet_list.append(bullet_sprite)
286
287 # Go ahead and move it a frame
288 bullet_sprite.update()
289
290 # Pew pew
291 arcade.play_sound(self.laser_sound, speed=random.random() * 3 + 0.5)
292
293 if symbol == arcade.key.LEFT:
294 self.player_sprite.change_angle = -TURN_SPEED
295 elif symbol == arcade.key.RIGHT:
296 self.player_sprite.change_angle = TURN_SPEED
297 elif symbol == arcade.key.UP:
298 self.player_sprite.thrust = THRUST_AMOUNT
299 elif symbol == arcade.key.DOWN:
300 self.player_sprite.thrust = -THRUST_AMOUNT
301
302 def on_key_release(self, symbol, modifiers):
303 """ Called whenever a key is released. """
304 if symbol == arcade.key.LEFT:
305 self.player_sprite.change_angle = 0
306 elif symbol == arcade.key.RIGHT:
307 self.player_sprite.change_angle = 0
308 elif symbol == arcade.key.UP:
309 self.player_sprite.thrust = 0
310 elif symbol == arcade.key.DOWN:
311 self.player_sprite.thrust = 0
312
313 def split_asteroid(self, asteroid: AsteroidSprite):
314 """ Split an asteroid into chunks. """
315 x = asteroid.center_x
316 y = asteroid.center_y
317 self.score += 1
318
319 if asteroid.size == 4:
320 for i in range(3):
321 image_no = random.randrange(2)
322 image_list = [":resources:images/space_shooter/meteorGrey_med1.png",
323 ":resources:images/space_shooter/meteorGrey_med2.png"]
324
325 enemy_sprite = AsteroidSprite(image_list[image_no],
326 scale=SCALE * 1.5)
327
328 enemy_sprite.center_y = y
329 enemy_sprite.center_x = x
330
331 enemy_sprite.change_x = random.random() * 2.5 - 1.25
332 enemy_sprite.change_y = random.random() * 2.5 - 1.25
333
334 enemy_sprite.change_angle = (random.random() - 0.5) * 2
335 enemy_sprite.size = 3
336
337 self.asteroid_list.append(enemy_sprite)
338 self.hit_sound1.play()
339
340 elif asteroid.size == 3:
341 for i in range(3):
342 image_no = random.randrange(2)
343 image_list = [":resources:images/space_shooter/meteorGrey_small1.png",
344 ":resources:images/space_shooter/meteorGrey_small2.png"]
345
346 enemy_sprite = AsteroidSprite(image_list[image_no],
347 scale=SCALE * 1.5)
348
349 enemy_sprite.center_y = y
350 enemy_sprite.center_x = x
351
352 enemy_sprite.change_x = random.random() * 3 - 1.5
353 enemy_sprite.change_y = random.random() * 3 - 1.5
354
355 enemy_sprite.change_angle = (random.random() - 0.5) * 2
356 enemy_sprite.size = 2
357
358 self.asteroid_list.append(enemy_sprite)
359 self.hit_sound2.play()
360
361 elif asteroid.size == 2:
362 for i in range(3):
363 image_no = random.randrange(2)
364 image_list = [":resources:images/space_shooter/meteorGrey_tiny1.png",
365 ":resources:images/space_shooter/meteorGrey_tiny2.png"]
366
367 enemy_sprite = AsteroidSprite(image_list[image_no],
368 scale=SCALE * 1.5)
369
370 enemy_sprite.center_y = y
371 enemy_sprite.center_x = x
372
373 enemy_sprite.change_x = random.random() * 3.5 - 1.75
374 enemy_sprite.change_y = random.random() * 3.5 - 1.75
375
376 enemy_sprite.change_angle = (random.random() - 0.5) * 2
377 enemy_sprite.size = 1
378
379 self.asteroid_list.append(enemy_sprite)
380 self.hit_sound3.play()
381
382 elif asteroid.size == 1:
383 self.hit_sound4.play()
384
385 def on_update(self, x):
386 """ Move everything """
387
388 if not self.game_over:
389 self.asteroid_list.update()
390 self.bullet_list.update()
391 self.player_sprite_list.update()
392
393 for bullet in self.bullet_list:
394 asteroids = arcade.check_for_collision_with_list(bullet,
395 self.asteroid_list)
396
397 for asteroid in asteroids:
398 # expected AsteroidSprite, got Sprite instead
399 self.split_asteroid(cast(AsteroidSprite, asteroid))
400 asteroid.remove_from_sprite_lists()
401 bullet.remove_from_sprite_lists()
402
403 # Remove bullet if it goes off-screen
404 size = max(bullet.width, bullet.height)
405 if bullet.center_x < 0 - size:
406 bullet.remove_from_sprite_lists()
407 if bullet.center_x > SCREEN_WIDTH + size:
408 bullet.remove_from_sprite_lists()
409 if bullet.center_y < 0 - size:
410 bullet.remove_from_sprite_lists()
411 if bullet.center_y > SCREEN_HEIGHT + size:
412 bullet.remove_from_sprite_lists()
413
414 if not self.player_sprite.respawning:
415 asteroids = arcade.check_for_collision_with_list(self.player_sprite,
416 self.asteroid_list)
417 if len(asteroids) > 0:
418 if self.lives > 0:
419 self.lives -= 1
420 self.player_sprite.respawn()
421 self.split_asteroid(cast(AsteroidSprite, asteroids[0]))
422 asteroids[0].remove_from_sprite_lists()
423 self.ship_life_list.pop().remove_from_sprite_lists()
424 print("Crash")
425 else:
426 self.game_over = True
427 print("Game over")
428
429 # Update the text objects
430 self.text_score.text = f"Score: {self.score}"
431 self.text_asteroid_count.text = f"Asteroid Count: {len(self.asteroid_list)}"
432
433
434def main():
435 """ Start the game """
436 window = MyGame()
437 window.start_new_game()
438 arcade.run()
439
440
441if __name__ == "__main__":
442 main()