Пример #1
0
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
Пример #2
0
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',
    }
Пример #3
0
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',
    }
Пример #4
0
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',
    }
Пример #5
0
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',
    }
Пример #6
0
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,
    }
Пример #7
0
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}
Пример #8
0
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',
    }
Пример #9
0
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,
    }
Пример #10
0
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',
    }