Game - D&D

devfest
gaming symbol
Design & Development

Mario Vitale

Why?

cabinet
cabinet

Agenda

gears
Design
  • Playability
  • Psy perspective
  • How to engage

Game target

playingFight
Learning to hunt
Risk
Social Role

It's not whether you win or lose... it's whether you're taking too damn long to take your turn.

Game components

SupermarioJump
Mechanics
SupermarioWallClimb
Event

Emotional triggers

emotionalchanges
Our unconscious react to changes with an emotional response

Game experience

GameExperience
“It’s dangerous to go alone, take this!” – The Legend of Zelda
“It’s time to kick ass and chew bubble gum… and I’m all outta gum.” – Duke Nukem

Flow

flow
Just stay in the Flow

Immersion

GameImmersion
The player’s experience mirrors the character’s experience

2-Factor Theory of Emotions

by Stanley Schachter and Jerome Singer
Battery
Arousal
Laptop Phone Drill
Cognitive Labeling

Cognitive Labeling

emotionsTravolta
In the two-factor theory of emotion, one source of arousal is interchangeable with another.

How-To

wheat Flow
water Challenge
hops Fiction Layer
A GAME is an artificial system for generating experiences.

Tools

graphic
Graphic
spritesheet
sound
Sound
coding
Coding
A game is a multi-sensorial experience

Tools

lib
Library
framework
Framework
encapsulation
Engine
The more it helps the less you know the more you depend on it

Project

pythonlogo
Python
PygameLogo
pygame
breakout
Breakout
fsm Torniqueterevolution
FSM

FSM

TurnstileStateMachine
Turnstile State Machine

FSM PNG

fsmpng princepeugeot

FSM Breakout

BreakoutStatesSchema

Architectural Plan

app plan
Flipbook
Like old cartoons.

State Class-UML

UMLState

Base-State

import pygame
import cfg
              
class BaseState(object):
  def __init__(self):
    '''
    Set the state default properties
    '''
    self.done = False
    self.quit = False
    self.next_state = None
    self.screen_rect = pygame.display.get_surface().get_rect()
    self.persist = {}
    self.font = pygame.font.Font(cfg.FONT, cfg.FONT_BIG)
                
  def startup(self, persistent):
    '''
    Pass the persistent data to the new state
    '''
    self.persist = persistent

  def get_event(self, event):
    '''
    Listen for the events of the state
    '''
    # common to all the states
    if event.type == pygame.QUIT:
      self.quit = True

  def update(self, dt):
    '''
    To use if the state changes in the dt (delta)
    e.g.: the splash screen which last a number of seconds
    '''
    pass

  def draw(self, surface):
    '''
    What the state must draw on the screen
    '''
    pass
            

Inter-Level

import os
import cfg
import init
import pygame
from .base_state import BaseState


class InterLevel(BaseState):
  def __init__(self):
    super(InterLevel, self).__init__()
    # default title to view
    self.title = self.font.render("Level 1", True, pygame.Color("grey"))
    self.title_rect = self.title.get_rect(center=(cfg.SCREENSIZE[0]/2,self.title.get_height()/2))
    self.fontscore = pygame.font.Font(cfg.FONT, cfg.FONT_SMALL)
    # inter-level screen background
    # get a random image and scale it to the screen size
    self.image = pygame.image.load(cfg.SPLASH_BKG_IMG)
    self.image = pygame.transform.scale(self.image, cfg.SCREENSIZE)
    self.img_rect = self.image.get_rect()

    self.next_state = "SERVE"
    self.time_active = 0
    
    # set the background music
    self.bkg_music = init.INTER_LEV_MUSIC
    self.bkg_music.set_volume(cfg.BKG_MUSIC_VOL)
    self.bkg_music.play(-1)

  def startup(self, persistent):
    self.persist = persistent
    # reset the active time
    self.time_active = 0
    # set the message to show
    if self.persist:
      self.title = self.font.render(self.persist["message"], True, pygame.Color("grey"))
      # set the score to show and the rect (score only if there's a persist)
      self.score = self.fontscore.render("Your score: "+str(self.persist["score"]), True, pygame.Color("grey"))
      self.score_rect = self.score.get_rect(center=self.screen_rect.center)
    else:
      self.title = self.font.render("Level 1", True, pygame.Color("grey"))
      self.score = self.fontscore.render("Your score: 0", True, pygame.Color("grey"))
    # title and score rect is always to get
    self.title_rect = self.title.get_rect(center=(cfg.SCREENSIZE[0]/2,self.title.get_height()/2))
    self.score_rect = self.score.get_rect(center=self.screen_rect.center)


  def update(self, dt):
    self.time_active += dt
    if self.time_active >= cfg.INTER_LEV_TIME:
      self.bkg_music.stop()
      self.done = True

  def draw(self, surface):
    surface.fill(pygame.Color("black"))
    #surface.blit(self.image, self.img_rect)
    surface.blit(self.title, self.title_rect)
    surface.blit(self.score, self.score_rect)
            

Serve

import os
import cfg
import init
import time
import pygame
import random
from states.base_state import BaseState
from actors.paddle import Paddle
from actors.boing import Boing
from actors.brick import Brick
from actors.heartswarm import HeartSwarm
from actors.score import Score


class Serve(BaseState):
  def __init__(self):
    super(Serve, self).__init__()

    # instruction to move to play state
    self.font = pygame.font.Font(cfg.FONT, cfg.FONT_MEDIUM)
    self.fontscore = pygame.font.Font(cfg.FONT, cfg.FONT_SMALL)
    self.title = self.font.render("Press spacebar to play", True, pygame.Color("grey"))
    self.title_rect = self.title.get_rect(center=self.screen_rect.center)
    
    # image and all the sizing are done in previous state
    self.next_state = "GAMEPLAY"

  ### State-defined Functions ###

  def startup(self, persistent):
    '''
    overwrites the default as initializes the board
    persistent data are not available at init
    '''

    self.persist = persistent
   
    # play the music
    # if persistent data are coming from a previous state keep it otherwise init their values here as they must persist
    if not "bkgimg" in self.persist:
      self.persist["bkgimg"] = pygame.image.load(self.get_random_image(cfg.BKG_IMGS_PATH))
      self.persist["bkgimg"] = pygame.transform.scale(self.persist["bkgimg"], cfg.SCREENSIZE)
      self.persist["bkgimg_rect"] = self.persist["bkgimg"].get_rect()
    if not "lives" in self.persist:
      self.persist["lives"] = cfg.LIVES
    if not "score" in self.persist:
      self.persist["score"] = 0  
    if not "scrtxt" in self.persist:
      self.persist["scrtxt"] = Score(self.persist["score"])  
    if not "level" in self.persist:
      self.persist["level"] = 1
    if not "bricks" in self.persist or len(self.persist["bricks"]) == 0:
      self.persist["bricks"] = []
      # set a new level brick schema
      self.random_level_gen()
    if not "paddle" in self.persist:
      self.persist["paddle"] = Paddle(random.randint(0, 2))
    if not "boing" in self.persist or self.persist["boing"].to_init:
      print("init ball")
      self.persist["boing"] = Boing(random.randint(0, 1), self.persist["level"])
    if not "heartswarm" in self.persist:
      self.persist["heartswarm"] = HeartSwarm(self.persist["lives"])
    
    # init the paddle and ball positions
    self.persist["boing"].reset()
    self.persist["paddle"].reset()    
    

  def get_event(self, event):
    if event.type == pygame.QUIT:
      self.quit = True
    if event.type == pygame.KEYDOWN:
      if event.key == pygame.K_SPACE:
        self.done = True


  def draw(self, surface):
    '''
    draw the board according to its status
    and highlight the cursor
    '''
    # clean-up
    surface.fill(pygame.Color("black")) 
    # draw the background
    surface.blit(self.persist["bkgimg"],self.persist["bkgimg_rect"])
    # other items
    self.persist["paddle"].draw(surface)
    self.persist["boing"].draw(surface)
    self.persist["heartswarm"].draw(surface)
    self.persist["scrtxt"].draw(surface)
    # wall
    for brk in self.persist["bricks"]:
      brk.draw(surface)
    surface.blit(self.title, self.title_rect)
    

  
  def get_random_image(self, imgs_path):
    '''
    Gets the list of images in the path passed as parameter.
    Returns a random image full path from the list
    '''
    imagenames = os.listdir(imgs_path)
    assert len(imagenames) > 0
    return os.path.join(imgs_path, random.choice(imagenames))
  

  def random_level_gen(self):    
    '''
    Generates a random brick schema
    '''
    # randomly set the nr of rows and columns (even) within a range (for a better shape)
    rownum = random.randint(cfg.WALL_MIN_ROW_NR, cfg.WALL_MAX_ROW_NR)
    colnum = random.randint(cfg.WALL_MIN_COL_NR, cfg.WALL_MAX_COL_NR)
    # to be sure the number of columns it's even (for a better shape)
    colnum = colnum % 2 == 0 and (colnum + 1) or colnum

    print("The level has",rownum,"rows and",colnum,"columns")

    # cycle on the y: horizontal drawing (line by line)
    for y in range(0, rownum):
      # check if skip the line (more probable to not skip), if only 2 lines then don't skip
      if rownum > 2:
        skip_line = random.choice([True, False, False])
      else: 
        skip_line = False
      # decide if line alternates the brick shape
      alternate_shape = random.choice([True, False])
      # if alternates then this flag is used to switch, start with a random value
      altflag = random.choice([True, False])

      # brick id define the shape: 2 shapes per level. It's the integer to get the image in the BRICKS array
      # the more the level is hight, the more the images are various
      leveller = min(self.persist["level"], len(init.BRICKS))
      shape1 = random.randint(0, leveller)
      shape2 = random.randint(0, leveller)
      
      # basing on the rownum and colnum calculate the offset to center the bricks
      wall_width = (cfg.BRICK_WIDTH + cfg.BRICK_CLEARANCE)*colnum + cfg.BRICK_CLEARANCE*2 # clearance is considered also to the beginning and to the end
      x_offset = (cfg.GAME_WIDTH - wall_width)/2 + cfg.WALL_X_OFFSET_ADJ
      wall_height = (cfg.BRICK_HEIGHT + cfg.BRICK_CLEARANCE)*colnum + cfg.BRICK_CLEARANCE*2 # clearance is considered also to the beginning and to the end
      y_offset = (cfg.GAME_WIDTH - wall_width)/2 + cfg.WALL_Y_OFFSET_ADJ

      # for each column in the current row, define brick position and shape and enter in the array
      for x in range(0, colnum):
        # if not skip the line place a brick
        if not skip_line:
          # if not skip the single brick (most of the probability to not skip)
          skip_brick = random.randint(0, 3) == 1 and True or False
          if not skip_brick:
            # init an object having the brick characteristics and populate it
            br = {}
            br["x"] = x*(cfg.BRICK_WIDTH + cfg.BRICK_CLEARANCE) + x_offset
            br["y"] = y*(cfg.BRICK_HEIGHT + cfg.BRICK_CLEARANCE) + y_offset
            # deal with alternated shapes
            if (alternate_shape and altflag):
              br["id"] = shape2
              # switch so next brick has a different shape
              altflag = not altflag
            else:
              br["id"] = shape1
            # assign the number of "lives" (hit it can stands) to the brick, depends on the level
            br["lives"] = self.persist["level"] + random.randint(0, 1)
            # create the brick item passing the data
            brk = Brick(br)
            # add the brick to the array
            self.persist["bricks"].append(brk)
    # if the bricks are 4 or less 
    if len(self.persist["bricks"]) < 5:
      print("Due to the random, the wall has few bricks let's try again")
      self.random_level_gen()
    print("amount of bricks to draw",len(self.persist["bricks"]))
              

Play

import os
import cfg
import init
import time
import pygame
import random
from states.base_state import BaseState
from actors.paddle import Paddle
from actors.boing import Boing
from actors.brick import Brick
from actors.heartswarm import HeartSwarm



class Gameplay(BaseState):
  def __init__(self):
    super(Gameplay, self).__init__()
    # set the font size for this state
    self.font = pygame.font.Font(cfg.FONT, cfg.FONT_SMALL)
    # default next-state is inter-level
    self.next_state = "INTERLEVEL"

  ### State-defined Functions ###

  def startup(self, persistent):
    '''
    overwrites the default as initializes the level
    persistent data are not available at init
    '''

    # gets the setup from previous state: serve
    self.persist = persistent
    # set a start random movement to the ball only if not already present (new play)
    if "boing" in self.persist:
      if self.persist["boing"].dx == 0:
        self.persist["boing"].dx = random.randint(-1, 1)
      if self.persist["boing"].dy == 0:
        self.persist["boing"].dy = - random.randint(1, 1)


  def get_event(self, event):
    if event.type == pygame.QUIT:
      self.quit = True
    self.persist["paddle"].get_event(event)
    if event.type == pygame.KEYDOWN:
      if event.key == pygame.K_SPACE:
        self.next_state = "PAUSE"
        self.done = True


  def update(self, dt):
    # update the game components using their own functions
    self.persist["paddle"].update(dt)
    self.persist["boing"].update(dt)

    # collision with paddle
    if self.persist["boing"].hit(self.persist["paddle"]):
      # ball is initialized outside the paddle and y is inverted
      self.persist["boing"].y = self.persist["paddle"].y - self.persist["paddle"].height/2
      self.persist["boing"].dy = - self.persist["boing"].dy

      # x get a direction and an angle according to the hit point (paddle divided in 3 parts)
      # first paddle part has been hit, then ball has an angle (paddle velocity/8 is added) - a little dx due to the position
      if self.persist["boing"].x < self.persist["paddle"].x + (self.persist["paddle"].width / 3):
        self.persist["boing"].dx = self.persist["boing"].dx + self.persist["paddle"].dx/8 - cfg.PADDLE_HIT_ANGLE_ADJ
      # third paddle part has been hit, then ball has an angle (paddle velocity/8 is added) + a little dx due to the position
      elif self.persist["boing"].x > self.persist["paddle"].x + (2*self.persist["paddle"].width / 3):
        self.persist["boing"].dx = self.persist["boing"].dx + self.persist["paddle"].dx/8 + cfg.PADDLE_HIT_ANGLE_ADJ
      # if is hit in the middle nothing change
      #elif (self.persist["boing"].x > self.persist["paddle"].x + (self.persist["paddle"].width / 3) and self.persist["boing"].x < self.persist["paddle"].x + (2*self.persist["paddle"].width / 3)): 
        #self.persist["boing"].x = self.persist["boing"].x

  
    # collision with brick
    for brk in self.persist["bricks"]:
      if self.persist["boing"].hit(brk):
        # increase the score
        self.persist["score"] = self.persist["score"] + cfg.BRICK_HIT_SCORE
        self.persist["scrtxt"].set_score(self.persist["score"])
        # set the ball's y behavior 
        # ball can hit bricks from top or bottom then must be differently initialized
        # hit from below
        if self.persist["boing"].y >= brk.y: 
          # set the ball's new position
          self.persist["boing"].y = brk.y + brk.height/2
        # hit from the top
        elif self.persist["boing"].y < brk.y: 
          self.persist["boing"].y = brk.y - brk.height/2
        # in all the cases invert the y
        self.persist["boing"].dy = - self.persist["boing"].dy
        
        # set the ball's x behavior
        # if hits the first third of the brick then dx goes on left
        if self.persist["boing"].x < brk.x + brk.width/3:
          self.persist["boing"].dx = - self.persist["boing"].dx - cfg.BRICK_HIT_ANGLE_ADJ
        elif self.persist["boing"].x > brk.x + (2*brk.width / 3):
          self.persist["boing"].dx = self.persist["boing"].dx + cfg.BRICK_HIT_ANGLE_ADJ
        # else hits in the middle and nothing change
        
        # set the brick (move to brick class)
        destroyed = brk.hit()
        if destroyed:
          self.persist["bricks"].remove(brk)

    # if balls flows below the screen is lost
    if self.persist["boing"].gone:
      self.persist["lives"] = self.persist["lives"] - 1
      #  update the heart swarm removing a heart
      self.persist["heartswarm"].set_lives(self.persist["lives"])
      # if 0 lives then game lost so next state is gameover
      if self.persist["lives"] == 0:
        self.next_state = "GAMEOVER"
        self.persist["message"] = "You lost!"
      # otherwise it serve another ball
      else:
        self.next_state = "SERVE"
      # move to the new state
      self.done = True

    # if all the bricks are destroyed then the level has been won
    if len(self.persist["bricks"]) == 0:
      self.persist["level"] = self.persist["level"]+1
      self.persist["message"] = "Level "+str(self.persist["level"])
      # renew wall and ball
      self.persist["bricks"] = {}
      self.persist["boing"].to_init = True
      self.next_state = "INTERLEVEL"
      # mov to the default next-state: interstate to recap
      self.done = True    
  

  def draw(self, surface):
    '''
    draw the board according to its status
    and highlight the cursor
    '''
    # clean up
    surface.fill(pygame.Color("black"))
    # draw the background
    surface.blit(self.persist["bkgimg"],self.persist["bkgimg_rect"])
    # other items
    self.persist["heartswarm"].draw(surface)
    self.persist["scrtxt"].draw(surface)
    self.persist["paddle"].draw(surface)
    self.persist["boing"].draw(surface)
    # wall
    for brk in self.persist["bricks"]:
      brk.draw(surface)   
              

Orchestrator

import pygame
import cfg

class Game(object):
  def __init__(self, screen, states, start_state):
    '''
    Initialize the game with a clock, loads the states
    '''
    self.done = False
    self.screen = screen
    self.clock = pygame.time.Clock()
    self.fps = cfg.FPS
    self.clk = cfg.CLK
    self.states = states
    self.state_name = start_state
    self.state = self.states[self.state_name]

  def event_loop(self):
    '''
    Listen for the event and pass them to the 
    event management function of the current state
    '''
    for event in pygame.event.get():
      self.state.get_event(event)

  def flip_state(self):
    '''
    Move to one state to the other passing 
    values that must persist in the landing state
    '''
    current_state = self.state_name
    next_state = self.state.next_state
    self.state.done = False
    self.state_name = next_state
    persistent = self.state.persist
    self.state = self.states[self.state_name]
    self.state.startup(persistent)

  def update(self, dt):
    '''
    Listen for global events like quit or state change then
    calls the update function of the current state passing the delta
    '''
    if self.state.quit:
      self.done = True
    elif self.state.done:
      self.flip_state()
    self.state.update(dt)

  def draw(self):
    '''
    Calls the current state draw function on the passed screen
    '''
    self.state.draw(self.screen)

  def run(self):
    '''
    Runs the game's main loop performing all the actions and setting the delta-time
    until done is set to True.
    '''
    while not self.done:
      dt = self.clock.tick(self.fps)
      self.event_loop()
      self.update(dt)
      self.draw()
      self.clock.tick(self.clk)
      pygame.display.update()
            
              

Main

import sys
import pygame
import cfg
import init
from states.menu import Menu
from states.inter_level import InterLevel
from states.game_play import Gameplay
from states.game_over import GameOver
from states.serve import Serve
from states.splash import Splash
from states.pause import Pause
from game import Game

__author__ = "Mario Vitale - m3o"
__version__= "v0.1"
__title__ = cfg.TITLE


def main():
  
  welcome = "Welcome to "+__title__+" - "+__version__+" by "+__author__
  print(welcome)
  screen = pygame.display.set_mode(cfg.SCREENSIZE)

  pygame.display.set_caption(__title__+" - "+__version__)
  states = {
    "SPLASH": Splash(),
    "SERVE": Serve(),
    "GAMEPLAY": Gameplay(),
    "PAUSE": Pause(),
    "INTERLEVEL": InterLevel(),
    "GAMEOVER": GameOver(),
  }

  game = Game(screen, states, "SPLASH")
  game.run()

  pygame.mixer.quit()
  pygame.quit()
  sys.exit()

if __name__ == '__main__':
  main()
              

Init

import cfg
import pygame

### INIT PYGAME LIBS
pygame.init()
pygame.mixer.init()

### PARSING FUNCTIONS
def get_quads(tilew, tileh, start_x, start_y, end_x, end_y):
  '''
  Returns the quad (rectangle) of a certain size from a defined atlas portion.
  tilew and tileh are width and height of the tile: the sprite in the atlas.
  start_x and start_y define the point where to start the scan.
  end_x and end_y define the point where to end the scan.
  All the rects for the desired items are placed in the returned array.
  '''
  rects = []
  y = start_y
  while y < end_y:
    x = start_x
    while x < end_x:
      #print("Item starts at: (",x,",",y,")")
      rects.append(pygame.Rect(x, y, tilew, tileh))
      x = x + tilew
    y = y + tileh
  return rects

def get_bricks():
  '''
  Basing on the atlas gets the bricks
  Brick block is 32x16
  Scans an area and remove the last 3 blocks (balls, hearts and key-brick)
  '''
  bricks = get_quads(32,16,0,0,192,48)
  return bricks#[:-3]

def get_balls():
  '''
  Basing on the atlas gets the balls
  Ball block is 8x8
  Scans an area and remove the last block which is empty
  '''
  balls = get_quads(8,8,96,48,128,56) # gets only the first 3
  return balls#[:-1]

def get_hearts():
  '''
  Basing on the atlas gets the hearts directly
  Heart block is 10x10
  '''
  hearts = {
    'full': pygame.Rect(128, 48, 10, 10),
    'empty': pygame.Rect(138, 48, 10, 10)
  }
  return hearts

def get_bonus():
  '''
  Basing on the atlas gets the bonuses
  bonus block is 16x16
  The area to scan is precise so no further actions needed
  '''
  return get_quads(16,16,0,192,160,192)

def get_paddles():
  '''
  Basing on the atlas gets the paddles
  Paddle's dimension is not regular: for each color there are several sizes.
  Scans an area and fills the array with sub-arrays, one for each dimension
  '''
  paddles = []
  # paddles are located in the following area
  x = 0
  y = 64
  # 4 iterations, for each one gets all the sizes directly
  for i in range(0,3):
    paddles.append(
      {
        'small': pygame.Rect(x,y,32,16),
        'medium': pygame.Rect(x+32,y,64,16),
        'big': pygame.Rect(x+96,y,96,16),
        'huge': pygame.Rect(x,y+16,128,16)
      }
    )
    # set the position for the new paddle set of a different color
    x = 0
    y = y+32
  return paddles


### GLOBAL IMAGES VARIABLES 

# load the atlas file once
ATLAS = pygame.image.load(cfg.ATLAS)
# get the array of bricks quads (rectangles)
BRICKS = get_bricks()
# get the array of balls quads
BOINGS = get_balls()
# get the array of hearts quads
HEARTS = get_hearts()
# get the array of paddles quads
PADDLES = get_paddles()
# get the array of bonus quads
BONUS = get_bonus()


### GLOBAL SOUND VARIABLES

# load the splash screen background music
SPLASH_MUSIC = pygame.mixer.Sound(cfg.SPLASH_MUSIC) 
# load the splash screen background music
INTER_LEV_MUSIC = pygame.mixer.Sound(cfg.INTER_LEV_MUSIC) 
# load the game background music
GAME_MUSIC = pygame.mixer.Sound(cfg.GAME_MUSIC) 
# load the brick hit sound
BRICK_HIT_SND = pygame.mixer.Sound(cfg.BRICK_HIT_SND) 
# load the brick destroyed sound
BRICK_BROKEN = pygame.mixer.Sound(cfg.BRICK_BROKEN)  
# load the paddle hit sound
PADDLE_HIT_SND = pygame.mixer.Sound(cfg.PADDLE_HIT_SND)  
# load the ball lost sound
BALL_LOST_SND = pygame.mixer.Sound(cfg.BALL_LOST_SND)  
# load the wall hit sound
WALL_HIT_SND = pygame.mixer.Sound(cfg.WALL_HIT_SND)                
              

Actors

import pygame
import cfg

class Actor(object):
  def __init__(self):
    '''
    Set the state default properties.
    It's generic
    '''
    self.id = 0
    self.lives = 0
    self.score = 0
    self.x = 0
    self.y = 0
    self.width = 0
    self.height = 0
    self.gone = False
    self.next_state = None
    self.persist = {}

  def startup(self, persistent):
    '''
    Pass the persistent data to the new state
    '''
    self.persist = persistent


  def update(self, dt):
    '''
    To use if the state changes in the dt (delta time)
    e.g.: the splash screen which last a number of seconds
    '''
    pass

  def draw(self, surface):
    '''
    What the state must draw on the screen
    '''
    pass

  def hit(self, target):
    '''
    What to do when the object collides with something (the target)
    basing on rectangles overlaps
    '''
    pass
              

Ball

import cfg
import init
import pygame
from actors.actor import Actor

class Boing(Actor):
  def __init__(self, color, level):
    super(Boing, self).__init__()
    # initialise the ball size 
    self.width = cfg.BOING_WIDTH
    self.height = cfg.BOING_HEIGHT
    # init ball color
    self.color = color
    # level-speed increment
    self.add_speed = (level-1) * cfg.BOING_SPEED_ADJ
    # init ball image from atlas and rectangle 
    self.boing = init.ATLAS
    self.boing_rect = init.BOINGS[self.color]
    # init the ball position and velocity via reset function
    self.to_init = False
    self.reset()

    #self.collision = False

  def hit(self, target):
    '''
    Returns if the ball collides or not with the target
    '''
    if self.x > target.x + target.width or target.x > self.x + self.width:
      return False
    if self.y > target.y + target.height or target.y > self.y + self.height:
      return False
    else:
      self.paddle_hit_sound = init.PADDLE_HIT_SND
      self.paddle_hit_sound.play()
      return True

  def reset(self):
    '''
    when jumps in serve state set the ball stationary in the middle
    constant values (4) are empiric adjustment
    '''
    # init position
    self.x = cfg.GAME_WIDTH / 2 - (self.width/2)
    self.y = cfg.GAME_HEIGHT - (self.height/2) - cfg.PADDLE_HEIGHT_ADJ - cfg.BOING_HEIGHT_ADJ
    # init the velocity as stopped
    self.dy = 0
    self.dx = 0
    # boing not lost
    self.gone = False



  def update(self, dt):
    self.x = self.x + self.dx * (dt + self.add_speed)
    self.y = self.y + self.dy * (dt + self.add_speed)
    # bounce against the left wall
    if self.x <= 0:
      self.x = 0
      self.dx = - self.dx 
      self.wall_hit_sound = init.WALL_HIT_SND
      self.wall_hit_sound.play()
    # bounce against the right wall
    if self.x >= cfg.GAME_WIDTH - self.width:
      self.x = cfg.GAME_WIDTH - self.width
      self.dx = - self.dx 
      self.wall_hit_sound = init.WALL_HIT_SND
      self.wall_hit_sound.play()
    # bounce against top wall
    if self.y <= 0:
      self.y = 0 + self.width
      self.dy = -self.dy 
      self.wall_hit_sound = init.WALL_HIT_SND
      self.wall_hit_sound.play()
    # doesn't bounce against bottom wall as you lose the ball (managed by play state)
    if self.y >= cfg.GAME_HEIGHT:
      self.ball_lost_sound = init.BALL_LOST_SND
      self.ball_lost_sound.play()
      self.gone = True

  
  def draw(self, surface):
    surface.blit(self.boing, (self.x, self.y), self.boing_rect)
              

Brick

from actors.actor import Actor
import init
import cfg


class Brick(Actor):
  def __init__(self, brk):
    super(Brick, self).__init__()
    # default data, these must update and persist in level transition
    self.id = brk["id"]
    self.lives = brk["lives"]
    self.x = brk["x"]
    self.y = brk["y"]
    self.width = cfg.BRICK_WIDTH
    self.height = cfg.BRICK_HEIGHT
    # init brick image and rect 
    self.brick = init.ATLAS
    self.brick_rect = init.BRICKS[self.id]
    
  def hit(self):
    '''
    decrease the brick life and returns if it has been destroyed
    '''
    self.lives = self.lives - 1
    if self.lives == 0:
      brick_broken_sound = init.BRICK_BROKEN
      brick_broken_sound.play()
      return True
    else:
      return False
      
  def draw(self, surface):
    surface.blit(self.brick, (self.x, self.y), self.brick_rect)
              

Demo

Summary

ComingSoon Puzzle
“If the only tool you have is a hammer, it’s hard to eat spaghetti.” quot. David Allen

Bibliography