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"""
 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()