lily-seabreeze/sappho

View on GitHub
demo/demo.py

Summary

Maintainability
C
1 day
Test Coverage
"""Demo pygame game using Sappho.

A very horrible and basic pygame game which
utilizes Sappho (hopefully) to the fullest and
acts as a test.

This could also serve as a template in the future.

Needs to use sprite groups.

"""

from __future__ import absolute_import
 
import random
import os.path
import sys
import pygame

DEMO_PATH = os.path.dirname(os.path.abspath(__file__))
os.chdir(DEMO_PATH)

# If being run from sappho project, make sure we use live code version of sappho
if os.path.exists(os.path.join(os.path.dirname(DEMO_PATH), "sappho")):
    sys.path.insert(0, os.path.dirname(DEMO_PATH))

from sappho import collide
from sappho.layers import SurfaceLayers
from sappho.camera import Camera, CameraCenterBehavior
from sappho.animate import AnimatedSprite
from sappho.tiles import TileMap, Tilesheet, tmx_file_to_tilemaps

import config

# init pygame
pygame.mixer.init(22100, -16, 2, 64)

RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

THIP_SOUND = pygame.mixer.Sound("thip.ogg")
SMALL_EXPLODE_SOUND = pygame.mixer.Sound("smallexplode.ogg")
BIG_LASER_SOUND = pygame.mixer.Sound("crush.ogg")
LASER_SOUND = pygame.mixer.Sound("laser.wav")


class Player(object):

    def __init__(self, topleft):
        self.sprite = AnimatedSprite.from_gif(config.ANIMATED_SPRITE_PATH, mask_threshold=127)
        self.bullet_duration = 150
        self.bullet_size = (2, 2)
        self.bullet_color = BLUE
        self.bullet_list = pygame.sprite.Group()
        self.x_speed = 0
        self.y_speed = 0
        self.shoot_sound = LASER_SOUND

    def shoot(self, x_speed, y_speed):
        self.shoot_sound.play()
        bullet = Bullet(self.bullet_duration, self.bullet_color, self.sprite.rect.center, x_speed, y_speed, self.bullet_size)
        self.bullet_list.add(bullet)

    def update(self, camera, wall_collision_group, layer_size, timedelta):
        self.sprite.update(timedelta)

        if self.x_speed == 0 and self.y_speed == 0:
            return None

        # move
        new_coord = (self.x_speed + self.sprite.rect.topleft[0],
                     self.y_speed + self.sprite.rect.topleft[1])

        dummy_future_rect = self.sprite.rect.copy()
        dummy_future_rect.topleft = new_coord
        wrapped_coord = wrap_logic(dummy_future_rect, layer_size).topleft

        if wrapped_coord != new_coord:
            # we wrapped around the screen pacman style
            oldie = self.sprite.rect.topleft
            self.sprite.rect.topleft = wrapped_coord
            collided_with = collide.collides_rect_mask(self.sprite, wall_collision_group)
            if collided_with:   
                self.sprite.rect.topleft = oldie
        else:
            # we did NOT wrap around the screen
            closest_position, collided_with = collide.move_as_close_as_possible(self.sprite, new_coord, wall_collision_group)
            self.sprite.rect.topleft = closest_position

        camera.scroll_to(self.sprite.rect)

        if collided_with:
            THIP_SOUND.play()
            self.bullet_duration = 500
            self.bullet_color = RED
            self.bullet_size = (4, 4)
            self.shoot_sound = BIG_LASER_SOUND
            self.x_speed = 0
            self.y_speed = 0 
        else:
            self.bullet_duration = 150
            self.bullet_size = (2, 2)
            self.shoot_sound = LASER_SOUND
            self.bullet_color = BLUE


class Asteroid(pygame.sprite.Sprite):
    COLOR = GREEN
    SPAWN_DISTANCE_FROM_PLAYER = 35

    def __init__(self, center, size, x_speed, y_speed):
        image = pygame.Surface([size, size])
        self.rect = image.get_rect()
        self.rect.center = center
        self.image = image
        self.image.fill(self.COLOR)
        self.x_speed = x_speed
        self.y_speed = y_speed
        super(Asteroid, self).__init__()

    @classmethod
    def add_if_below_threshold(cls, player, asteroid_list, at_least_x_existing):

        if len(asteroid_list) < at_least_x_existing:
            plus_or_minus_x = random.choice([1, -1])
            new_asteroid_x = player.sprite.rect.top - (cls.SPAWN_DISTANCE_FROM_PLAYER * plus_or_minus_x)

            if new_asteroid_x > player.sprite.rect.left:
                x_speed = -1
            else:
                x_speed = 1

            plus_or_minus_y = random.choice([1, -1])
            new_asteroid_y = player.sprite.rect.left - (cls.SPAWN_DISTANCE_FROM_PLAYER * plus_or_minus_y)

            if new_asteroid_y > player.sprite.rect.top:
                y_speed = -1
            else:
                y_speed = 1

            another_asteroid = Asteroid((new_asteroid_x, new_asteroid_y), 10, x_speed, y_speed)
            asteroid_list.add(another_asteroid)

    def new_smaller_asteroid(self):
        """New x and y speed based on player coords

        """

        new_asteroid_x_speed = random.choice([1, -1])
        new_asteroid_y_speed = random.choice([1, -1])

        new_asteroid_size = self.rect.width / 2
        new_asteroid = Asteroid(self.rect.center,
                                new_asteroid_size,
                                new_asteroid_x_speed,
                                new_asteroid_y_speed)
        return new_asteroid

    def explode(self, asteroid_list):
        """Explode into as many pieces as ... size

        """

        if self.rect.width > 5:

            for i in range(self.rect.width / 2):
                new_asteroid = self.new_smaller_asteroid()
                asteroid_list.add(new_asteroid)

        asteroid_list.remove(self)

    def update(self, wall_list, layer_size, asteroid_list, player_bullet_list, timedelta):
        self.rect.topleft = (self.rect.left + self.x_speed,
                             self.rect.top + self.y_speed)

        new_rect = wrap_logic(self.rect, layer_size)
        self.rect = new_rect
             
        # colliding with wall?
        collision_wall = collide.collides_rect_mask(self, wall_list)

        if collision_wall:
            self.explode(asteroid_list)


class Bullet(pygame.sprite.Sprite):

    def __init__(self, duration, color, center, x_speed, y_speed, size):
        self.image = pygame.Surface(size)
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.rect.center = center
        self.x_speed = x_speed
        self.y_speed = y_speed
        self.current_duration = 0
        self.total_duration = duration
        super(Bullet, self).__init__()

    def update(self, wall_list, asteroid_list, player_bullet_list, timedelta):
        self.current_duration += timedelta

        if self.current_duration >= self.total_duration:
            player_bullet_list.remove(self)

        new_coord = (self.rect.left + self.x_speed,
                     self.rect.top + self.y_speed)
        collision_asteroids = collide.sprites_in_orthogonal_path(self,
                                                                 new_coord,
                                                                 asteroid_list)

        if collision_asteroids:
            SMALL_EXPLODE_SOUND.play()

            for asteroid in collision_asteroids:
                asteroid.explode(asteroid_list)

            player_bullet_list.remove(self)
             
        # colliding with wall?
        collision_wall = collide.sprites_in_orthogonal_path(self, new_coord, wall_list)

        if collision_wall:
            player_bullet_list.remove(self)
            THIP_SOUND.play()
        else:
            self.rect.topleft = new_coord


def wrap_logic(rect, layer_size):
    new_rect = rect.copy()

    if new_rect.centery > layer_size[1]:
        new_rect.centery -= layer_size[1]
    elif new_rect.centery < 0:
        new_rect.centery += layer_size[1]

    if new_rect.centerx > layer_size[0]:
        new_rect.centerx -= layer_size[0]
    elif new_rect.centerx < 0:
        new_rect.centerx += layer_size[0]

    return new_rect


# Setup #######################################################################
pygame.init()
 
# Set the width and height of the screen [width,height]
screen = pygame.display.set_mode(config.RESOLUTION)
 
# Set the caption in the title bar
pygame.display.set_caption(config.WINDOW_TITLE)
 
# Loop until the user clicks the close button.
done = False
 
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
 
# Hide the mouse cursor
pygame.mouse.set_visible(0)
 
# a background animation
# will be paralax
animated_bg = AnimatedSprite.from_gif("bg.gif")

# The sprite which the player controls
player = Player(config.START_POSITION)

# Load the scene, namely the layered map. Layered maps are
# represented as a list of TileMap objects.
tilesheet = Tilesheet.from_file(config.TILESHEET_PATH, *config.START_POSITION)
tilemaps_by_layer = tmx_file_to_tilemaps(config.TMX_PATH, tilesheet)

# ... Make a list of surfaces from the tilemaps.
tilemap_surfaces = []

for tilemap_representing_layer in tilemaps_by_layer:
    tilemap_surfaces.append(tilemap_representing_layer.to_surface())

# Set up the camera
surface_size = tilemap_surfaces[0].get_size()
camera = Camera(surface_size, config.RESOLUTION, config.VIEWPORT,
                behavior=CameraCenterBehavior())

# The render layers which we draw to
layers = SurfaceLayers(camera.source_surface, len(tilemap_surfaces))

# Asteroids
asteroid_list = pygame.sprite.Group()

# Main program loop ###########################################################
while not done:

    # Process events
    for event in pygame.event.get():

        if event.type == pygame.QUIT:
            done = True
 
        # User pressed down on a key
        elif event.type == pygame.KEYDOWN:

            # Figure out if it was an arrow key. If so
            # adjust speed.
            if event.key == pygame.K_LEFT:
                player.x_speed = max([player.x_speed - 1, -config.MAX_SPEED])
            elif event.key == pygame.K_RIGHT:
                player.x_speed = min([player.x_speed + 1, config.MAX_SPEED])
            elif event.key == pygame.K_UP:
                player.y_speed = max([player.y_speed - 1, -config.MAX_SPEED])
            elif event.key == pygame.K_DOWN:
                player.y_speed = min([player.y_speed + 1, config.MAX_SPEED])
            elif event.key == pygame.K_d:
                player.shoot(6, 0)
            elif event.key == pygame.K_a:
                player.shoot(-6, 0)
            elif event.key == pygame.K_s:
                player.shoot(0, 6)
            elif event.key == pygame.K_w:
                player.shoot(0, -6)

    # Test if the player is going to collide with any other objects
    # and if not, move the player

    # Move the object according to the speed vector.
    #
    # We will be resetting player.sprite.rect.topleft
    # to old_topleft if there is a collision.
    tilemap_on_players_index = tilemaps_by_layer[config.ANIMATED_SPRITE_Z_INDEX]
    collision_group_on_player_index = tilemap_on_players_index.collision_group
    timedelta = clock.get_time()
    player.update(camera, collision_group_on_player_index, layers[0].get_size(), timedelta)
 
    # create some asteroids, hurdled t the player
    # we should make these chase the player, actually...
    Asteroid.add_if_below_threshold(player, asteroid_list, 10)

    # DRAWING/RENDER CODE

    # first let's render each tilemap on its respective surface
    for i, tilemap_layer in enumerate(tilemap_surfaces):
        layers[i].blit(animated_bg.image, camera.view_rect.topleft)
        layers[i].blit(tilemap_layer, (0, 0))


    # Finally let's render the animated sprite on some
    # arbitrary layer. In the future the TMX will set this.
    layers[config.ANIMATED_SPRITE_Z_INDEX].blit(player.sprite.image,
                                                player.sprite.rect.topleft)

    # draw asteroids
    asteroid_list.draw(layers[config.ANIMATED_SPRITE_Z_INDEX])
    # draw bullets
    player.bullet_list.draw(layers[config.ANIMATED_SPRITE_Z_INDEX])

    # ... Draw those layers!
    layers.render()
     
    # Let's get the timedelta and then send it to the appropriate things...
    camera.update_state("hahahahahah lies lies lies")
    animated_bg.update(timedelta)
    player.bullet_list.update(collision_group_on_player_index, asteroid_list, player.bullet_list, timedelta)
    asteroid_list.update(collision_group_on_player_index, layers[0].get_size(), asteroid_list, player.bullet_list, timedelta)
 
    # Go ahead and update the screen with what we've drawn.
    screen.blit(camera, (0, 0))
    pygame.display.flip()
 
    # Limit frames per second
    clock.tick(60)
 
# Close the window and quit.
pygame.quit()