class HudObject(GameObject): ''' HudObject is meant for displays like lives, score, etc. It is its own type so it can be in collisions._do_not_check, because we don't intend for these to collide with anything. ''' STATES = config.Enum('IDLE') fonts = (config.FONT, ) def __init__(self, image, pos): super().__init__() self.image = image self.rect = pygame.Rect(pos, image.get_size()) del self.velocity, self.acceleration, self.position, self.next_state def center(self): self.rect.centerx = config.SCREEN_RECT.centerx return self def update(self): ''' Since HudObjects have no functionality, let's make update() do nothing! ''' pass
class Particle(GameObject): ''' Tiny bits and pieces used for graphical effects. Meant for things like explosions, sparkles, impacts, etc. Not meant to be instantiated alone; should only be held by ParticleEmitters. Particles should have some randomization, particularly in initial direction. ''' START_POS = (-100.0, -100.0) STATES = config.Enum(*PARTICLE_STATES) def __init__(self, image, move_func=_p_move, appear_func=_p_appear): ''' @ivar move_func: The function that defines motion; called each update() Takes one parameter ''' super().__init__() self.appear_func = partial(appear_func, self) self.image = image self.move_func = partial(move_func, self) self.position = list(Particle.START_POS) self.rect = Rect(self.position, self.image.get_size()) self.state = Particle.STATES.IDLE def appear(self): self.appear_func() self.change_state(Particle.STATES.ACTIVE) def move(self): self.move_func() if not self.rect.colliderect(config.SCREEN_RECT): #If we're no longer on-screen... self.change_state(Particle.STATES.LEAVING) def leave(self): self.kill() self.acceleration = [0.0, 0.0] self.velocity = [0.0, 0.0] self.position = list(Particle.START_POS) self.rect.topleft = self.position self.change_state(Particle.STATES.IDLE) actions = { STATES.IDLE: None, STATES.APPEARING: 'appear', STATES.ACTIVE: 'move', STATES.LEAVING: 'leave', }
class Bullet(GameObject): FRAME = Rect(227, 6, 26, 19) STATES = config.Enum(*BULLET_STATES) SPRITE = config.get_sprite(FRAME) def __init__(self): super().__init__() self.image = self.__class__.SPRITE #@UndefinedVariable self.rect = self.__class__.START_POS.copy() self.position = list(self.rect.topleft) self.state = self.__class__.STATES.IDLE self.image.set_colorkey(color.COLOR_KEY, config.BLIT_FLAGS) def start_moving(self): ''' Begins moving. ''' self.position = list(self.rect.topleft) self.velocity[1] = self.__class__.SPEED self.change_state(self.__class__.STATES.MOVING) def move(self): ''' Bullets only move vertically in Invasodado. ''' self.position[1] += self.velocity[1] self.rect.top = self.position[1] + .5 def reset(self): ''' Resets the bullet back to its initial position. ''' self.kill() self.velocity[1] = 0 self.rect = self.__class__.START_POS.copy() self.position = list(self.rect.topleft) self.change_state(self.__class__.STATES.IDLE) actions = { STATES.IDLE: None, STATES.FIRED: 'start_moving', STATES.MOVING: 'move', STATES.RESET: 'reset', }
class ComboCounter(HudObject): STATES = config.Enum(*STATE_NAMES) def __init__(self, num, pos): super().__init__(hudobject.make_text(str(num), pos=pos, surfaces=True), pos) self.next_state = None self.position = list(pos) self.rect = Rect(pos, self.image.get_size()) self.time_left = 0 self.change_state(ComboCounter.STATES.IDLE) def update(self): GameObject.update(self) def move(self): self.position[1] -= SPEED self.rect.top = self.position[1] + .5 self.time_left -= 1 if not self.time_left: self.change_state(ComboCounter.STATES.STANDING) self.time_left = TIME_STANDING def stand(self): self.time_left -= 1 if not self.time_left: self.change_state(ComboCounter.STATES.LEAVING) self.time_left = 0 def vanish(self): self.kill() self.change_state(ComboCounter.STATES.IDLE) def appear(self): self.time_left = TIME_MOVING self.change_state(ComboCounter.STATES.MOVING) actions = { STATES.IDLE: None, STATES.APPEARING: 'appear', STATES.MOVING: 'move', STATES.STANDING: 'stand', STATES.LEAVING: 'vanish', }
class Enemy(GameObject): STATES = config.Enum(*ENEMY_STATES) anim = 0.0 base_speed = 0.5 GROUP = None shoot_odds = 0.002 should_flip = False start_time = None velocity = [0.5, 0.0] def __init__(self, form_position): super().__init__() ### Local variables #################################################### the_color = choice(color.LIST) the_id = id(the_color) ######################################################################## ### Object Attributes ################################################## self.amount_lowered = 0 self._anim = 0.0 self.color = the_color self.column = None self._form_position = form_position self.current_frame_list = ENEMY_FRAMES_COLOR_BLIND if settings.SETTINGS[ 'color_blind'] else ENEMY_FRAMES self.image = self.current_frame_list[the_id][0] self.position = list(START_POS) self.rect = Rect(START_POS, self.image.get_size()) self.state = Enemy.STATES.IDLE self.emitter = ParticleEmitter(color.color_particles[the_id], self.rect, 1) ######################################################################## ### Preparation ######################################################## del self.acceleration, self.velocity ######################################################################## def appear(self): self.add(Enemy.GROUP) self.position = [ START_POS[0] * (self._form_position[0] + 1) * 1.5, START_POS[1] * (self._form_position[1] + 1) * 1.5, ] self.rect.topleft = (self.position[0] + .5, self.position[1] + .5) self.color = choice(color.LIST) self.__animate() self.emitter.pool = color.color_particles[id(self.color)] self.change_state(Enemy.STATES.ACTIVE) def move(self): self.__animate() self.position[0] += Enemy.velocity[0] self.rect.topleft = (self.position[0] + .5, self.position[1] + .5) if uniform(0, 1) < Enemy.shoot_odds and not enemybullet.EnemyBullet.halt: #With Enemy.shoot_odds% of firing... #TODO: Use another probability distribution b = enemybullet.get_enemy_bullet() b.rect.midtop = self.rect.midbottom b.position = list(b.rect.topleft) b.add(enemybullet.EnemyBullet.GROUP) if not Enemy.should_flip: #If the squadron of enemies is not marked to reverse direction... if self.rect.left < 0 or self.rect.right > config.SCREEN_WIDTH: #If this enemy touches either end of the screen... Enemy.should_flip = True def die(self): balloflight.get_ball(self.rect.topleft, self.column, self.color).add(Enemy.GROUP) _hurt.play() self.emitter.burst(20) self.kill() self.position = [-300.0, -300.0] self.rect.topleft = self.position self.change_state(Enemy.STATES.IDLE) Enemy.velocity[0] += copysign(0.1, Enemy.velocity[0]) #^ Increase the enemy squadron's speed (copysign() considers direction) def lower(self): self.__animate() self.amount_lowered += 1 self.position[1] += 1 self.rect.top = self.position[1] if self.amount_lowered == LOWER_INCREMENT: self.amount_lowered = 0 self.change_state(Enemy.STATES.ACTIVE) def cheer(self): self.__animate() self.position[1] -= 2 * sin((Enemy.anim / 2) - (pi / 4) * self._form_position[0]) self.rect.top = self.position[1] + .5 def __animate(self): self._anim = int(3 - abs(Enemy.anim - 3)) % 4 self.image = self.current_frame_list[id(self.color)][self._anim] actions = { STATES.APPEARING: 'appear', STATES.LOWERING: 'lower', STATES.ACTIVE: 'move', STATES.DYING: 'die', STATES.IDLE: None, STATES.CHEERING: 'cheer', }
class UFO(GameObject): STATES = config.Enum(*UFO_STATES) GROUP = None BLOCK_GROUP = None def __init__(self): super().__init__() self._anim = 0.0 self.column = None self.current_frame_list = UFO_FRAMES self.image = config.get_sprite(FRAMES[0]) self.odds = expovariate(AVG_WAIT) self.position = list(START_POS) self.rect = Rect(START_POS, self.image.get_size()) self.state = UFO.STATES.IDLE self.emitter = ParticleEmitter(color.random_color_particles, self.rect) del self.acceleration def appear(self): ''' Appear on-screen, but not for very long! ''' INVADE.play(-1) self.position = list(START_POS) self.rect.topleft = list(START_POS) self.change_state(UFO.STATES.ACTIVE) self.velocity[0] = -2.0 def move(self): ''' Move left on the screen, and oscillate up and down. ''' position = self.position rect = self.rect self._anim += 0.5 self.image = UFO_FRAMES[id(choice(color.LIST)) ] \ [int(self._anim) % len(FRAMES)] position[0] += self.velocity[0] position[1] += sin(self._anim / 4) rect.topleft = (position[0] + .5, position[1] + .5) if rect.right < 0: #If we've gone past the left edge of the screen... self.change_state(UFO.STATES.LEAVING) def die(self): ''' Vanish and release a special Block that clears lots of other Blocks. ''' self.emitter.rect = self.rect self.emitter.burst(30) DEATH.play() UFO.BLOCK_GROUP.add(get_block((self.rect.centerx, 0), special=True)) gamedata.score += 90 self.change_state(UFO.STATES.LEAVING) def leave(self): INVADE.stop() self.velocity[0] = 0 self.position = list(START_POS) self.rect.topleft = START_POS self.change_state(UFO.STATES.IDLE) def wait(self): ''' Wait off-screen, and only come back with a specific probability. ''' if uniform(0, 1) < self.odds: #With a certain probability... self.odds = expovariate(AVG_WAIT) self.change_state(UFO.STATES.APPEARING) actions = { STATES.IDLE: 'wait', STATES.APPEARING: 'appear', STATES.ACTIVE: 'move', STATES.DYING: 'die', STATES.LEAVING: 'leave', STATES.GAMEOVER: None, }
class EnemyBullet(Bullet): SPEED = 2 START_POS = Rect(30, config.screen.get_height() * 2, 5, 5) STATES = config.Enum(*BULLET_STATES) FRAME = Rect(262, 6, 20, 19) GROUP = None halt = False def __init__(self): super().__init__() self.blink_timer = 3 * 60 self.image = config.SPRITES.subsurface(self.__class__.FRAME) self.image.set_colorkey(color.COLOR_KEY, config.BLIT_FLAGS) def move(self): ''' Moves down the screen. ''' super().move() if self.rect.top > config.SCREEN_HEIGHT: #If below the bottom of the screen... self.change_state(self.__class__.STATES.RESET) def reset(self): ''' Remove this EnemyBullet from the game screen, but not from memory. ''' super().reset() _enemy_bullets.add(self) self.blink_timer = 3 * 60 def blink(self): self.blink_timer -= 1 self.image.set_alpha(256 * (sin(self.blink_timer // 4) > 0)) if not self.blink_timer: #If we're done animating... self.image.set_alpha(256) EnemyBullet.halt = False self.change_state(EnemyBullet.STATES.RESET) def kill_player(self, other): ''' Kills the player. Called if this EnemyBullet collides with the player. ''' if not other.invincible and other.state == Ship.STATES.ACTIVE: #If the player is not invincible... gamedata.lives -= 1 all_disappear() EnemyBullet.halt = True other.change_state(Ship.STATES.DYING) self.change_state(self.__class__.STATES.DYING) actions = { STATES.IDLE: None, STATES.FIRED: 'start_moving', STATES.MOVING: 'move', STATES.DYING: 'blink', STATES.RESET: 'reset', } collisions = {Ship: kill_player}
class BallOfLight(GameObject): STATES = config.Enum(*BALL_STATES) BLOCK_GROUP = None block_mod = None ENEMY_GROUP = None def __init__(self, startpos=(-300.0, -300.0), newcolor=choice(color.LIST)): GameObject.__init__(self) self._anim = 0 self.color = newcolor self.current_frame_list = _ball_frames_color_blind if settings.SETTINGS[ 'color_blind'] else _ball_frames self.image = self.current_frame_list[id(newcolor)][0] size = self.image.get_size() self.rect = Rect(startpos, size) self.position = list(self.rect.topleft) self.progress = 0 self._target = [None, 0] self.startpos = startpos self.state = BallOfLight.STATES.IDLE del self.acceleration, self.velocity def appear(self): self.image = self.current_frame_list[id(self.color)][0] self.position = list(self.startpos) self.progress = -1 self.rect.topleft = (self.startpos[0] + .5, self.startpos[1] + .5) self.change_state(BallOfLight.STATES.MOVING) assert config.SCREEN_RECT.collidepoint(self._target), \ "BallOfLight's target should be on-screen, but it's %s" % self._target def move(self): position = self.position startpos = self.startpos target = self._target self.progress += 1 percent = self.progress / TIME_TO_MOVE if self._anim < len(FRAMES) - 1: #If we haven't finished animating... self._anim += 1 self.image = self.current_frame_list[id(self.color)][self._anim] if percent >= 1: #If we've reached our target location... self.change_state(BallOfLight.STATES.DYING) else: dx = (percent * percent) * (3 - 2 * percent) dp = 1 - dx position[0] = (startpos[0] * dx) + (target[0] * self.image.get_width() * dp) position[1] = (startpos[1] * dp) + (target[1] * dx) self.rect.topleft = (position[0] + .5, position[1] + .5) assert self.rect.colliderect(config.SCREEN_RECT), \ "A BallOfLight at %s is trying to move off-screen!" % position def vanish(self): _balls.add(self) BallOfLight.BLOCK_GROUP.add( BallOfLight.block_mod.get_block([self._target[0] * 32, 8.0], self.color)) self.kill() self._anim = 0 self.position = [-300.0, -300.0] self.rect.topleft = (self.position[0] + .5, self.position[1] + .5) self.change_state(BallOfLight.STATES.IDLE) actions = { STATES.IDLE: None, STATES.APPEARING: 'appear', STATES.MOVING: 'move', STATES.DYING: 'vanish', }
class Ship(GameObject): ''' The Ship is the player character. There's only going to be one instance of it, but it has to inherit from pygame.sprite.Sprite, so we can't make it a true Python singleton (i.e. a module). ''' STATES = config.Enum(*SHIP_STATES) GROUP = None def __init__(self): ''' @ivar anim: A counter for ship animation @ivar image: The graphic @ivar invincible: How many frames of invincibility the player has if any @ivar my_bullet: The single bullet this ship may fire ''' super().__init__() self.anim = 0.0 self.appear_emitter = ParticleEmitter(APPEAR_POOL, START_POS.copy(), 2) self.emitter = ParticleEmitter(DEATH_POOL, START_POS.copy(), 2) self.flames = FlameTrail() self.image = FRAMES[0] self.invincible = 0 self.light_column = LightColumn() self.my_bullet = ShipBullet() self.position = list(START_POS.topleft) self.rect = START_POS.copy() self.respawn_time = 3 * 60 # In frames self.change_state(Ship.STATES.RESPAWN) def on_fire_bullet(self): bul = self.my_bullet if bul.state == ShipBullet.STATES.IDLE and self.state == Ship.STATES.ACTIVE: #If our bullet is not already on-screen... bul.add(Ship.GROUP) self.anim = 1 self.image = FRAMES[self.anim] bul.rect.center = self.rect.center bul.position = list(self.rect.topleft) bul.change_state(ShipBullet.STATES.FIRED) def respawn(self): self.appear_emitter.burst(200) APPEAR.stop() APPEAR.play() for i in chain(FRAMES, FlameTrail.FRAMES, {self.light_column.image}): i.set_alpha(128) self.invincible = 250 self.light_column.rect.midbottom = self.rect.midtop self.position = list(START_POS.topleft) self.rect = START_POS.copy() self.respawn_time = 3 * 60 self.change_state(Ship.STATES.ACTIVE) def move(self): keys = pygame.key.get_pressed() #Shorthand for which keys are pressed rect = self.rect width = self.image.get_width() if self.state not in { Ship.STATES.DYING, Ship.STATES.DEAD, Ship.STATES.IDLE }: if (keys[K_LEFT] or keys[K_a]) and rect.left > 0: #If we're pressing left and not at the left edge of the screen... self.position[0] -= SPEED elif (keys[K_RIGHT] or keys[K_d]) and rect.right < config.SCREEN_RECT.right: #If we're pressing right and not at the right edge of the screen... self.position[0] += SPEED rect.left = self.position[0] + 0.5 self.flames.rect.midtop = (rect.midbottom[0], rect.midbottom[1] - 1) #Compensate for the gap in the flames ^^^ self.light_column.position[0] = self.position[0] self.light_column.rect.left = round( self.light_column.position[0] / width) * width if self.invincible: #If we're invincible... self.invincible -= 1 elif self.image.get_alpha() == 128: for i in chain(FRAMES, FlameTrail.FRAMES): i.set_alpha(255) self.anim = self.anim + ( 0 < self.anim < len(FRAMES) - 1) / 3 if self.anim != 4 else 0.0 self.image = FRAMES[int(self.anim)] if gamedata.combo_time == gamedata.MAX_COMBO_TIME and gamedata.combo > 1: counter = get_combo_counter(gamedata.combo, self.rect.topleft) counter.rect.midbottom = self.rect.midtop counter.position = list(counter.rect.topleft) counter.change_state(counter.__class__.STATES.APPEARING) Ship.GROUP.add(counter) def die(self): DEATH.play() for i in chain(FRAMES, FlameTrail.FRAMES, (self.light_column.image, )): i.set_alpha(0) self.emitter.rect = self.rect self.emitter.burst(100) self.change_state(Ship.STATES.DEAD) def instadie(self, other): if gamedata.lives: #If we have any lives... gamedata.lives = 0 self.die() def wait_to_respawn(self): self.respawn_time -= 1 if not self.respawn_time: #If we're done waiting to respawn... self.change_state(Ship.STATES.RESPAWN) actions = { STATES.IDLE: None, STATES.SPAWNING: 'respawn', STATES.ACTIVE: 'move', STATES.DYING: 'die', STATES.DEAD: 'wait_to_respawn', STATES.RESPAWN: 'respawn', } collisions = { Enemy: instadie, }
class Block(GameObject): ''' Blocks are left by enemies when they're killed. Match three of the same color, and they'll disappear. ''' STATES = config.Enum(*BLOCK_STATES) block_full = False GROUP = None particle_group = None def __init__(self, position, newcolor=choice(color.LIST), special=False): GameObject.__init__(self) self._anim = 0 self.color = newcolor self.temp_color = self.color self.current_frame_list = _block_frames_color_blind if settings.SETTINGS[ 'color_blind'] else _block_frames self.image = self.current_frame_list[id(self.color)][0] self.position = position self.rect = pygame.Rect(position, self.image.get_size()) #(x, y) self._special = special self.state = Block.STATES.IDLE def __str__(self): return ( "<Block - color: %s, cell: %s, position: %s, rect: %s, state: %i>" % (self.color, self.gridcell, self.position, self.rect, self.state)) def __repr__(self): return self.__str__() def appear(self): self.position = self.__get_snap() self.rect.topleft = self.position self.image = self.current_frame_list[id(self.color)][0] self.gridcell = [ self.rect.centerx // self.rect.width, self.rect.centery // self.rect.height, ] #(x, y) self.emitter = ParticleEmitter(color.color_particles[id(self.color)], self.rect, 5, Block.particle_group) self.change_state(Block.STATES.START_FALLING) self.add(Block.GROUP) def start_falling(self): ''' Starts the Block falling down. Only called once before this Block's state switches to STATES.FALLING. Blocks that are falling must not be part of matches. ''' self.acceleration[1] = GRAVITY blockgrid.check_block(self, False) if self.gridcell[1]: #If we're not at the top of the grid... block_above = blockgrid.blocks[self.gridcell[0]][self.gridcell[1] - 1] if block_above: #If there's at least one block above us... assert isinstance(block_above, Block), \ "%s expected a Block, got a %s" % (self, block_above) for i in blockgrid.blocks[self.gridcell[0]]: #For all grid cells above us... if i and not i.velocity[1]: #If this is a block that's not moving... i.change_state(Block.STATES.START_FALLING) else: break self.change_state(Block.STATES.FALLING) def fall(self): ''' Falls down. For the sake of efficiency, blocks work independently of the collision detection system, since they're only going to move vertically, and only depend on other blocks for collisions. ''' gridcell = self.gridcell position = self.position rect = self.rect self.velocity[1] = min(MAX_SPEED, self.velocity[1] + self.acceleration[1]) position[1] += self.velocity[1] rect.top = position[1] + 0.5 #Round to the nearest integer gridcell[1] = self.rect.centery // self.rect.height self.emitter.rect.topleft = rect.topleft self.__animate() if rect.bottom >= blockgrid.RECT.bottom: #If we've hit the bottom of the grid... rect.bottom = blockgrid.RECT.bottom position[1] = rect.top self.change_state(Block.STATES.IMPACT) elif self.gridcell[1] + 1 < blockgrid.SIZE[1]: #Else if it was another block... below = blockgrid.blocks[gridcell[0]][gridcell[1] + 1] if below and rect.bottom >= below.rect.top: #If we've gone past the block below... rect.bottom = below.rect.top position[1] = rect.top self.change_state(Block.STATES.IMPACT) assert isinstance(below, Block) or below is None, \ "A %s is trying to collide with a stray %s!" % (self, below) assert self.state == Block.STATES.FALLING \ and rect.colliderect(blockgrid.RECT) \ and blockgrid.RECT.collidepoint(position), \ "An active %s has somehow left the field!" % self def wait(self): ''' Constantly checks to see if this block can fall. ''' gridcell = self.gridcell if self.rect.bottom < blockgrid.RECT.bottom: #If we're not at the bottom of the grid... block_below = blockgrid.blocks[gridcell[0]][gridcell[1] + 1] if not block_below or block_below.velocity[1]: #If there's no block directly below... blockgrid.check_block(self, False) self.acceleration[1] = GRAVITY self.change_state(Block.STATES.START_FALLING) if __debug__ and self.rect.collidepoint(pygame.mouse.get_pos()): print(self) def stop(self): ''' Handles the Block when it hits the bottom of the grid or another block. Changes velocity, plays sounds, etc. ''' self.acceleration[1] = 0.0 self.velocity[1] = 0.0 self.position = self.__get_snap() self.rect.topleft = self.position self.gridcell[1] = self.rect.centery // self.rect.height #(row, col) self.state = Block.STATES.ACTIVE blockgrid.blocks[self.gridcell[0]][self.gridcell[1]] = self blockgrid.check_block(self, True) _bump.play() #blockgrid.update() if self._special: #If this is a special block... UFO_BLOCK.play() self.emitter.pool = color.random_color_particles if self.gridcell[1] < blockgrid.SIZE[1] - 1: #If we're not at the bottom of the grid... self.color = blockgrid.blocks[self.gridcell[0]][ self.gridcell[1] + 1].color blockgrid.clear_color(self.color) else: blockgrid.clear_row(self.gridcell[1]) elif not self.gridcell[1]: #If we go past the the playing field... self._anim = len(FRAMES) - 2 #Bring us to the second-to-last frame self.__animate() #And let the animation system finish gamedata.lives = 0 def vanish(self): blockgrid.check_block(self, False) self.emitter.burst(20) self.kill() blockgrid.blocks[self.gridcell[0]][self.gridcell[1]] = None self._anim = 0 self.position = [-300.0, -300.0] self.rect.topleft = self.position self.gridcell = None self.change_state(Block.STATES.IDLE) self.__replace() def __replace(self): ''' Puts this Block back in the set of spares. @postcondition: This Block is ready to be recycled. ''' _blocks_set.add(self) def __animate(self): if self._anim < len(FRAMES) - 1: #If we haven't hit the last frame of animation... self._anim += 1 self.image = self.current_frame_list[id(self.color)][self._anim] if self._special: #If this is a _special block... self.image = self.current_frame_list[id(choice( color.LIST))][self._anim] def __get_snap(self): ''' Returns a position list that snaps this block to the grid. ''' size = self.image.get_size() return [ round(self.position[0] // size[0]) * size[0], round(self.position[1] // size[1]) * size[1], ] actions = { STATES.IDLE: None, STATES.APPEARING: 'appear', STATES.ACTIVE: 'wait', STATES.FALLING: 'fall', STATES.START_FALLING: 'start_falling', STATES.IMPACT: 'stop', STATES.DYING: 'vanish', }