Пример #1
0
class Bonus(retro.Sprite):
    BONUS1 = types.SimpleNamespace(
        id=0,
        img=retro.Image(assets("bonus1.png")),
        color=(255, 184, 151),
        value=10,
        offset=(0, 0),
    )
    BONUS2 = types.SimpleNamespace(
        id=1,
        img=retro.Image(assets("bonus2.png")),
        color=(255, 136, 84),
        value=50,
        offset=(-6, -6),
    )

    def __new__(cls, pos, color):
        if color == cls.BONUS1.color: bonus = cls.BONUS1
        elif color == cls.BONUS2.color: bonus = cls.BONUS2
        else: return None

        self = retro.Sprite.__new__(cls)
        retro.Sprite.__init__(self, bonus.img)
        self.rect.topleft = numpy.add(pos, bonus.offset).tolist()
        self.id = bonus.id
        self.value = bonus.value

        return self

    def __init__(self, pos, color):
        pass
Пример #2
0
def TestInit():
    s1 = retro.Image((100, 100))
    s1_rect = s1.rect()
    s1_rect.move_ip(100, 10)

    s2 = retro.Image(assets("img.png"))
    s2_rect = s2.rect()
    s2_area = retro.Rect(20, 10, 30, 30)
    s2_rect.move_ip(100, 110)

    s3 = retro.Image(assets("trap.png"))
    s3_rect = s3.rect()
    s3_rect.move_ip(100, 150)

    s4 = s3.copy()
    s4_rect = s3_rect.copy()
    s4_rect.move_ip(50, 0)

    s5 = s4
    s5_rect = s4_rect.copy()
    s5_rect.move_ip(50, 0)
    s5.draw_line(retro.GREEN, (0, 0), (30, 30))

    def draw(target):
        target.draw_img(s1, s1_rect.topleft)
        target.draw_img(s2, s2_rect.topleft, s2_area)
        target.draw_img(s3, s3_rect.topleft)
        target.draw_img(s4, s4_rect.topleft)
        target.draw_img(s5, s5_rect.topleft)

    return draw
Пример #3
0
class Mine:
    ICON = retro.Image(asset("ui_mine.png"))

    def __init__(self, lemming):
        self.lemming = lemming

    def start(self):
        self.enabled = False
        return self

    def run(self):
        if not self.enabled:
            self.lemming.start_animation("MINE")
            self.enabled = True

        if not self.lemming.animations.finished:
            return

        self.lemming.bg.original.draw_circle(
            color=retro.BLACK,
            center=self.lemming.rect.center,
            radius=18,
        )

        dx = self.lemming.actions.walk.dx
        self.lemming.rect.move_ip(3 * dx, 3)
        self.lemming.start_animation("MINE")
Пример #4
0
class Build:
    ICON = retro.Image(asset("ui_build.png"))

    def __init__(self, lemming):
        self.lemming = lemming
        self.count = 0

    @property
    def finished(self):
        return (self.count >= 12)

    def start(self):
        self.lemming.start_animation("BUILD")
        return self

    def run(self):
        if not self.lemming.animations.finished: return

        dx = self.lemming.actions.walk.dx
        rect = self.lemming.rect.copy()
        rect.size = (15, 3)
        rect.top = self.lemming.rect.bottom - rect.height
        if (dx > 0): rect.left = self.lemming.rect.right - (rect.width // 2)
        elif (dx < 0): rect.left -= rect.width // 2

        self.lemming.bg.original.draw_rect(retro.GREY, rect)

        self.lemming.rect.move_ip(dx * rect.width // 2, -rect.height)

        self.count += 1
        self.lemming.start_animation("BUILD")
Пример #5
0
class Bird(retro.Sprite):
    IMG = retro.Image(assets("bird.png"))
    ACCEL = 1
    DEFAULT_SPEED = -9
    MAX_SPEED = 9

    def __init__(self, window):
        retro.Sprite.__init__(self, self.IMG)
        self.window = window
        self.fitness = 0
        self.alive = True
        self.rect.center = (75, self.window.rect().centery)
        self.flap()

    def flap(self):
        if self.alive: self.speed_y = self.DEFAULT_SPEED

    def collide(self, *elements):
        return any(self.rect.colliderect(e.rect) for e in elements)

    def update(self):
        if self.alive:
            self.speed_y += self.ACCEL
            self.speed_y = min(self.speed_y, self.MAX_SPEED)
            self.rect.y += self.speed_y
            self.fitness += 1

    def draw(self):
        if self.alive: retro.Sprite.draw(self, self.window)
Пример #6
0
class Bomb:
    ICON = retro.Image(asset("ui_bomb.png"))

    def __init__(self, lemming):
        self.lemming = lemming

    def start(self):
        self.ticker = retro.Ticker(end=135)
        self.explode = False
        return self

    def run(self):
        if not self.explode:
            self.lemming.start_animation("BOMB")
            self.explode = True

            self.lemming.bg.original.draw_circle(
                color=retro.BLACK,
                center=self.lemming.bounding_rect.midbottom,
                radius=20,
            )
        elif self.lemming.animations.finished:
            self.lemming.kill()

    def draw_ticker(self):
        if self.explode: return

        window = self.lemming.window
        ticker_surface = retro.Sprite(window.fonts[0].render(
            text=f"{self.ticker.remaining}",
            color=retro.WHITE,
        ))
        ticker_surface.rect.midbottom = self.lemming.bounding_rect.midtop
        ticker_surface.draw(window)
Пример #7
0
def TestTransform():
    obj1 = retro.Image((50, 50))
    obj1.draw_line(
        color=retro.GREEN,
        start_pos=(0, 0),
        end_pos=(25, 25),
        width=5,
    )
    obj1_rect = obj1.rect()
    obj1_rect.move_ip(10, 300)

    obj2 = obj1.copy()
    obj2.flip(x=True, y=False)
    obj2_rect = obj2.rect()
    obj2_rect.topleft = obj1_rect.topright

    obj3 = retro.Image((50, 50))
    obj3.fill(color=retro.BLUE)
    obj3.draw_rect(
        color=retro.WHITE,
        rect=retro.Rect(10, 10, 25, 25),
        width=4,
    )
    obj3.rotate(45)
    obj3_rect = obj3.rect()
    obj3_rect.topleft = obj1_rect.bottomleft

    obj4 = obj3.copy()
    obj4.resize((25, 25))
    obj4_rect = obj4.rect()
    obj4_rect.topleft = obj3_rect.topright

    obj5 = obj3.copy()
    obj5.scale(1.4)
    obj5_rect = obj5.rect()
    obj5_rect.topleft = obj3_rect.bottomleft

    def draw(target):
        target.draw_img(obj1, obj1_rect.topleft)
        target.draw_img(obj2, obj2_rect.topleft)
        target.draw_img(obj3, obj3_rect.topleft)
        target.draw_img(obj4, obj4_rect.topleft)
        target.draw_img(obj5, obj5_rect.topleft)

    return draw
Пример #8
0
class Explosion(retro.Sprite):
    IMG = retro.Image(asset("bang.png"))

    def __init__(self, center):
        retro.Sprite.__init__(self, self.IMG)
        self.rect.center = center
        self.ticker = retro.Ticker(end=120)

    def update(self):
        if self.ticker.finished: self.kill()
Пример #9
0
    def __init__(self, window, nbirds):
        self.window = window
        self.nbirds = nbirds

        self.bg = retro.Image(assets("bg.png"))
        self.ground = Ground(window)
        self.pipes = Pipes(window)
        self.target = self.pipes[0]
        self.birds = [Bird(window) for i in range(nbirds)]

        self.finished = False
Пример #10
0
class Maze(retro.Sprite):
    IMG = retro.Image(assets('maze.png'))
    RANGEW = range(0, IMG.rect().w, 16)
    RANGEH = range(0, IMG.rect().h, 16)
    WALLS = None
    BONUSES = None

    # Defer constants initialization to avoid circular dependency.
    def __new__(cls):
        if not cls.WALLS: cls.WALLS = Walls()
        if not cls.BONUSES: cls.BONUSES = Bonuses()
        return retro.Sprite.__new__(cls)

    def __init__(self):
        retro.Sprite.__init__(self, self.IMG.copy())
        self.bonuses = self.BONUSES.copy()
        self.walls = self.WALLS

    # Checks whether the (`i, `j`) coordinates are inside the maze.
    @classmethod
    def inside(cls, i, j):
        return (i in range(len(cls.RANGEW)) and j in range(len(cls.RANGEH)))

    @classmethod
    def tile_pos(cls, pos):
        return (pos[0] // 16, pos[1] // 16)

    # Returns a Maze iterator.
    # If `window` is specified, iterates over the neighborhood centered on
    # `window.center` and of reach `window.reach`.
    @classmethod
    def iterator(self, transpose=False, window=None):
        def windowit(range, i):
            minv = window.center[i] - window.reach
            maxv = window.center[i] + window.reach + 1
            if minv < 0:
                maxv += abs(minv)
                minv = 0
            if maxv >= len(range):
                minv -= maxv - len(range)
                maxv = len(range)
            return itertools.islice(enumerate(range), minv, maxv)

        it1, it2 = enumerate(self.RANGEW), enumerate(self.RANGEH)
        if window:
            it1, it2 = windowit(self.RANGEW, 0), windowit(self.RANGEH, 1)
        if transpose: it1, it2 = it2, it1
        for ix, jy in itertools.product(it1, it2):
            if transpose: ix, jy = jy, ix
            yield ix, jy

    def draw(self, target):
        retro.Sprite.draw(self, target)
        self.bonuses.draw(target)
Пример #11
0
class Stop:
    ICON = retro.Image(asset("ui_stop.png"))

    def __init__(self, lemming):
        self.lemming = lemming

    def start(self):
        self.lemming.start_animation("STOP")
        return self

    def run(self):
        pass
Пример #12
0
class Crosshair(retro.Sprite):
    IMG = retro.Image(asset("crosshair.png"))

    def __init__(self, window):
        self.window = window

        retro.Sprite.__init__(self, self.IMG)
        self.rect.center = self.window.rect().center

        self.speed = 5

    def scroll_vec(self):
        p = self.rect.center
        offset = 20
        w = self.window.rect().w
        h = self.window.rect().h
        return retro.Directions(
            up=(0 <= p[1] <= offset),
            down=(h - offset <= p[1] <= h),
            left=(0 <= p[0] <= offset),
            right=(w - offset <= p[0] <= w),
        ).vec

    def move(self, stage):
        scroll_vec = self.scroll_vec()

        # move crosshair
        key_hold = self.window.events.key_hold
        move_vec = retro.Directions(
            up=key_hold(retro.K_UP),
            down=key_hold(retro.K_DOWN),
            left=key_hold(retro.K_LEFT),
            right=key_hold(retro.K_RIGHT),
        ).vec

        for i, _ in enumerate(move_vec):
            move_vec[i] -= scroll_vec[i]
        move_vec = numpy.clip(move_vec, -1, 1)

        self.rect.move_ip(
            move_vec[0] * self.speed,
            move_vec[1] * self.speed,
        )

        # move camera
        stage.camera.move_ip(
            scroll_vec[0] * self.speed,
            scroll_vec[1] * self.speed,
        )
        stage.camera.clamp_ip(stage.image.rect())
Пример #13
0
class Float:
    ICON = retro.Image(asset("ui_float.png"))

    def __init__(self, lemming):
        self.lemming = lemming

    def start(self):
        self.enabled = False
        return self

    def run(self):
        if not self.enabled:
            self.lemming.start_animation("FLOAT")
            self.enabled = True
        self.lemming.rect.move_ip(0, 1)
Пример #14
0
class Ground(retro.Sprite):
    IMG = retro.Image(assets("ground.png"))
    SPEED = 4

    def __init__(self, window):
        retro.Sprite.__init__(self, self.IMG)
        self.window = window
        self.shift = (self.rect.width - self.window.rect().width)
        self.rect.bottom = self.window.rect().bottom

    def update(self):
        self.rect.x = (self.rect.x - self.SPEED) % -self.shift

    def draw(self):
        retro.Sprite.draw(self, self.window)
Пример #15
0
class Ammunitions(retro.Sprite):
    IMG = retro.Image(asset("bullet.png"))
    IMG.scale(0.5)

    def __init__(self, window):
        self.window = window

        retro.Sprite.__init__(self, self.IMG)
        self.rect.bottomleft = self.window.rect().bottomleft

        self.count = 12

    def draw(self, dest):
        for i in range(self.count):
            self.rect.left = i * self.rect.width
            retro.Sprite.draw(self, dest)
Пример #16
0
class Kidnaper(Enemy):
    KIDNAPER_IMG = retro.Image(asset("bandit_kidnaper.png"), )

    def __init__(self):
        Enemy.__init__(self, self.KIDNAPER_IMG)

    def kill(self, p):
        kidnaper = self.rect.copy()
        kidnaper.width //= 2
        victim = self.rect.copy()
        victim.width //= 2
        victim.x += victim.width

        victim_killed = (self.alive and victim.collidepoint(p))
        kidnaper_killed = (self.alive and kidnaper.collidepoint(p))
        if victim_killed or kidnaper_killed: self.alive = False
        return (kidnaper_killed - victim_killed)
Пример #17
0
class Hide(retro.Sprite):
    IMG = retro.Image(asset("hide.png"))

    def __init__(self, window):
        self.window = window

        retro.Sprite.__init__(self, self.IMG)
        self.rect.center = self.window.rect().center

        self.hidden = False

    def update(self):
        self.hidden = self.window.events.key_hold(retro.K_LSHIFT)

    def draw(self, dest):
        if self.hidden:
            retro.Sprite.draw(self, dest)
Пример #18
0
class Pipes(list):
    IMG_BOTTOM = retro.Image(assets("pipe.png"))
    IMG_TOP = IMG_BOTTOM.copy()
    IMG_TOP.flip(x=True, y=True)
    GAP_HEIGHT = 100
    OFFSET_HEIGHT = 80

    def __init__(self, window):
        self.window = window
        list.__init__(self)
        self.generate()
        self.generate()

    def generate(self):
        x = self.window.rect().right if not self else \
            self[-1].left + (self.window.rect().right // 2)
        y = random.randrange(
            self.OFFSET_HEIGHT,
            self.window.rect().height - self.GAP_HEIGHT -
            Ground.IMG.rect().height - self.OFFSET_HEIGHT)

        ptop = retro.Sprite(self.IMG_TOP)
        ptop.rect.bottom = ptop.rect.top + y
        ptop.rect.left = x

        pbot = retro.Sprite(self.IMG_BOTTOM)
        pbot.rect.top = y + self.GAP_HEIGHT
        pbot.rect.left = x

        self.append(Pipe(ptop=ptop, pbot=pbot))

    def update(self):
        for pipe in self:
            pipe.ptop.rect.x -= Ground.SPEED
            pipe.pbot.rect.x -= Ground.SPEED

        # generate new pipe when the leftmost pipe starts to disappear
        if (len(self) == 2) and (self[0].left < 0): self.generate()
        # delete leftmost pipe when it goes out of screen
        if self[0].right <= 0: del self[0]

    def draw(self):
        for pipe in self:
            pipe.ptop.draw(self.window)
            pipe.pbot.draw(self.window)
Пример #19
0
class Runner(Enemy):
    RUNNER_IMG = retro.Image(asset("bandit_street3.png"), )

    def __init__(self, stage):
        Enemy.__init__(self, self.RUNNER_IMG)
        self.stage = stage
        self.dx = -2

    def move(self):
        self.rect.x += self.dx
        if not self.stage.image.rect().contains(self.rect):
            self.dx *= -1
            self.image.flip(x=True, y=False)
            self.rect.clamp_ip(self.stage.image.rect())

    def update(self, target):
        Enemy.update(self, target)
        self.move()
Пример #20
0
class DigV:
    ICON = retro.Image(asset("ui_digv.png"))

    def __init__(self, lemming):
        self.lemming = lemming

    def start(self):
        self.lemming.start_animation("DIGV")
        return self

    def run(self):
        dx = self.lemming.actions.walk.dx

        rect = self.lemming.rect.copy()
        rect.top = rect.bottom - 1
        rect.left += 10 if dx > 0 else 0
        rect.size = (20, 2)

        self.lemming.bg.original.draw_rect(retro.BLACK, rect)
        self.lemming.rect.move_ip(0, 1)
Пример #21
0
class DigH:
    ICON = retro.Image(asset("ui_digh.png"))

    def __init__(self, lemming):
        self.lemming = lemming

    def start(self):
        self.enabled = False
        return self

    def run(self):
        if not self.enabled:
            self.lemming.start_animation("DIGH")
            self.enabled = True

        dx = self.lemming.actions.walk.dx

        rect = self.lemming.rect.copy()
        rect.left = rect.right - 15 if dx > 0 else rect.left - 1
        rect.width = 16

        self.lemming.bg.original.draw_rect(retro.BLACK, rect)
        self.lemming.rect.move_ip(dx, 0)
Пример #22
0
class Ghost(Entity):
    IMG = retro.Image(assets("ghost.png"))
    BONUS = 200

    def __init__(self, pos):
        Entity.__init__(
            self,
            sprite=retro.Sprite(
                image=self.IMG,
                animations=retro.Animations(
                    frame_size=(32, 32),
                    period=3,
                    WALK_L=([0], 0),
                    WALK_U=([0], 0),
                    WALK_R=([0], 0),
                    WALK_D=([0], 0),
                    FEAR_L=([1], 0),
                    FEAR_U=([1], 0),
                    FEAR_R=([1], 0),
                    FEAR_D=([1], 0),
                ),
            ),
            pos=pos,
            speed=2,
            curdir=[0, 0],
            nxtdir=random.choice(([-1, 0], [1, 0])),
        )

        self.state = State(self)

    @property
    def bounding_rect(self):
        return Entity.bounding_rect(self, 12)

    def next_dir(self):
        dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]]
        opdir = numpy.negative(self.curdir).tolist()
        if opdir in dirs: dirs.remove(opdir)
        self.nxtdir = random.choice(dirs)

    def collide_maze(self, maze):
        if not self.nxtcol:
            self.curdir = self.nxtdir
        elif self.curcol is None:
            self.curdir = numpy.negative(self.curdir).tolist()

        elif self.curcol:
            self.next_dir()
            return True

        return False

    def kill(self):
        for g in self.groups:
            g.notify_kill()
        Entity.kill(self)

    def update(self, maze, player):
        self.state.update(player)
        if (self.curdir == self.nxtdir):
            self.next_dir()
        Entity.update(self, maze)
Пример #23
0
class Bonuses(list):
    IMG = retro.Image(assets('bonuses.png'))

    def __init__(self):
        self.count = 0
        list.__init__(self, [])
        for (i, x), (j, y) in Maze.iterator():
            if j == 0: self.append([])
            pos = (x + 6, y + 6)
            b = Bonus(pos, self.IMG[pos])
            self[i].append(b)
            if b: self.count += 1

    def copy(self):
        new = list.__new__(self.__class__)
        new.count = self.count
        list.__init__(new, [lst.copy() for lst in self])
        return new

    def iterator(self):
        for i, line in enumerate(self):
            for j, b in enumerate(line):
                yield i, j, b

    # Search inside growing neighborhoods until a bonus is found.
    def nearest(self, sprite):
        max_reach = max(len(Maze.RANGEW), len(Maze.RANGEH))

        for reach in range(0, max_reach):
            _, _, b = next(self.neighborhood(sprite, reach),
                           (None, None, None))
            if b: return b

        return None

    # Iterator yielding bonuses contained inside a neighborhood centered around
    # `sprite` and defined by a hollow rectangle of a specific `reach`.
    # examples:
    # reach = 0 | reach = 1 | reach = 2
    # ·····     | ·····     | ▫▫▫▫▫
    # ·····     | ·▫▫▫·     | ▫···▫
    # ··▫··     | ·▫s▫·     | ▫·s·▫
    # ·····     | ·▫▫▫·     | ▫···▫
    # ·····     | ·····     | ▫▫▫▫▫
    def neighborhood(self, sprite, reach=0):
        i, j = Maze.tile_pos(sprite.rect.center)
        if reach == 0: it = ((0, 0), )
        else:
            it = itertools.chain(
                itertools.product(range(-reach, reach + 1), (-reach, reach)),
                itertools.product((-reach, reach), range(-reach + 1, reach)),
            )

        for k, l in it:
            k += i
            l += j
            if not Maze.inside(k, l): continue
            b = self[k][l]
            if b: yield k, l, b

    def remove(self, i, j):
        self[i][j] = None
        self.count -= 1

    def draw(self, image):
        for _, _, b in self.iterator():
            if b: image.draw_img(b.image, b.rect)
Пример #24
0
 def __init__(self):
     self.tile_size = 20
     retro.Sprite.__init__(self, retro.Image(asset('maze.png')))
     self.image.scale(self.tile_size)
     self.init_items()
Пример #25
0
class Player(Entity):
    IMG = retro.Image(assets("pacman.png"))

    def __init__(self, pos):
        Entity.__init__(
            self,
            sprite=retro.Sprite(
                image=self.IMG,
                animations=retro.Animations(
                    frame_size=(32, 32),
                    period=3,
                    STOP_L=([2], 0),
                    STOP_U=([3], 0),
                    STOP_R=([0], 0),
                    STOP_D=([4], 0),
                    WALK_L=([2, 1], 0),
                    WALK_U=([3, 1], 0),
                    WALK_R=([0, 1], 0),
                    WALK_D=([4, 1], 0),
                ),
            ),
            pos=pos,
            speed=4,
        )

        self.score = 0
        self.powerup = Powerup()

    @property
    def bounding_rect(self):
        return Entity.bounding_rect(self, 4)

    def collide_maze(self, maze):
        if self.curcol is None:
            if self.curdir[0]:
                self.rect.centerx = abs(self.rect.centerx - maze.rect.w)
            if self.curdir[1]:
                self.rect.centery = abs(self.rect.centery - maze.rect.h)
        elif not self.nxtcol:
            self.set_animation("WALK")
            self.curdir = self.nxtdir
        elif self.curcol:
            self.set_animation("STOP")
            return True
        return False

    def collide_bonus(self, maze):
        for i, j, b in maze.bonuses.neighborhood(self):
            if not b.rect.colliderect(self.bounding_rect): continue
            maze.bonuses.remove(i, j)
            if (b.id == b.BONUS2.id): self.powerup.start()
            self.score += b.value

    # Returns 0 if no collision happened,
    #         1 if the player killed a ghost,
    #        -1 if a ghost killed the player.
    def collide_ghost(self, ghosts):
        g = next((g for g in ghosts
                  if g.bounding_rect.colliderect(self.bounding_rect)), None)
        if not g: return 0

        if g.state != g.state.FEAR: return -1

        g.kill()
        self.score += self.powerup.mul * g.BONUS
        self.powerup.mul *= 2
        return 1

    def update(self, maze):
        self.collide_bonus(maze)
        Entity.update(self, maze)

    def draw_score(self, image):
        font = retro.Font(36)
        txt = retro.Sprite(
            font.render(
                text=f"SCORE: {self.score}",
                color=retro.WHITE,
                bgcolor=retro.BLACK,
            ))
        txt.rect.bottomleft = image.rect().bottomleft
        txt.draw(image)
Пример #26
0
    return draw


# ---

window = retro.Window(
    title='image',
    size=(800, 600),
    fps=30,
    headless='--interactive' not in sys.argv,
)
test_draw = TestDraw()
test_init = TestInit()
test_tf = TestTransform()


def render(target):
    target.fill(retro.WHITE)
    test_draw(target)
    test_init(target)
    test_tf(target)


if window.headless:
    render(window)
    # window.save(assets('expectation_image.png'))
    assert window == retro.Image(assets('expectation_image.png'))
else:
    window.loop(None, lambda: render(window))
Пример #27
0
class Lemming(retro.Sprite):
    IMG = retro.Image(asset("lemming.png"))

    def __init__(self, window, bg, position):
        self.window = window
        self.bg = bg

        revrange = lambda start, end: range(end - 1, start - 1, -1)
        retro.Sprite.__init__(
            self=self,
            image=self.IMG,
            animations=retro.Animations(
                frame_size=(30, 30),
                period=4,
                WALK_L=(range(0, 8), 0),
                WALK_R=(revrange(0, 8), 12),
                WALKA_L=(range(0, 8), 11),
                WALKA_R=(revrange(0, 8), 23),
                FALL_L=(range(0, 4), 1),
                FALL_R=(revrange(0, 4), 13),
                FLOAT_L=(range(0, 6), 3),
                FLOAT_R=(revrange(0, 6), 15),
                STOP_L=(range(0, 16), 4),
                STOP_R=(revrange(0, 16), 16),
                BOMB_L=(range(0, 14), 5),
                BOMB_R=(revrange(0, 14), 17),
                BUILD_L=(range(0, 16), 6),
                BUILD_R=(revrange(0, 16), 18),
                DIGV_L=(range(0, 16), 7),
                DIGV_R=(revrange(0, 16), 19),
                DIGH_L=(range(0, 12), 8),
                DIGH_R=(revrange(0, 12), 20),
                MINE_L=(range(0, 17), 9),
                MINE_R=(revrange(0, 17), 21),
                DEAD_L=(range(0, 16), 10),
                DEAD_R=(revrange(0, 16), 22),
            ),
        )
        self.rect.topleft = position

        self.actions = Actions(self)
        self.state = None

    @property
    def bounding_rect(self):
        dx = self.actions.walk.dx
        rect = self.rect.copy()
        rect.width //= 2
        if dx > 0: rect.left += rect.width
        return rect

    def set_animation(self, name):
        dx = self.actions.walk.dx
        if dx < 0: self.animations.set(f"{name}_L")
        elif dx > 0: self.animations.set(f"{name}_R")
        else: self.animations.set("NONE")

    def start_animation(self, name):
        dx = self.actions.walk.dx
        if dx < 0: self.animations.start(f"{name}_L")
        elif dx > 0: self.animations.start(f"{name}_R")
        else: self.animations.start("NONE")

    def collisions(self, surface):
        directions = retro.Collisions.pixel_mid(surface, self.bounding_rect,
                                                retro.BLACK).invert()

        outside = (None in directions)
        directions.replace(None, True)

        fall = not directions.down

        dx = self.actions.walk.dx
        side = (directions.vec[0] == dx)

        return types.SimpleNamespace(
            outside=outside,
            fall=fall,
            side=side,
        )

    def update(self, new_action):
        collisions_all = self.collisions(self.bg.image)
        collisions_bg = self.collisions(self.bg.original)

        if self.state is None:
            self.state = self.actions.fall.start()

        elif self.state == self.actions.walk:
            if collisions_all.fall:
                self.state = self.actions.fall.start()
            elif new_action:
                self.state = self.actions.from_class(new_action).start()
            else:
                self.actions.walk.run(collisions_all)

        elif self.state == self.actions.fall:
            if collisions_all.fall:
                self.actions.fall.run()
            elif self.actions.fall.dead:
                self.state = self.actions.dead.start()
            else:
                self.actions.fall.clamp()
                self.state = self.actions.walk.start()

        elif self.state == self.actions.float:
            if collisions_all.fall:
                self.actions.float.run()
            elif self.actions.float.enabled:
                self.state = self.actions.walk.start()
            else:
                self.actions.walk.run(collisions_all, pending_action=True)

        elif self.state == self.actions.stop:
            self.actions.stop.run()

        elif self.state == self.actions.bomb:
            if self.actions.bomb.ticker.finished or collisions_all.fall:
                self.actions.bomb.run()
            else:
                self.actions.walk.run(collisions_all, pending_action=True)

        elif self.state == self.actions.build:
            if self.actions.build.finished or collisions_bg.side:
                self.state = self.actions.walk.start()
            else:
                self.actions.build.run()

        elif self.state == self.actions.digv:
            if (not collisions_bg.fall) and (not collisions_bg.outside):
                self.actions.digv.run()
            else:
                self.state = self.actions.walk.start()

        elif self.state == self.actions.digh:
            if (collisions_bg.side and (not collisions_bg.fall)
                    and (not collisions_bg.outside)):
                self.actions.digh.run()
            elif collisions_all.fall or self.actions.digh.enabled:
                self.state = self.actions.walk.start()
            else:
                self.actions.walk.run(collisions_all, pending_action=True)

        elif self.state == self.actions.mine:
            if (collisions_bg.side and (not collisions_bg.fall)
                    and (not collisions_bg.outside)):
                self.actions.mine.run()
            elif collisions_all.fall or self.actions.mine.enabled:
                self.state = self.actions.walk.start()
            else:
                self.actions.walk.run(collisions_all, pending_action=True)

        elif self.state == self.actions.dead:
            self.actions.dead.run()

    def draw_bg(self):
        if self.state == self.actions.stop:
            retro.Sprite.draw(self, self.bg.image)

    def draw_screen(self):
        if self.state == self.actions.stop: return
        elif self.state == self.actions.bomb:
            self.actions.bomb.draw_ticker()

        retro.Sprite.draw(self, self.window)