6
\$\begingroup\$

This is my first game. Everything in the game was created by me during this week - from assets, music and, of course, code.

As I am new to Python I have wondered if the design template for this game is well achieved and in what aspects it could improve.

The game simply consists of a basket that collects the clothes of an old lady and has to deliver them to her. For each delivery the player scores, thus maintaining the best possible score.

I'd like to improve the game in terms of performance, better models to achieve some of my ideas and best Python coding practices.

context.py

import random, sys
from macros import *
import __init__ as meta


class Item:
    def __init__(self, images):
        self.items_floor = []
        self.images      = images.values()

        self.reset()

    def reset(self):
        self.image = random.choice(self.images)
        self.size  = self.image.get_size()

        self.X = random.randint(0, RESOLUTION[WIDTH] - 200)
        self.Y = -self.size[HEIGHT]

        self.center_X = self.X + self.size[WIDTH] // 2
        self.dY = 20 + random.randint(-5, 5)

    @property
    def position(self):
        return self.X, self.Y

    def hit_collapsing_line(self):
        return self.Y + self.size[HEIGHT] >= Context.collapsing_line

    def hit_ground(self):
        return self.Y + self.size[HEIGHT] + self.dY >= RESOLUTION[HEIGHT]

    def collapsed(self, basket):
        return basket.X - basket.size[WIDTH] < self.X < basket.X + basket.size[WIDTH]

    def get_center(self):
        return self.center_X, self.Y

    def add_item_floor(self, shirt):
        sitting_shirt = self.ItemThumbnail(shirt)
        self.items_floor.append(sitting_shirt)

    def draw_items(self, screen):
        for shirt in self.items_floor:
            thumbnail = shirt.get_thumbnail()
            screen.blit(thumbnail[0], thumbnail[1])

class ItemThumbnail:
    def __init__(self, shirt):
        self.image      = shirt.image
        self.position   = shirt.X, shirt.Y

    def get_thumbnail(self):
        return self.image, self.position


class Basket(object):
    standard_speed       = 3
    friction_coefficient = 1.5

    def __init__(self):
        self.images = Context.load_basket_images()
        self.image  = None

        self.load = 0
        self.size = self.image.get_size()

        self.X = -random.randint(750, 1300)
        self.Y = RESOLUTION[HEIGHT] - self.size[HEIGHT] - BOTTOM_PADDING

        self.centerY = self.Y + self.size[HEIGHT] // 2
        self.half_width = self.size[WIDTH] // 2

        self.direction  = 1
        self.dX         = 0

        self.speed      = self.standard_speed
        self.vel        = self.direction * self.speed

    def action(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_a:
                self.direction = -1
            elif event.key == pygame.K_d:
                self.direction = 1
        elif event.type == pygame.KEYUP:
            self.stop()

            pressed_ = pygame.key.get_pressed()
            if pressed_[pygame.K_a]:        #
                self.direction = -1         # solves double pressing bug
            elif pressed_[pygame.K_d]:      #
                self.direction = 1          #

        self.vel = self.direction * self.speed

    def move(self, runtime=True):
        friction = 0
        if self.vel == 0 and not self.dX == 0:
            friction = -self.dX // abs(self.dX) * self.friction_coefficient

        self.dX = int(self.dX + self.vel + friction)
        self.X  += self.dX

        if runtime:
            self.hit_edges()

    @property
    def position(self):
        return self.X, self.Y

    def entrance(self):
        if self.X > 0 and self.direction == 1:
            self.direction = -1
            self.vel = self.direction * self.speed
        elif self.dX < -30 and self.direction == -1:
            self.stop()

        self.move(False)

    def exit(self):
        if self.X + self.size[WIDTH] >= 0:
            self.dX -= self.standard_speed

    def hit_edges(self):
        hit_left    = self.X + self.dX < 0
        hit_right   = self.X + self.dX + self.size[WIDTH] >= RESOLUTION[WIDTH]
        if hit_left or hit_right:
            self.X = 0 if hit_left else RESOLUTION[WIDTH] - self.size[WIDTH]
            self.dX = -self.dX / 2

    def scores(self, context):
        if self.X + self.size[WIDTH] >= RESOLUTION[WIDTH] - 100:
            context.update_score(self.load)
            self.load = 0

    def update(self):
        if self._load < len(self.images):
            self.image = self.images[self._load]

    def get_center(self):
        return self.X + self.half_width, self.centerY

    def stop(self):
        self.direction = 0
        self.vel = 0

    @property
    def load(self):
        return self._load

    @load.setter
    def load(self, value):
        self._load = value
        if value == 0:
            self.speed = 3
        else:
            self.speed -= self.speed / 2
        self.update()


class Grandma(object):
    GRANDMA_STD_POSITION    = 1400, 400
    speed                   = 3

    def __init__(self):
        self.images = Context.load_grandma_images()
        self.image  = self.images[0]

        self.position = [RESOLUTION[WIDTH], self.GRANDMA_STD_POSITION[HEIGHT]]

    def at_position(self):
        return self.position[WIDTH] <= self.GRANDMA_STD_POSITION[WIDTH]

    def update(self, items_on_floor):
        if items_on_floor < len(self.images):
            self.image = self.images[items_on_floor]
        self.humor = items_on_floor

    def entrance(self):
        if self.position[WIDTH] > self.GRANDMA_STD_POSITION[WIDTH]:
            self.position[WIDTH] -= self.speed

    def exit(self):
        if self.position[WIDTH] <= RESOLUTION[WIDTH]:
            self.position[WIDTH] += self.speed


class Background:
    def __init__(self, images):
        self.background = images[IMG_BACKGROUND]
        self.foreground = images[IMG_FOREGROUND]

        self.cloud_manager = self.CloudManager(images[KEY_CLOUDS].values())
        self.cloud_manager.generate_cloud()

    def draw(self, screen):
        screen.blit(self.background, (0, 0))

        self.cloud_manager.move()

        if self.cloud_manager.clouds[len(self.cloud_manager.clouds) - 1][1][WIDTH] <= self.cloud_manager.create_next:
            self.cloud_manager.generate_cloud()

        clouds = self.cloud_manager.clouds
        for cloud in clouds:
            screen.blit(cloud[0], (cloud[1][WIDTH], cloud[1][HEIGHT]))

        screen.blit(self.foreground, (0, 0))

class CloudManager:
    starting_X  = RESOLUTION[WIDTH]
    cluster_max = 3

    def __init__(self, images):
        self.images = images
        self.clouds = []

        self.create_next = 0

    def cloud_factory(self):
        image   = random.choice(self.images)
        dX      = -random.randint(1, 3) / 3
        position= self.starting_X, random.randint(-10, 400)

        return image, position, dX

    def generate_cloud(self):
        self.clouds.append(self.cloud_factory())
        self.create_next = random.randint(500, 1200)

    def move(self):
        temp_cloud = []
        for i in range(0, len(self.clouds)):
            cloud = self.clouds[i]
            temp_cloud.append((cloud[0], (cloud[1][WIDTH] + cloud[2], cloud[1][HEIGHT]), cloud[2]))

        self.clouds = temp_cloud

    def get_clouds(self):
        return self.clouds


class Context:
    fps             = FPS
    collapsing_line = RESOLUTION[HEIGHT]
    delivering_line = RESOLUTION[WIDTH] - 200   # right edge minus grandma's width
    center_screen   = RESOLUTION[WIDTH] // 2, RESOLUTION[HEIGHT] // 2
    best_score      = 0

    def __init__(self):
        pygame.init()

        self.screen = pygame.display.set_mode(RESOLUTION, pygame.HWSURFACE | pygame.DOUBLEBUF)

        pygame.display.set_caption(meta.__title__)

        self.font       = pygame.font.SysFont(FONT_TYPE, 50)
        self._images    = get_image_map()
        self.score      = 0

        self.audio_score    = load_audio(AUDIO_SCORE)
        self.audio_scream   = load_audio(AUDIO_OVER_SCREAM)
        self.audio_load     = load_audio(AUDIO_TAKE_ITEM)

        pygame.mixer.music.load(RES_AUDIO + AUDIO_MAIN_THEME)
        self.start_music()

    def get_image(self, key):
        return self._images[key]

    def draw_text(self):
        text_score      = TEXT_SCORE + str(self.score)
        text_best_score = TEXT_BEST_SCORE + str(self.best_score)

        surface_score       = self.font.render(text_score, False, FONT_COLOR)
        surface_best_score  = self.font.render(text_best_score, False, FONT_COLOR)

        self.screen.blit(surface_best_score, (50, 100))
        self.screen.blit(surface_score, (50, 50))

    def update_score(self, load):
        self.score += load
        if self.score > self.best_score:
            self.best_score = self.score

    @staticmethod
    def load_basket_images():
        basket_images = get_image_map()[KEY_BASKET]
        return (basket_images[IMG_BASKET_EMPTY],
                basket_images[IMG_BASKET_FULL],
                basket_images[IMG_BASKET_FULL_TOTAL],
                basket_images[IMG_BASKET_LOADED])

    @staticmethod
    def load_grandma_images():
        grandma_images = get_image_map()[KEY_GRANDMA]
        return (grandma_images[IMG_GRANDMA_UNHAPPY],
                grandma_images[IMG_GRANDMA_MAD],
                grandma_images[IMG_GRANDMA_ANGRY],
                grandma_images[IMG_GRANDMA_RAGED])

    @staticmethod
    def start_music():
        pygame.mixer.music.play(-1)

    @staticmethod
    def set_collapsing_line(height):
        Context.collapsing_line = RESOLUTION[HEIGHT] - height

class Blinker:
    timer       = 0
    position    = 600, 500

    def __init__(self, screen, text, lap=30):
        self.screen = screen
        self.text   = text
        self.lap    = lap

    def update(self):
        self.timer += 1
        if self.timer < self.lap:
            self.screen.blit(self.text, self.position)
        elif self.timer == self.lap * 2:
            self.timer = 0

    def __del__(self):
        self.timer = 0


class Game:
    context     = Context()
    background  = Background(context.get_image(KEY_BACKGROUND))
    clock       = pygame.time.Clock()
    fps         = context.fps
    alpha       = 255

    def __init__(self):
        self.game_on    = True

        self.basket     = Basket()
        self.item       = Item(self.context.get_image(KEY_ITEMS))
        self.grandma    = Grandma()

        self.context.set_collapsing_line(self.basket.size[HEIGHT])
        self.fade_surface = pygame.Surface(RESOLUTION)

    def run(self):
        self.start()

        while self.game_on:
            self.event_loop()
            self.update()
            self.draw()

            pygame.display.update()
            self.clock.tick(self.fps)

        return self.end()

    def event_loop(self):
        for event in pygame.event.get():
            if len(self.item.items_floor) >= 4:
                self.basket.stop()
                if self.basket.dX == 0:
                    self.game_on = False
            else:
                self.basket.action(event)

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self.pause()
            if event.type == pygame.QUIT:
                self.quit()

    def pause(self):
        text    = self.context.font.render(TEXT_PAUSE, False, FONT_COLOR)
        blinker = self.context.Blinker(self.context.screen, text)

        on_pause = True
        while on_pause:
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        on_pause = False
                elif event.type == pygame.QUIT:
                    self.quit()

            self.draw()

            blinker.update()
            pygame.display.update()

            self.clock.tick(self.fps)

    def update(self):
        self.basket.move()
        self.basket.scores(self.context)

        print self.context.score
        if self.item.hit_collapsing_line():
            if self.item.collapsed(self.basket):
                self.basket.load += 1
                self.item.reset()
            elif self.item.hit_ground():
                self.item.add_item_floor(self.item)
                self.item.reset()

        self.item.Y = self.item.Y + self.item.dY

    def draw(self):
        self.background.draw(self.context.screen)
        self.item.draw_items(self.context.screen)

        self.grandma.update(len(self.item.items_floor))

        self.context.screen.blit(self.grandma.image, self.grandma.position)
        self.context.screen.blit(self.item.image, self.item.position)
        self.context.screen.blit(self.basket.image, self.basket.position)

        self.context.draw_text()

        pygame.display.flip()

    def start(self):
        text    = self.context.font.render(TEXT_START, False, FONT_COLOR)
        blinker = self.context.Blinker(self.context.screen, text)

        animation = True
        while animation:
            self.grandma.entrance()
            self.basket.entrance()

            self.background.draw(self.context.screen)

            self.context.screen.blit(self.grandma.image, self.grandma.position)
            self.context.screen.blit(self.basket.image, self.basket.position)

            if self.grandma.position[WIDTH] <= self.grandma.GRANDMA_STD_POSITION[WIDTH]:
                blinker.update()
                for event in pygame.event.get():
                    if event.type == pygame.KEYDOWN:
                        if event.key == pygame.K_SPACE:
                            animation = False
                    elif event.type == pygame.QUIT:
                        self.quit()

            self.fade(-1)

            pygame.display.flip()
            pygame.display.update()

            self.clock.tick(self.fps)

    def end(self):
        self.context.score = 0

        over_images = get_image_map()[KEY_OVER]
        face_open   = over_images[IMG_OVER_FACE_OPEN]
        face_closed = over_images[IMG_OVER_FACE_CLOSED]

        face = face_closed
        body = over_images[IMG_OVER_BODY]

        face_position = [660, RESOLUTION[HEIGHT] + 100]

        text    = self.context.font.render(TEXT_LOST, False, (255, 255, 255))
        blinker = self.context.Blinker(self.context.screen, text)
        dir = 1
        dY = 0
        pressed_start = False

        animation = True
        while animation: # TODO improve this
            self.background.draw(self.context.screen)
            self.item.draw_items(self.context.screen)

            self.context.screen.blit(self.basket.image, self.basket.position)

            if blinker.timer % 10:
                dir = -dir

            face_position[WIDTH] += dir * (blinker.timer % 5)

            if self.grandma.position[WIDTH] >= RESOLUTION[WIDTH] and face_position[HEIGHT] >= 150:
                dY += 5
                face_position[HEIGHT] -= 5
                self.context.screen.blit(body, (0, RESOLUTION[HEIGHT] - dY))
                self.context.screen.blit(face, face_position)
            elif self.basket.dX == 0:
                self.grandma.exit()
                self.context.screen.blit(self.grandma.image, self.grandma.position)

            if face_position[HEIGHT] <= 150:
                self.context.screen.blit(body, (0, RESOLUTION[HEIGHT] - dY))
                self.context.screen.blit(face, face_position)
                if blinker.timer == blinker.lap:
                    face = face_open

            if face == face_open:
                self.basket.exit()

            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        pressed_start = True
                elif event.type == pygame.QUIT or event.type == pygame.K_ESCAPE:
                    self.quit()

            if pressed_start:
                self.fade()
            else:
                blinker.update()

            pygame.display.flip()
            pygame.display.update()

            self.clock.tick(self.fps)
            if self.alpha >= 255:
                return True
        return False

    def fade(self, multiplier=1):
        if 0 <= self.alpha <= 255:
            self.alpha += 8 * multiplier
            self.fade_surface.set_alpha(self.alpha)
            self.fade_surface.fill((0, 0, 0))
            self.context.screen.blit(self.fade_surface, (0, 0))
        elif self.alpha < 0:
            self.alpha = 0
        elif self.alpha > 255:
            self.alpha = 255

    @staticmethod
    def quit():
        pygame.quit()
        sys.exit()

macros.py

import pygame
import os


WIDTH, HEIGHT           = 0, 1
RESOLUTION              = 1600, 900
FPS                     = 30

BOTTOM_PADDING          = 50

DIR_RES                 = "resources/"
RES_IMAGES              = DIR_RES + "images/"
RES_AUDIO               = DIR_RES + "audio/"

FONT_TYPE               = "Comic Sans MS"
FONT_COLOR              = (255, 255, 255)

# static elements
IMG_BACKGROUND          = "background.png"
IMG_FOREGROUND          = "foreground.png"

# basket states
IMG_BASKET_EMPTY        = "basket.png"
IMG_BASKET_FULL         = "basket_full.png"
IMG_BASKET_FULL_TOTAL   = "basket_full_total.png"
IMG_BASKET_LOADED       = "basket_loaded.png"
IMG_BASKET_INCLINED_L   = "basket_inclined_left.png"
IMG_BASKET_INCLINED_R   = "basket_inclined_right.png"

# grandma states
IMG_GRANDMA_UNHAPPY     = "grandma_unhappy.png"
IMG_GRANDMA_MAD         = "grandma_mad.png"
IMG_GRANDMA_ANGRY       = "grandma_angry.png"
IMG_GRANDMA_RAGED       = "grandma_raged.png"

# falling items
IMG_SHIRT_BLUE          = "item_shirt_blue.png"
IMG_SHIRT_GREEN         = "item_shirt_green.png"
IMG_SHIRT_RED           = "item_shirt_red.png"
IMG_SHIRT_ORANGE        = "item_shirt_orange.png"
IMG_SHIRT_PURPLE        = "item_shirt_purple.png"
IMG_NECKLACE            = "item_necklace.png"
IMG_PAN                 = "item_pan.png"
IMG_BOXERS              = "item_boxers.png"

# clouds and passing elements
IMG_CLOUD_S             = "cloud_S.png"
IMG_CLOUD_MS            = "cloud_MS.png"
IMG_CLOUD_M             = "cloud_M.png"
IMG_CLOUD_ML            = "cloud_ML.png"
IMG_CLOUD_L             = "cloud_L.png"
IMG_CLOUD_XL            = "cloud_XL.png"

# game over screen
IMG_OVER_FACE_OPEN      = "over_face_open.png"
IMG_OVER_FACE_CLOSED    = "over_face_closed.png"
IMG_OVER_BODY           = "over_grandma_body.png"

KEY_CLOUDS              = "clouds"
KEY_BACKGROUND          = "background"
KEY_BASKET              = "basket_states"
KEY_ITEMS               = "items"
KEY_GRANDMA             = "grandma"
KEY_OVER                = "over"

KEY_HUMOR_UNHAPPY       = "unhappy"         # neutral state
KEY_HUMOR_MAD           = "mad"
KEY_HUMOR_ANGRY         = "angry"
KEY_HUMOR_RAGED         = "raged"           # one more item on the ground and the player loses the game

TEXT_START              = "space bar to help grandma"
TEXT_LOST               = "space bar to try again"
TEXT_PAUSE              = "space bar to continue"
TEXT_SCORE              = "GIVEN: "
TEXT_BEST_SCORE         = "BEST : "

# audio TODO misses converting this to .wav
AUDIO_MAIN_THEME        = "main_theme.mp3"
AUDIO_SCORE             = "action_score.mp3"
AUDIO_TAKE_ITEM         = "action_take_item.mp3"
AUDIO_OVER_SCREAM       = "over_scream.mp3"


def load_audio(name):
    return pygame.mixer.Sound(RES_AUDIO + name)


def load_image(name):
    image_path = RES_IMAGES + name
    return pygame.image.load(os.path.join(image_path)).convert_alpha()


def get_image_map():
    return {
                  KEY_GRANDMA :
                      {IMG_GRANDMA_UNHAPPY  : load_image(IMG_GRANDMA_UNHAPPY),
                       IMG_GRANDMA_MAD      : load_image(IMG_GRANDMA_MAD),
                       IMG_GRANDMA_ANGRY    : load_image(IMG_GRANDMA_ANGRY),
                       IMG_GRANDMA_RAGED    : load_image(IMG_GRANDMA_RAGED)},
                  KEY_BASKET :
                      {IMG_BASKET_EMPTY      : load_image(IMG_BASKET_EMPTY),
                       IMG_BASKET_FULL       : load_image(IMG_BASKET_FULL),
                       IMG_BASKET_FULL_TOTAL : load_image(IMG_BASKET_FULL_TOTAL),
                       IMG_BASKET_LOADED     : load_image(IMG_BASKET_LOADED)},
                  KEY_ITEMS :
                      {IMG_SHIRT_BLUE   : load_image(IMG_SHIRT_BLUE),
                       IMG_SHIRT_RED    : load_image(IMG_SHIRT_RED),
                       IMG_SHIRT_GREEN  : load_image(IMG_SHIRT_GREEN),
                       IMG_SHIRT_ORANGE : load_image(IMG_SHIRT_ORANGE),
                       IMG_SHIRT_PURPLE : load_image(IMG_SHIRT_PURPLE),
                       IMG_BOXERS       : load_image(IMG_BOXERS),
                       IMG_NECKLACE     : load_image(IMG_NECKLACE),
                       IMG_PAN          : load_image(IMG_PAN)},
                  KEY_BACKGROUND :
                      {IMG_BACKGROUND   : load_image(IMG_BACKGROUND),
                       IMG_FOREGROUND       : load_image(IMG_FOREGROUND),
                       KEY_CLOUDS:
                          {IMG_CLOUD_S  : load_image(IMG_CLOUD_S),
                           IMG_CLOUD_MS : load_image(IMG_CLOUD_MS),
                           IMG_CLOUD_M  : load_image(IMG_CLOUD_M),
                           IMG_CLOUD_ML : load_image(IMG_CLOUD_ML),
                           IMG_CLOUD_L  : load_image(IMG_CLOUD_L),
                           IMG_CLOUD_XL : load_image(IMG_CLOUD_XL)}
                       },
                  KEY_OVER :
                      {IMG_OVER_FACE_OPEN   : load_image(IMG_OVER_FACE_OPEN),
                       IMG_OVER_FACE_CLOSED : load_image(IMG_OVER_FACE_CLOSED),
                       IMG_OVER_BODY        : load_image(IMG_OVER_BODY)}
    }
\$\endgroup\$
  • \$\begingroup\$ indentation for class Game is off \$\endgroup\$ – hjpotter92 Apr 21 '18 at 7:14
  • \$\begingroup\$ That was an issue that happened when I copy-pasted the code \$\endgroup\$ – tvaz Apr 21 '18 at 14:03
  • \$\begingroup\$ I tried to fix the copy-paste error but I might have made a mistake. \$\endgroup\$ – Gareth Rees May 9 '18 at 9:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.