1#!/usr/bin/env python
2""" pygame.examples.aliens
3
4Shows a mini game where you have to defend against aliens.
5
6What does it show you about pygame?
7
8* pg.sprite, the difference between Sprite and Group.
9* dirty rectangle optimization for processing for speed.
10* music with pg.mixer.music, including fadeout
11* sound effects with pg.Sound
12* event processing, keyboard handling, QUIT handling.
13* a main loop frame limited with a game clock from pg.time.Clock
14* fullscreen switching.
15
16
17Controls
18--------
19
20* Left and right arrows to move.
21* Space bar to shoot
22* f key to toggle between fullscreen.
23
24"""
25
26import random
27import os
28
29# import basic pygame modules
30import pygame as pg
31
32# see if we can load more than standard BMP
33if not pg.image.get_extended():
34    raise SystemExit("Sorry, extended image module required")
35
36
37# game constants
38MAX_SHOTS = 2  # most player bullets onscreen
39ALIEN_ODDS = 22  # chances a new alien appears
40BOMB_ODDS = 60  # chances a new bomb will drop
41ALIEN_RELOAD = 12  # frames between new aliens
42SCREENRECT = pg.Rect(0, 0, 640, 480)
43SCORE = 0
44
45main_dir = os.path.split(os.path.abspath(__file__))[0]
46
47
48def load_image(file):
49    """loads an image, prepares it for play"""
50    file = os.path.join(main_dir, "data", file)
51    try:
52        surface = pg.image.load(file)
53    except pg.error:
54        raise SystemExit('Could not load image "%s" %s' % (file, pg.get_error()))
55    return surface.convert()
56
57
58def load_sound(file):
59    """because pygame can be be compiled without mixer."""
60    if not pg.mixer:
61        return None
62    file = os.path.join(main_dir, "data", file)
63    try:
64        sound = pg.mixer.Sound(file)
65        return sound
66    except pg.error:
67        print("Warning, unable to load, %s" % file)
68    return None
69
70
71# Each type of game object gets an init and an update function.
72# The update function is called once per frame, and it is when each object should
73# change it's current position and state.
74#
75# The Player object actually gets a "move" function instead of update,
76# since it is passed extra information about the keyboard.
77
78
79class Player(pg.sprite.Sprite):
80    """Representing the player as a moon buggy type car."""
81
82    speed = 10
83    bounce = 24
84    gun_offset = -11
85    images = []
86
87    def __init__(self):
88        pg.sprite.Sprite.__init__(self, self.containers)
89        self.image = self.images[0]
90        self.rect = self.image.get_rect(midbottom=SCREENRECT.midbottom)
91        self.reloading = 0
92        self.origtop = self.rect.top
93        self.facing = -1
94
95    def move(self, direction):
96        if direction:
97            self.facing = direction
98        self.rect.move_ip(direction * self.speed, 0)
99        self.rect = self.rect.clamp(SCREENRECT)
100        if direction < 0:
101            self.image = self.images[0]
102        elif direction > 0:
103            self.image = self.images[1]
104        self.rect.top = self.origtop - (self.rect.left // self.bounce % 2)
105
106    def gunpos(self):
107        pos = self.facing * self.gun_offset + self.rect.centerx
108        return pos, self.rect.top
109
110
111class Alien(pg.sprite.Sprite):
112    """An alien space ship. That slowly moves down the screen."""
113
114    speed = 13
115    animcycle = 12
116    images = []
117
118    def __init__(self):
119        pg.sprite.Sprite.__init__(self, self.containers)
120        self.image = self.images[0]
121        self.rect = self.image.get_rect()
122        self.facing = random.choice((-1, 1)) * Alien.speed
123        self.frame = 0
124        if self.facing < 0:
125            self.rect.right = SCREENRECT.right
126
127    def update(self):
128        self.rect.move_ip(self.facing, 0)
129        if not SCREENRECT.contains(self.rect):
130            self.facing = -self.facing
131            self.rect.top = self.rect.bottom + 1
132            self.rect = self.rect.clamp(SCREENRECT)
133        self.frame = self.frame + 1
134        self.image = self.images[self.frame // self.animcycle % 3]
135
136
137class Explosion(pg.sprite.Sprite):
138    """An explosion. Hopefully the Alien and not the player!"""
139
140    defaultlife = 12
141    animcycle = 3
142    images = []
143
144    def __init__(self, actor):
145        pg.sprite.Sprite.__init__(self, self.containers)
146        self.image = self.images[0]
147        self.rect = self.image.get_rect(center=actor.rect.center)
148        self.life = self.defaultlife
149
150    def update(self):
151        """called every time around the game loop.
152
153        Show the explosion surface for 'defaultlife'.
154        Every game tick(update), we decrease the 'life'.
155
156        Also we animate the explosion.
157        """
158        self.life = self.life - 1
159        self.image = self.images[self.life // self.animcycle % 2]
160        if self.life <= 0:
161            self.kill()
162
163
164class Shot(pg.sprite.Sprite):
165    """a bullet the Player sprite fires."""
166
167    speed = -11
168    images = []
169
170    def __init__(self, pos):
171        pg.sprite.Sprite.__init__(self, self.containers)
172        self.image = self.images[0]
173        self.rect = self.image.get_rect(midbottom=pos)
174
175    def update(self):
176        """called every time around the game loop.
177
178        Every tick we move the shot upwards.
179        """
180        self.rect.move_ip(0, self.speed)
181        if self.rect.top <= 0:
182            self.kill()
183
184
185class Bomb(pg.sprite.Sprite):
186    """A bomb the aliens drop."""
187
188    speed = 9
189    images = []
190
191    def __init__(self, alien):
192        pg.sprite.Sprite.__init__(self, self.containers)
193        self.image = self.images[0]
194        self.rect = self.image.get_rect(midbottom=alien.rect.move(0, 5).midbottom)
195
196    def update(self):
197        """called every time around the game loop.
198
199        Every frame we move the sprite 'rect' down.
200        When it reaches the bottom we:
201
202        - make an explosion.
203        - remove the Bomb.
204        """
205        self.rect.move_ip(0, self.speed)
206        if self.rect.bottom >= 470:
207            Explosion(self)
208            self.kill()
209
210
211class Score(pg.sprite.Sprite):
212    """to keep track of the score."""
213
214    def __init__(self):
215        pg.sprite.Sprite.__init__(self)
216        self.font = pg.font.Font(None, 20)
217        self.font.set_italic(1)
218        self.color = pg.Color("white")
219        self.lastscore = -1
220        self.update()
221        self.rect = self.image.get_rect().move(10, 450)
222
223    def update(self):
224        """We only update the score in update() when it has changed."""
225        if SCORE != self.lastscore:
226            self.lastscore = SCORE
227            msg = "Score: %d" % SCORE
228            self.image = self.font.render(msg, 0, self.color)
229
230
231def main(winstyle=0):
232    # Initialize pygame
233    if pg.get_sdl_version()[0] == 2:
234        pg.mixer.pre_init(44100, 32, 2, 1024)
235    pg.init()
236    if pg.mixer and not pg.mixer.get_init():
237        print("Warning, no sound")
238        pg.mixer = None
239
240    fullscreen = False
241    # Set the display mode
242    winstyle = 0  # |FULLSCREEN
243    bestdepth = pg.display.mode_ok(SCREENRECT.size, winstyle, 32)
244    screen = pg.display.set_mode(SCREENRECT.size, winstyle, bestdepth)
245
246    # Load images, assign to sprite classes
247    # (do this before the classes are used, after screen setup)
248    img = load_image("player1.gif")
249    Player.images = [img, pg.transform.flip(img, 1, 0)]
250    img = load_image("explosion1.gif")
251    Explosion.images = [img, pg.transform.flip(img, 1, 1)]
252    Alien.images = [load_image(im) for im in ("alien1.gif", "alien2.gif", "alien3.gif")]
253    Bomb.images = [load_image("bomb.gif")]
254    Shot.images = [load_image("shot.gif")]
255
256    # decorate the game window
257    icon = pg.transform.scale(Alien.images[0], (32, 32))
258    pg.display.set_icon(icon)
259    pg.display.set_caption("Pygame Aliens")
260    pg.mouse.set_visible(0)
261
262    # create the background, tile the bgd image
263    bgdtile = load_image("background.gif")
264    background = pg.Surface(SCREENRECT.size)
265    for x in range(0, SCREENRECT.width, bgdtile.get_width()):
266        background.blit(bgdtile, (x, 0))
267    screen.blit(background, (0, 0))
268    pg.display.flip()
269
270    # load the sound effects
271    boom_sound = load_sound("boom.wav")
272    shoot_sound = load_sound("car_door.wav")
273    if pg.mixer:
274        music = os.path.join(main_dir, "data", "house_lo.wav")
275        pg.mixer.music.load(music)
276        pg.mixer.music.play(-1)
277
278    # Initialize Game Groups
279    aliens = pg.sprite.Group()
280    shots = pg.sprite.Group()
281    bombs = pg.sprite.Group()
282    all = pg.sprite.RenderUpdates()
283    lastalien = pg.sprite.GroupSingle()
284
285    # assign default groups to each sprite class
286    Player.containers = all
287    Alien.containers = aliens, all, lastalien
288    Shot.containers = shots, all
289    Bomb.containers = bombs, all
290    Explosion.containers = all
291    Score.containers = all
292
293    # Create Some Starting Values
294    global score
295    alienreload = ALIEN_RELOAD
296    clock = pg.time.Clock()
297
298    # initialize our starting sprites
299    global SCORE
300    player = Player()
301    Alien()  # note, this 'lives' because it goes into a sprite group
302    if pg.font:
303        all.add(Score())
304
305    # Run our main loop whilst the player is alive.
306    while player.alive():
307
308        # get input
309        for event in pg.event.get():
310            if event.type == pg.QUIT:
311                return
312            if event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE:
313                return
314            elif event.type == pg.KEYDOWN:
315                if event.key == pg.K_f:
316                    if not fullscreen:
317                        print("Changing to FULLSCREEN")
318                        screen_backup = screen.copy()
319                        screen = pg.display.set_mode(
320                            SCREENRECT.size, winstyle | pg.FULLSCREEN, bestdepth
321                        )
322                        screen.blit(screen_backup, (0, 0))
323                    else:
324                        print("Changing to windowed mode")
325                        screen_backup = screen.copy()
326                        screen = pg.display.set_mode(
327                            SCREENRECT.size, winstyle, bestdepth
328                        )
329                        screen.blit(screen_backup, (0, 0))
330                    pg.display.flip()
331                    fullscreen = not fullscreen
332
333        keystate = pg.key.get_pressed()
334
335        # clear/erase the last drawn sprites
336        all.clear(screen, background)
337
338        # update all the sprites
339        all.update()
340
341        # handle player input
342        direction = keystate[pg.K_RIGHT] - keystate[pg.K_LEFT]
343        player.move(direction)
344        firing = keystate[pg.K_SPACE]
345        if not player.reloading and firing and len(shots) < MAX_SHOTS:
346            Shot(player.gunpos())
347            if pg.mixer:
348                shoot_sound.play()
349        player.reloading = firing
350
351        # Create new alien
352        if alienreload:
353            alienreload = alienreload - 1
354        elif not int(random.random() * ALIEN_ODDS):
355            Alien()
356            alienreload = ALIEN_RELOAD
357
358        # Drop bombs
359        if lastalien and not int(random.random() * BOMB_ODDS):
360            Bomb(lastalien.sprite)
361
362        # Detect collisions between aliens and players.
363        for alien in pg.sprite.spritecollide(player, aliens, 1):
364            if pg.mixer:
365                boom_sound.play()
366            Explosion(alien)
367            Explosion(player)
368            SCORE = SCORE + 1
369            player.kill()
370
371        # See if shots hit the aliens.
372        for alien in pg.sprite.groupcollide(aliens, shots, 1, 1).keys():
373            if pg.mixer:
374                boom_sound.play()
375            Explosion(alien)
376            SCORE = SCORE + 1
377
378        # See if alien boms hit the player.
379        for bomb in pg.sprite.spritecollide(player, bombs, 1):
380            if pg.mixer:
381                boom_sound.play()
382            Explosion(player)
383            Explosion(bomb)
384            player.kill()
385
386        # draw the scene
387        dirty = all.draw(screen)
388        pg.display.update(dirty)
389
390        # cap the framerate at 40fps. Also called 40HZ or 40 times per second.
391        clock.tick(40)
392
393    if pg.mixer:
394        pg.mixer.music.fadeout(1000)
395    pg.time.wait(1000)
396
397
398# call the "main" function if running this script
399if __name__ == "__main__":
400    main()
401    pg.quit()
402