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