Asteroid Smasher

This is a sample asteroid smasher game made with the Arcade library.

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