import sys

import pyxel

# --- Internal render canvas (256) shown at display_scale 2 -> 512px window.
# This gives a 2x "zoom", a camera that follows the player, and doubles the
# apparent size of all text in one move.
CANVAS = 256
MAP_TILES = 60
MAP_PX = MAP_TILES * 8  # 480

# Code-defined sound slots (kept high to avoid clobbering res.pyxres music).
SFX_MUSIC_DRONE = 53   # walking A-minor bass (music channel 0)
SFX_MUSIC_MELODY = 54  # sparse calm melody (music channel 1)
SFX_STEP = 55     # soft footstep tick while walking
SFX_SPAWN = 56    # ominous purple-minion duplication
SFX_DUNK = 57     # banana dunked into the magic drink
SFX_CONVERT = 58  # purple minion turned yellow
SFX_LOSE = 59     # game-over sting
SFX_EAT = 60      # Minecraft-style "eating" chew on banana pickup
SFX_KEY = 61      # bright blip on key pickup
SFX_SWORD = 62    # sword swing
SFX_WIN = 63      # victory jingle


# Conveyor-belt tile IDs and push vectors. The conveyor pushes any actor
# standing on the tile by CONVEYOR_SPEED pixels per frame in the given
# direction (collisions still respected — they slam against walls).
TILE_CONVEYOR_UP    = 20
TILE_CONVEYOR_DOWN  = 21
TILE_CONVEYOR_LEFT  = 22
TILE_CONVEYOR_RIGHT = 23
CONVEYOR_VEC = {
    TILE_CONVEYOR_UP:    (0, -1),
    TILE_CONVEYOR_DOWN:  (0,  1),
    TILE_CONVEYOR_LEFT:  (-1, 0),
    TILE_CONVEYOR_RIGHT: (1,  0),
}
CONVEYOR_SPEED = 1               # gentle drift used for enemies
CONVEYOR_TRANSPORT_SPEED = 3     # locked auto-ride speed for the player

# Decorative warning-sign tile, placed near trap conveyors that push into a
# wall. Passable like floor — purely a visual heads-up to the player.
TILE_WARNING = 24
# Decorative caution-stripe floor tile, marks dangerous zones.
TILE_HAZARD = 25


# Hand-built 5x7 chunky font for the title (pyxel's default font is tiny).
CHUNKY_FONT = {
    "T": ["XXXXX", "..X..", "..X..", "..X..", "..X..", "..X..", "..X.."],
    "H": ["X...X", "X...X", "X...X", "XXXXX", "X...X", "X...X", "X...X"],
    "E": ["XXXXX", "X....", "X....", "XXXX.", "X....", "X....", "XXXXX"],
    "L": ["X....", "X....", "X....", "X....", "X....", "X....", "XXXXX"],
    "O": [".XXX.", "X...X", "X...X", "X...X", "X...X", "X...X", ".XXX."],
    "S": [".XXXX", "X....", "X....", ".XXX.", "....X", "....X", "XXXX."],
    "M": ["X...X", "XX.XX", "X.X.X", "X.X.X", "X...X", "X...X", "X...X"],
    "I": ["XXXXX", "..X..", "..X..", "..X..", "..X..", "..X..", "XXXXX"],
    "N": ["X...X", "XX..X", "X.X.X", "X..XX", "X...X", "X...X", "X...X"],
}


class Player:
    """The minion controlled by the player: position, stats, inventory, art."""

    SPEED = 2

    def __init__(self):
        # Animation frames keyed by (has_sword, min(inventory, 2)) -> 4 frames.
        self.animations = {
            (False, 0): [(0, 184, 13, 17), (0, 0, 13, 17), (24, 184, 21, 17), (0, 80, 13, 17)],
            (False, 1): [(80, 184, 20, 17), (24, 0, 20, 17), (56, 184, 24, 17), (0, 40, 20, 17)],
            (False, 2): [(153, 184, 27, 17), (57, 0, 27, 17), (113, 184, 27, 17), (17, 80, 27, 17)],
            (True, 0):  [(216, 176, 16, 25), (40, 16, 16, 25), (191, 176, 21, 25), (24, 40, 16, 25)],
            (True, 1):  [(232, 176, 20, 25), (64, 32, 20, 25), (8, 208, 24, 25), (40, 48, 20, 25)],
            (True, 2):  [(33, 208, 27, 25), (57, 80, 27, 25), (65, 208, 27, 25), (88, 16, 27, 25)],
        }
        self.reset()

    def reset(self):
        """Full reset for a brand-new game."""
        self.hp = 3
        self.has_sword = False
        self.magic_bananas = 0
        self.x = 16
        self.y = 224
        self.enter_level()

    def enter_level(self):
        """Per-level reset; keeps hp, sword and magic bananas (campaign carry)."""
        self.inventory = 0
        self.bananas = 0
        self.attack_timer = 0
        self.is_moving = False

    def hitbox_points(self, x, y):
        """Four corner sample points used for tile collision."""
        return [(x + 3, y + 3), (x + 10, y + 3), (x + 3, y + 14), (x + 10, y + 14)]

    def draw(self):
        frame_index = 1
        if self.is_moving:
            frame_index = (pyxel.frame_count // 5) % 4
        frames = self.animations[(self.has_sword, min(self.inventory, 2))]
        sx, sy, w, h = frames[frame_index]
        pyxel.blt(self.x, self.y, 0, sx, sy, w, h, 0)

        if self.attack_timer > 0:
            pyxel.blt(self.x + 10, self.y + 2, 0, 16, 24, 16, 16, 0)


class Enemy:
    """A chasing monster (res.pyxres sprite) or the level-4 Gru hunter."""

    def __init__(self, x, y, sprite, hp=3, speed=0.5,
                 gate_x=None, reset_on_hit=False, show_hp=False, is_gru=False,
                 phasing=False):
        self.start_x, self.start_y = x, y
        self.sprite = sprite          # (bank, sx, sy, w, h)
        self.max_hp = hp
        self.speed = speed
        self.gate_x = gate_x          # only chase once player passes this x
        self.reset_on_hit = reset_on_hit
        self.show_hp = show_hp
        self.is_gru = is_gru
        self.phasing = phasing        # ignore wall/water tiles while chasing
        self.reset()

    def reset(self):
        self.x = self.start_x
        self.y = self.start_y
        self.hp = self.max_hp
        self.alive = True

    def active(self, player):
        return self.alive and (self.gate_x is None or player.x > self.gate_x)

    def chase(self, app, player):
        """Move toward the player, sliding along walls; return True on contact.

        Movement is resolved one axis at a time and cancelled if the next step
        would enter a solid tile, so enemies and Gru respect the same terrain
        the player does instead of phasing through walls and water.
        """
        if not self.active(player):
            return False
        step_x = self.speed if self.x < player.x else -self.speed if self.x > player.x else 0
        if step_x and (self.phasing or not app.blocked(self.x + step_x, self.y)):
            self.x += step_x
        step_y = self.speed if self.y < player.y else -self.speed if self.y > player.y else 0
        if step_y and (self.phasing or not app.blocked(self.x, self.y + step_y)):
            self.y += step_y
        if not self.phasing:
            app.apply_conveyor(self)
        return abs(self.x - player.x) < 10 and abs(self.y - player.y) < 10

    def take_hit(self, player, reach=32):
        """Apply one point of sword damage if the player's centre is within
        a circular `reach` of the enemy's centre (not corner-to-corner)."""
        if not self.alive or self.is_gru:
            return
        _, _, _, w, h = self.sprite
        dx = (player.x + 6) - (self.x + w / 2)
        dy = (player.y + 8) - (self.y + h / 2)
        if dx * dx + dy * dy < reach * reach:
            self.hp -= 1
            if self.hp <= 0:
                self.alive = False


class Minion:
    """Level-4 actor: a wandering purple to be converted, or a yellow helper."""

    def __init__(self, x, y, team):
        self.x = float(x)
        self.y = float(y)
        self.team = team   # 'purple' | 'yellow'
        self.dx = 0.0
        self.dy = 0.0
        self.t = 0

    def _step(self, app, mx, my):
        if not app.blocked(self.x + mx, self.y):
            self.x += mx
        else:
            self.dx = -self.dx
        if not app.blocked(self.x, self.y + my):
            self.y += my
        else:
            self.dy = -self.dy

    def wander(self, app):
        self.t -= 1
        if self.t <= 0:
            self.t = pyxel.rndi(20, 50)
            self.dx = pyxel.rndi(-1, 1)
            self.dy = pyxel.rndi(-1, 1)
        self._step(app, self.dx * 0.6, self.dy * 0.6)

    def seek(self, app, target):
        if target is None:
            return
        sx = 1 if target.x > self.x else (-1 if target.x < self.x else 0)
        sy = 1 if target.y > self.y else (-1 if target.y < self.y else 0)
        self._step(app, sx * 1.2, sy * 1.2)


class Autopilot:
    """Demo bot: picks a per-level sub-goal and returns the keys to press.

    Inert unless App runs with autoplay on. Reuses the BFS + wall-slide idea
    from test_flow.py to navigate, and drives the same inputs a human would.
    """

    def __init__(self):
        self.reset()

    def reset(self):
        self.last_pos = None
        self.stuck = 0
        self.detour = []
        self.crossed = False   # level 1: already over the bridge?

    # --- helpers -------------------------------------------------------------
    def _tiles(self, app, tid):
        out = []
        for ty in range(MAP_TILES):
            for tx in range(MAP_TILES):
                if app.get_tile(tx, ty) == tid:
                    out.append((tx, ty))
        return out

    def _nearest(self, tiles, p):
        if not tiles:
            return None
        return min(tiles, key=lambda t: abs(t[0] * 8 - p.x) + abs(t[1] * 8 - p.y))

    def _bfs_dir(self, app, target, blocked):
        """Key for the first step of the shortest path to `target`, or None."""
        from collections import deque
        p = app.player
        sx, sy = (int(p.x) + 7) // 8, (int(p.y) + 8) // 8
        if target is None or (sx, sy) == target:
            return None
        prev = {(sx, sy): None}
        q = deque([(sx, sy)])
        while q:
            cx, cy = q.popleft()
            if (cx, cy) == target:
                break
            for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                nb = (cx + dx, cy + dy)
                if nb not in prev and 0 <= nb[0] < MAP_TILES and 0 <= nb[1] < MAP_TILES \
                        and not blocked(*nb):
                    prev[nb] = (cx, cy)
                    q.append(nb)
        if target not in prev:
            return None
        cur = target
        while prev[cur] != (sx, sy):
            cur = prev[cur]
        ntx, nty = cur
        if ntx > sx:
            return pyxel.KEY_D
        if ntx < sx:
            return pyxel.KEY_A
        if nty > sy:
            return pyxel.KEY_S
        return pyxel.KEY_W

    # --- per-level sub-goal --------------------------------------------------
    def _subgoal(self, app):
        """Return (target_tile_or_None, blocked_fn) for the current level."""
        p = app.player
        lvl = app.level

        if lvl == 1:
            def blocked(tx, ty):
                t = app.get_tile(tx, ty)
                if t in (14, 4, 15, 16, 17):   # walls, water, wrong-colour keys
                    return True
                if t == 12 and p.inventory < 2:  # locked yellow gate
                    return True
                return False

            if p.inventory < 2 and self._tiles(app, 2):
                return self._nearest(self._tiles(app, 2), p), blocked
            if not p.has_sword and self._tiles(app, 9):
                return self._nearest(self._tiles(app, 9), p), blocked
            if app.cables_activated < 2 and self._tiles(app, 5):
                return self._nearest(self._tiles(app, 5), p), blocked
            # Keys, sword and both levers done -> deal with the bridge, then exit.
            if p.x > 352:
                self.crossed = True   # past the water; the game reverts the bridge
            if self.crossed or app.bridge_exists:
                return self._nearest(self._tiles(app, 3), p), blocked
            if p.x > 240 and 170 < p.y < 260:
                return None, blocked   # wait in the build zone for the bridge
            return (33, 27), blocked

        def walls_only(tx, ty):
            return app.get_tile(tx, ty) == 14

        if lvl == 2:
            return self._nearest(self._tiles(app, 2), p), walls_only

        if lvl == 3:
            if p.bananas + p.magic_bananas < app.goal_magic and self._tiles(app, 2):
                return self._nearest(self._tiles(app, 2), p), walls_only
            return self._nearest(self._tiles(app, 8), p), walls_only

        # Level 4: walk into the nearest purple minion to convert it.
        if p.magic_bananas > 0:
            purples = [m for m in app.minions if m.team == "purple"]
            if purples:
                m = min(purples, key=lambda m: abs(m.x - p.x) + abs(m.y - p.y))
                return (int((m.x + 4) // 8), int((m.y + 4) // 8)), walls_only
        return None, walls_only  # out of ammo: let the yellow helpers finish

    # --- main decision -------------------------------------------------------
    def decide(self, app):
        st = app.status
        fc = pyxel.frame_count
        if st in ("START", "LEVELSTART"):
            return set(), ({pyxel.KEY_RETURN} if fc % 10 == 0 else set())
        if st in ("GAMEOVER", "WIN"):
            return set(), ({pyxel.KEY_R} if fc % 20 == 0 else set())

        p = app.player
        held, pressed = set(), set()

        # Swing the sword at any monster that gets close.
        if p.has_sword:
            for e in app.enemies:
                if e.alive and abs(e.x - p.x) < 22 and abs(e.y - p.y) < 22:
                    pressed.add(pyxel.KEY_SPACE)
                    break

        # Throw bananas into the cauldron when standing next to it.
        if app.level == 3 and p.bananas > 0 and app.near_cauldron(p):
            pressed.add(pyxel.KEY_B)

        target, blocked = self._subgoal(app)
        base = self._bfs_dir(app, target, blocked)
        move = self.detour.pop() if self.detour else base
        if move is not None:
            held.add(move)

        # Wall-slide recovery when a move makes no progress.
        pos = (int(p.x), int(p.y))
        if target is not None and pos == self.last_pos:
            self.stuck += 1
            if self.stuck >= 3:
                opts = (pyxel.KEY_W, pyxel.KEY_S) if base in (pyxel.KEY_A, pyxel.KEY_D) \
                    else (pyxel.KEY_A, pyxel.KEY_D)
                self.detour = [opts[pyxel.rndi(0, 1)]] * 5
                self.stuck = 0
        else:
            self.stuck = 0
        self.last_pos = pos
        return held, pressed


class App:
    def __init__(self, autoplay=False):
        self.autoplay = autoplay
        self.bot = Autopilot()
        self._bot_held = set()
        self._bot_pressed = set()
        try:
            pyxel.init(CANVAS, CANVAS, title="Minion Rescue - A Gru Mission",
                       display_scale=2, fps=60)
        except RuntimeError:
            pass

        try:
            pyxel.load("res2.pyxres")
        except:
            pyxel.images[0].rect(0, 0, 16, 16, 1)

        self.define_sounds()
        try:
            pyxel.playm(0, loop=True)
        except Exception:
            pass

        self.dialog_lines = [
            "Minion! Du hast dich in",
            "meinem Experiment-Komplex",
            "verirrt. Finde den Ausgang",
            "des Labyrinths und komm",
            "zurueck zu mir!",
        ]
        self.dialog_char = 0
        self.dialog_line = 0
        self.dialog_timer = 0
        self.intro_page = 0

        # Defaults so draw() is always safe before a level is built.
        self.level = 1
        self.score = 0
        self.combo = 1
        self.combo_timer = 0
        self.shake = 0
        self.enemies = []
        self.minions = []
        self.gru = None
        self.spawn_timer = 0
        self.spawn_interval = 480
        self.lose_threshold = 15
        self.goal_bananas = 8
        self.goal_magic = 5
        self.max_keys = 2
        self.player_start = (16, 224)
        self.cables_activated = 0
        self.bridge_exists = False
        self.bridge_width = 0
        self.level_title = ""
        self.level_goal = []

        self.player = Player()
        self.status = "START"

        pyxel.run(self.update, self.draw)

    # ------------------------------------------------------------------ audio
    def define_sounds(self):
        """Build SFX in code so they exist regardless of res.pyxres contents."""
        try:
            pyxel.sounds[SFX_MUSIC_DRONE].set(
                "a1a1a1a1c2c2e2e2d2d2c2c2a1a1g1a1",
                "t", "2" * 16, "n" * 16, 50)
            pyxel.sounds[SFX_MUSIC_MELODY].set(
                "c3rrre3rg3ra3rg3re3rrrc3rrrg3ra3rc4rg3re3rrr",
                "t", "3" * 32, "n" * 32, 25)
            pyxel.musics[0].set([SFX_MUSIC_DRONE], [SFX_MUSIC_MELODY], [], [])
            pyxel.sounds[SFX_STEP].set("c1", "n", "2", "f", 6)
            pyxel.sounds[SFX_SPAWN].set("c1e1", "n", "66", "ff", 14)
            pyxel.sounds[SFX_DUNK].set("g1c2e2", "t", "567", "nnf", 10)
            pyxel.sounds[SFX_CONVERT].set("c3g3c4", "p", "765", "nnf", 6)
            pyxel.sounds[SFX_LOSE].set("c2g1c1", "s", "765", "nnf", 18)
            pyxel.sounds[SFX_EAT].set("c1c0", "n", "65", "ff", 18)
            pyxel.sounds[SFX_KEY].set("c3e3g3", "p", "766", "nnn", 8)
            pyxel.sounds[SFX_SWORD].set("a2c2", "n", "73", "ff", 6)
            pyxel.sounds[SFX_WIN].set("c3e3g3c4", "s", "7777", "nnnf", 12)
        except Exception:
            pass

    def play(self, slot, ch=3):
        try:
            pyxel.play(ch, slot)
        except Exception:
            pass

    # ----------------------------------------------------------- score / juice
    def add_score(self, base):
        self.score += base * self.combo
        self.combo = min(self.combo + 1, 9)
        self.combo_timer = 90

    # ------------------------------------------------------------------ levels
    def new_game(self):
        self.level = 1
        self.score = 0
        self.combo = 1
        self.combo_timer = 0
        self.shake = 0
        self.player.reset()
        self.begin_level()

    def begin_level(self):
        self.player.enter_level()
        self.cables_activated = 0
        self.bridge_exists = False
        self.bridge_width = 0
        self.enemies = []
        self.minions = []
        self.gru = None
        self.spawn_timer = self.spawn_interval
        self.setup_level()
        self.status = "LEVELSTART"

    def advance_level(self):
        self.level += 1
        if self.level > 4:
            self.status = "WIN"
            self.play(SFX_WIN)
        else:
            self.begin_level()

    def setup_level(self):
        tm = pyxel.tilemaps[0]
        for x in range(MAP_TILES):
            for y in range(MAP_TILES):
                tm.pset(x, y, (0, 20))
        for i in range(MAP_TILES):
            tm.pset(i, 0, (14, 0))
            tm.pset(i, MAP_TILES - 1, (14, 0))
            tm.pset(0, i, (14, 0))
            tm.pset(MAP_TILES - 1, i, (14, 0))

        if self.level == 1:
            self.setup_level_1(tm)
        elif self.level == 2:
            self.setup_level_2(tm)
        elif self.level == 3:
            self.setup_level_3(tm)
        elif self.level == 4:
            self.setup_level_4(tm)

    def setup_level_1(self, tm):
        self.max_keys = 2
        self.level_title = "LEVEL 1"
        self.level_goal = ["Schnapp das Schwert (Monster!).", "Lege BEIDE Hebel um -> Bruecke.",
                           "Sammle 2 gelbe Schluessel,", "oeffne das gelbe Tor",
                           "und ab zum EXIT!"]
        self.player_start = (16, 224)

        tm.pset(58, 25, (0, 20))
        tm.pset(58, 26, (0, 20))

        for x in range(5, 30, 6):
            for y in range(1, 59):
                tm.pset(x, y, (14, 0))

        for dy in range(10, 15): tm.pset(5, dy, (0, 20))
        for dy in range(45, 50): tm.pset(5, dy, (0, 20))
        for dy in range(26, 31): tm.pset(5, dy, (0, 20))
        for dy in range(25, 30): tm.pset(11, dy, (0, 20))
        for dy in range(5, 10):  tm.pset(17, dy, (0, 20))
        for dy in range(50, 55): tm.pset(17, dy, (0, 20))
        for dy in range(27, 32): tm.pset(17, dy, (0, 20))
        for dy in range(20, 25): tm.pset(23, dy, (0, 20))
        for dy in range(35, 40): tm.pset(29, dy, (0, 20))

        for x in range(6, 11):  tm.pset(x, 20, (14, 0))
        for x in range(12, 17): tm.pset(x, 40, (14, 0))
        for x in range(18, 23): tm.pset(x, 15, (14, 0))
        for x in range(24, 29): tm.pset(x, 50, (14, 0))

        tm.pset(2, 2, (5, 0))
        tm.pset(20, 55, (5, 0))

        for x in range(35, 45):
            for y in range(1, 59):
                tm.pset(x, y, (4, 0))

        for x in range(48, 58):
            tm.pset(x, 15, (14, 0))
            tm.pset(x, 35, (14, 0))
        for y in range(15, 36):
            tm.pset(48, y, (14, 0))
            tm.pset(58, y, (14, 0))

        tm.pset(48, 25, (12, 0))
        tm.pset(48, 26, (0, 20))
        tm.pset(48, 27, (0, 20))
        tm.pset(53, 25, (3, 0))

        tm.pset(26, 45, (2, 0))
        tm.pset(8, 30, (2, 0))
        tm.pset(20, 10, (9, 0))

        # Caution stripes lining the chemical river near the bridge zone.
        for y in range(20, 36):
            tm.pset(34, y, (TILE_HAZARD, 0))
            tm.pset(45, y, (TILE_HAZARD, 0))

        self.enemies = [
            Enemy(400, 400, (1, 0, 0, 23, 19), hp=3, speed=0.5, reset_on_hit=True,
                  phasing=True),
            Enemy(53 * 8, 20 * 8, (1, 0, 24, 23, 19), hp=3, speed=0.5,
                  gate_x=45 * 8, show_hp=True),
        ]
        self.player.x, self.player.y = self.player_start

    def setup_level_2(self, tm):
        self.goal_bananas = 8
        self.level_title = "LEVEL 2"
        self.level_goal = ["Sammle ALLE Bananen!", "Pass auf die 3 Monster auf -",
                           "dein Schwert hilft dir."]
        self.player_start = (24, 24)

        W = (14, 0)
        OPEN = (0, 20)

        # Compartment maze: 3x2 grid of rooms connected by narrow doorways.
        # Vertical dividers at x=20 and x=40, horizontal divider at y=30.
        for y in range(1, 59):
            tm.pset(20, y, W)
            tm.pset(40, y, W)
        for x in range(1, 59):
            tm.pset(x, 30, W)
        # Doorways through each divider (4 tiles wide so the player fits).
        for y in (14, 15, 16, 17, 44, 45, 46, 47): tm.pset(20, y, OPEN)
        for y in (19, 20, 21, 22, 49, 50, 51, 52): tm.pset(40, y, OPEN)
        for x in (9, 10, 11, 12, 29, 30, 31, 32, 49, 50, 51, 52):
            tm.pset(x, 30, OPEN)

        # Internal walls inside each room — forces actual navigation.
        # TL room
        for x in range(5, 12): tm.pset(x, 8, W)
        for y in range(15, 22): tm.pset(8, y, W)
        # TM room
        for x in range(25, 36): tm.pset(x, 12, W)
        for y in range(18, 25): tm.pset(28, y, W)
        # TR room
        for x in range(45, 55): tm.pset(x, 8, W)
        for y in range(15, 22): tm.pset(50, y, W)
        # BL room
        for x in range(5, 12): tm.pset(x, 40, W)
        for y in range(45, 55): tm.pset(12, y, W)
        # BM room
        for x in range(25, 36): tm.pset(x, 42, W)
        for y in range(48, 55): tm.pset(32, y, W)
        # BR room
        for x in range(45, 55): tm.pset(x, 42, W)
        for y in range(45, 53): tm.pset(48, y, W)

        # === Conveyor puzzle layout ===

        # IDEA #1 — Banana delivery alcove in TR.
        # A long downward conveyor at x=55-56 transports the player from
        # the top of the room into a small walled spot where a banana sits.
        # You CAN walk in from the south, but the conveyor is the obvious
        # and fast way to reach it from above.
        for x in (55, 56):
            for y in range(8, 22): tm.pset(x, y, (TILE_CONVEYOR_DOWN, 0))
        tm.pset(54, 22, W)   # west wall of the delivery alcove
        # Banana itself is placed in the banana list below at (55, 22).

        # IDEA #3 — Monster-trap conveyor in BL.
        # An east-flowing strip ends at the internal x=12 wall. When the
        # BL monster wanders onto it, the slow drift pins it against the
        # wall. The player approaches from above/below and swings.
        # NB: this is also a TRAP for the player — riding it = game over.
        # Warning signs around every entry point flag the danger.
        tm.pset(12, 42, W)   # extend wall up so the trap has a stopper
        tm.pset(12, 43, W)
        for y in (42, 43):
            for x in range(3, 12): tm.pset(x, y, (TILE_CONVEYOR_RIGHT, 0))
        # Warning signs flanking the conveyor (west, north, south entries).
        tm.pset(2, 42, (TILE_WARNING, 0))
        tm.pset(2, 43, (TILE_WARNING, 0))
        for x in (4, 7, 10):
            tm.pset(x, 41, (TILE_WARNING, 0))   # north side
            tm.pset(x, 44, (TILE_WARNING, 0))   # south side
        # Caution stripes at the trap-conveyor corners.
        tm.pset(3, 41, (TILE_HAZARD, 0))
        tm.pset(11, 41, (TILE_HAZARD, 0))
        tm.pset(3, 44, (TILE_HAZARD, 0))
        tm.pset(11, 44, (TILE_HAZARD, 0))

        # IDEA #6 — One-way TR -> BR conveyor at the divider doorway.
        # The 4-tile-wide opening at x=49-52, y=30 is replaced by a
        # downward conveyor. Going TR -> BR rides through cleanly; going
        # BR -> TR is bounced back, forcing the player to take the
        # BR -> BM -> TM -> TR detour.
        for x in range(49, 53):
            for y in range(28, 32): tm.pset(x, y, (TILE_CONVEYOR_DOWN, 0))

        # Keep the leftward TM strip — blocks direct TL -> TM entry so the
        # player has to detour TL -> BL -> BM -> TM (another forced detour
        # in the same spirit as idea #6).
        for y in (15, 16):
            for x in range(22, 30): tm.pset(x, y, (TILE_CONVEYOR_LEFT, 0))

        # Bananas — (55, 20) moved to (55, 22) to be the alcove payoff.
        for (bx, by) in [(5, 4), (15, 25), (35, 5), (45, 4),
                         (55, 22), (10, 50), (35, 55), (50, 55)]:
            tm.pset(bx, by, (2, 0))

        # Three monsters, one per "zone".
        self.enemies = [
            Enemy(35 * 8, 18 * 8, (1, 0, 0, 23, 19), speed=0.6, reset_on_hit=True),
            Enemy(10 * 8, 50 * 8, (1, 0, 24, 23, 19), speed=0.5, reset_on_hit=True),
            Enemy(50 * 8, 45 * 8, (1, 0, 0, 23, 19), speed=0.7, reset_on_hit=True),
        ]
        self.player.x, self.player.y = self.player_start

    def setup_level_3(self, tm):
        self.goal_magic = 5
        self.level_title = "LEVEL 3"
        self.level_goal = ["Sammle Bananen und bringe", "sie zum Kessel,",
                           "um Zauberbananen zu brauen."]
        self.player_start = (24, 40)

        W = (14, 0)
        OPEN = (0, 20)

        # Central cauldron chamber — walled on all four sides with a single
        # narrow north entrance, so the player must commit to a return trip.
        for x in range(25, 36):
            tm.pset(x, 26, W)
            tm.pset(x, 34, W)
        for y in range(26, 35):
            tm.pset(25, y, W)
            tm.pset(35, y, W)
        # Wide entrance so the player fits comfortably.
        for x in (28, 29, 30, 31): tm.pset(x, 26, OPEN)

        # Cauldron at the centre of the chamber.
        tm.pset(30, 30, (8, 0))

        # Caution stripes outside the cauldron-chamber entrance.
        for x in (28, 29, 30, 31):
            tm.pset(x, 25, (TILE_HAZARD, 0))

        # Outer maze: winding L-shaped wall sections in each quadrant.
        # Top-left
        for y in range(8, 20): tm.pset(15, y, W)
        for x in range(8, 16): tm.pset(x, 20, W)
        # Top-right
        for x in range(38, 50): tm.pset(x, 12, W)
        for y in range(12, 24): tm.pset(50, y, W)
        # Bottom-left
        for y in range(36, 50): tm.pset(15, y, W)
        for x in range(5, 18): tm.pset(x, 50, W)
        # Bottom-right
        for x in range(38, 52): tm.pset(x, 42, W)
        for y in range(42, 55): tm.pset(45, y, W)

        # Diagonal funnels guiding the player toward the cauldron entrance.
        for x in range(18, 25): tm.pset(x, 22, W)
        for y in range(20, 26): tm.pset(20, y, W)
        for x in range(36, 42): tm.pset(x, 38, W)
        for y in range(34, 40): tm.pset(40, y, W)

        # Bananas — 10 around the perimeter (need 5 dunks).
        for (bx, by) in [(5, 5), (22, 8), (45, 5), (55, 5),
                         (52, 28), (5, 30), (52, 45),
                         (22, 55), (8, 55), (38, 55)]:
            tm.pset(bx, by, (2, 0))
        self.player.x, self.player.y = self.player_start

    def setup_level_4(self, tm):
        self.spawn_interval = 480       # ~8s between duplication waves
        self.lose_threshold = 15        # too many purple minions -> you lose
        self.level_title = "LEVEL 4"
        self.level_goal = ["Verwandle alle lila Minions", "mit Zauberbananen zu gelb!",
                           "Sie vermehren sich - bei zu", "vielen hast du verloren!"]
        self.player_start = (240, 240)  # tile (30, 30) — centre of map

        W = (14, 0)
        OPEN = (0, 20)

        # Central starting chamber with four cardinal exits.
        for x in range(25, 36):
            tm.pset(x, 25, W)
            tm.pset(x, 35, W)
        for y in range(25, 36):
            tm.pset(25, y, W)
            tm.pset(35, y, W)
        for d in (-1, 0, 1, 2):
            tm.pset(29 + d, 25, OPEN)
            tm.pset(29 + d, 35, OPEN)
            tm.pset(25, 29 + d, OPEN)
            tm.pset(35, 29 + d, OPEN)

        # Caution stripes ringing the central chamber's 4 exits.
        for d in (-1, 0, 1, 2):
            tm.pset(29 + d, 24, (TILE_HAZARD, 0))   # outside N exit
            tm.pset(29 + d, 36, (TILE_HAZARD, 0))   # outside S exit
            tm.pset(24, 29 + d, (TILE_HAZARD, 0))   # outside W exit
            tm.pset(36, 29 + d, (TILE_HAZARD, 0))   # outside E exit

        # Four corner chambers — each a 10x10 box with one opening
        # toward the centre. Purple minions wander inside, can be cornered.
        # Top-left
        for x in range(5, 16):
            tm.pset(x, 5, W); tm.pset(x, 16, W)
        for y in range(5, 17):
            tm.pset(5, y, W); tm.pset(16, y, W)
        for d in (-1, 0, 1, 2): tm.pset(9 + d, 16, OPEN)
        # Top-right
        for x in range(44, 55):
            tm.pset(x, 5, W); tm.pset(x, 16, W)
        for y in range(5, 17):
            tm.pset(44, y, W); tm.pset(55, y, W)
        for d in (-1, 0, 1, 2): tm.pset(49 + d, 16, OPEN)
        # Bottom-left
        for x in range(5, 16):
            tm.pset(x, 44, W); tm.pset(x, 55, W)
        for y in range(44, 56):
            tm.pset(5, y, W); tm.pset(16, y, W)
        for d in (-1, 0, 1, 2): tm.pset(9 + d, 44, OPEN)
        # Bottom-right
        for x in range(44, 55):
            tm.pset(x, 44, W); tm.pset(x, 55, W)
        for y in range(44, 56):
            tm.pset(44, y, W); tm.pset(55, y, W)
        for d in (-1, 0, 1, 2): tm.pset(49 + d, 44, OPEN)

        # Twisty connecting corridors between the corners and the centre.
        for x in range(17, 25): tm.pset(x, 12, W)
        for y in range(12, 20): tm.pset(20, y, W)
        for y in range(17, 25): tm.pset(42, y, W)
        for x in range(35, 43): tm.pset(x, 20, W)
        for x in range(35, 43): tm.pset(x, 40, W)
        for y in range(40, 48): tm.pset(40, y, W)
        for y in range(35, 43): tm.pset(18, y, W)
        for x in range(17, 25): tm.pset(x, 40, W)

        # Short dead-end stubs to make chase paths trickier.
        for x in range(12, 18): tm.pset(x, 25, W)
        for x in range(42, 48): tm.pset(x, 25, W)
        for x in range(12, 18): tm.pset(x, 35, W)
        for x in range(42, 48): tm.pset(x, 35, W)

        # One purple minion seeded in each corner chamber, plus one extra.
        self.minions = [
            Minion(80, 80, "purple"),     # top-left chamber
            Minion(400, 80, "purple"),    # top-right chamber
            Minion(80, 400, "purple"),    # bottom-left chamber
            Minion(400, 400, "purple"),   # bottom-right chamber
            Minion(240, 60, "purple"),    # top corridor
        ]
        self.gru = None
        self.spawn_timer = self.spawn_interval

        # The magic bananas brewed in level 3 are the ammo here. Guarantee a
        # starting stock so the level is winnable even if reached out of order.
        if self.player.magic_bananas < 5:
            self.player.magic_bananas = 5
        self.player.x, self.player.y = self.player_start

    # ------------------------------------------------------------------ tiles
    def get_tile(self, x, y):
        val = pyxel.tilemaps[0].pget(x, y)
        if isinstance(val, tuple):
            return val[0] if val else 0
        return val

    def blocked(self, x, y):
        tx = int((x + 4) // 8)
        ty = int((y + 5) // 8)
        if tx < 0 or ty < 0 or tx >= MAP_TILES or ty >= MAP_TILES:
            return True
        t = self.get_tile(tx, ty)
        return t == 14 or t == 4

    def _conveyor_dir_under(self, actor):
        """Return the conveyor tile id under the actor's centre, or None.

        Sampling at the visual centre means the player must clearly stand
        on the strip — and since strips are now 2 tiles thick perpendicular
        to flow, the window is wide enough that you can't 'walk over' it.
        """
        if hasattr(actor, "sprite"):
            _, _, _, w, h = actor.sprite
            cx = (actor.x + w // 2) // 8
            cy = (actor.y + h // 2) // 8
        else:
            cx = (actor.x + 6) // 8
            cy = (actor.y + 8) // 8
        tile = self.get_tile(cx, cy)
        return tile if tile in CONVEYOR_VEC else None

    def apply_conveyor(self, actor):
        """Gentle drift used for enemies (not the player). Walls block."""
        cdir = self._conveyor_dir_under(actor)
        if cdir is None:
            return
        vec = CONVEYOR_VEC[cdir]
        dx = vec[0] * CONVEYOR_SPEED
        dy = vec[1] * CONVEYOR_SPEED
        if not self.blocked(actor.x + dx, actor.y + dy):
            actor.x += dx
            actor.y += dy

    def find_open_near(self, x, y):
        for _ in range(12):
            nx = max(16, min(x + pyxel.rndi(-24, 24), MAP_PX - 24))
            ny = max(16, min(y + pyxel.rndi(-24, 24), MAP_PX - 24))
            if not self.blocked(nx, ny):
                return nx, ny
        return max(16, min(x, MAP_PX - 24)), max(16, min(y, MAP_PX - 24))

    def reset_levers(self):
        tm = pyxel.tilemaps[0]
        if self.cables_activated > 0:
            self.cables_activated = 0
            self.bridge_exists = False
            for cx in range(MAP_TILES):
                for cy in range(MAP_TILES):
                    if self.get_tile(cx, cy) == 6:
                        tm.pset(cx, cy, (5, 0))
            for bx in range(35, 45):
                for by in range(24, 31):
                    if self.get_tile(bx, by) == 7:
                        tm.pset(bx, by, (4, 0))

    def nearest_purple(self, m):
        best, bd = None, 1e9
        for o in self.minions:
            if o.team == "purple":
                d = abs(o.x - m.x) + abs(o.y - m.y)
                if d < bd:
                    bd, best = d, o
        return best

    # ------------------------------------------------------------------- art
    def draw_minion_small(self, x, y, purple):
        if purple:
            self.draw_player_sprite_purple(x, y)
        else:
            pyxel.blt(x, y, 0, 0, 0, 13, 17, 0)

    def draw_player_sprite_purple(self, x, y):
        """Recolour the player's idle sprite as a spooky purple minion."""
        pyxel.pal(10, 2)   # yellow skin -> purple
        pyxel.blt(x, y, 0, 0, 0, 13, 17, 0)
        pyxel.pal()
        # Spooky red pupils painted over the original navy ones.
        pyxel.rect(x + 3, y + 4, 2, 2, 8)
        pyxel.rect(x + 8, y + 4, 2, 2, 8)

    def draw_gru_small(self, x, y):
        pyxel.rect(x + 1, y + 6, 11, 10, 0)
        pyxel.rect(x - 1, y + 8, 2, 5, 0)
        pyxel.rect(x + 12, y + 8, 2, 5, 0)
        pyxel.rect(x + 3, y, 8, 7, 9)
        pyxel.rect(x + 3, y + 1, 8, 1, 0)
        pyxel.rect(x + 3, y + 2, 3, 2, 7)
        pyxel.rect(x + 7, y + 2, 3, 2, 7)
        pyxel.pset(x + 4, y + 3, 0)
        pyxel.pset(x + 8, y + 3, 0)
        pyxel.rect(x + 6, y + 4, 2, 2, 9)

    def draw_minion_yellow(self, x, y):
        S = 2
        BODY, STRAP, RIM, GLASS = 10, 0, 9, 6
        DENIM, BLACK, MOUTH = 12, 0, 4
        pyxel.rect(x + 0 * S, y + 0 * S, 10 * S, 13 * S, BLACK)
        pyxel.rect(x + 1 * S, y + 0 * S, 8 * S, 12 * S, BODY)
        pyxel.rect(x + 1 * S, y + 2 * S, 8 * S, 1 * S, STRAP)
        pyxel.rect(x + 1 * S, y + 1 * S, 3 * S, 3 * S, RIM)
        pyxel.rect(x + 6 * S, y + 1 * S, 3 * S, 3 * S, RIM)
        pyxel.rect(x + 2 * S, y + 2 * S, 1 * S, 1 * S, GLASS)
        pyxel.rect(x + 7 * S, y + 2 * S, 1 * S, 1 * S, GLASS)
        pyxel.pset(x + 2 * S, y + 2 * S, BLACK)
        pyxel.pset(x + 7 * S, y + 2 * S, BLACK)
        pyxel.rect(x + 3 * S, y + 5 * S, 4 * S, 1 * S, MOUTH)
        pyxel.pset(x + 2 * S, y + 5 * S - 1, BLACK)
        pyxel.pset(x + 7 * S, y + 5 * S - 1, BLACK)
        pyxel.rect(x + 1 * S, y + 7 * S, 8 * S, 5 * S, DENIM)
        pyxel.rect(x + 4 * S, y + 8 * S, 2 * S, 2 * S, BLACK)
        pyxel.rect(x + 2 * S, y + 7 * S, 1 * S, 3 * S, RIM)
        pyxel.rect(x + 7 * S, y + 7 * S, 1 * S, 3 * S, RIM)
        pyxel.rect(x + 0 * S, y + 7 * S, 1 * S, 3 * S, BODY)
        pyxel.rect(x + 9 * S, y + 7 * S, 1 * S, 3 * S, BODY)
        pyxel.rect(x + 1 * S, y + 11 * S, 3 * S, 2 * S, BODY)
        pyxel.rect(x + 6 * S, y + 11 * S, 3 * S, 2 * S, BODY)
        pyxel.rect(x + 1 * S, y + 13 * S, 3 * S, 1 * S, BLACK)
        pyxel.rect(x + 6 * S, y + 13 * S, 3 * S, 1 * S, BLACK)

    def draw_minion_purple(self, x, y):
        S = 2
        BODY, HAIR, RIM, GLASS = 2, 14, 5, 8
        DENIM, BLACK, FANG = 1, 0, 7
        pyxel.rect(x + 0 * S, y + 0 * S, 10 * S, 13 * S, BLACK)
        pyxel.rect(x + 1 * S, y + 0 * S, 8 * S, 12 * S, BODY)
        pyxel.rect(x + 2 * S, y - 1 * S, 1 * S, 1 * S, HAIR)
        pyxel.rect(x + 4 * S, y - 1 * S, 2 * S, 1 * S, HAIR)
        pyxel.rect(x + 7 * S, y - 1 * S, 1 * S, 1 * S, HAIR)
        pyxel.rect(x + 1 * S, y + 2 * S, 8 * S, 1 * S, BLACK)
        pyxel.rect(x + 1 * S, y + 1 * S, 3 * S, 3 * S, RIM)
        pyxel.rect(x + 6 * S, y + 1 * S, 3 * S, 3 * S, RIM)
        pyxel.rect(x + 2 * S, y + 2 * S, 1 * S, 1 * S, GLASS)
        pyxel.rect(x + 7 * S, y + 2 * S, 1 * S, 1 * S, GLASS)
        pyxel.rect(x + 3 * S, y + 6 * S, 4 * S, 1 * S, BLACK)
        pyxel.pset(x + 3 * S, y + 5 * S, FANG)
        pyxel.pset(x + 6 * S, y + 5 * S, FANG)
        pyxel.rect(x + 1 * S, y + 7 * S, 8 * S, 5 * S, DENIM)
        pyxel.rect(x + 2 * S, y + 7 * S, 1 * S, 3 * S, RIM)
        pyxel.rect(x + 7 * S, y + 7 * S, 1 * S, 3 * S, RIM)
        pyxel.rect(x + 0 * S, y + 7 * S, 1 * S, 3 * S, BODY)
        pyxel.rect(x + 9 * S, y + 7 * S, 1 * S, 3 * S, BODY)
        pyxel.rect(x + 1 * S, y + 11 * S, 3 * S, 2 * S, BODY)
        pyxel.rect(x + 6 * S, y + 11 * S, 3 * S, 2 * S, BODY)
        pyxel.rect(x + 1 * S, y + 13 * S, 3 * S, 1 * S, BLACK)
        pyxel.rect(x + 6 * S, y + 13 * S, 3 * S, 1 * S, BLACK)

    def draw_gru(self, x, y):
        """Confident Gru: bald oval head with thick brows, no neck (head
        sits right on the broad coat), two long grey-and-black striped
        scarf ends, thin legs apart for a planted stance."""
        SKIN = 15
        BLACK = 0
        WHITE = 7
        GRAY = 5

        # Bald oval head, standing upright. No neck — chin sits on coat.
        pyxel.rect(x + 7, y, 4, 2, SKIN)
        pyxel.rect(x + 5, y + 2, 8, 10, SKIN)
        pyxel.rect(x + 7, y + 12, 4, 2, SKIN)
        pyxel.rect(x + 8, y + 14, 2, 1, SKIN)

        # Thick eyebrows.
        pyxel.rect(x + 5, y + 3, 4, 2, BLACK)
        pyxel.rect(x + 11, y + 3, 4, 2, BLACK)

        # Small eyes with black pupils.
        pyxel.rect(x + 6, y + 6, 2, 2, WHITE)
        pyxel.rect(x + 12, y + 6, 2, 2, WHITE)
        pyxel.pset(x + 7, y + 7, BLACK)
        pyxel.pset(x + 12, y + 7, BLACK)

        # Long pointed nose down the centre.
        pyxel.rect(x + 9, y + 7, 1, 7, SKIN)

        # Small mouth.
        pyxel.line(x + 8, y + 11, x + 11, y + 11, BLACK)

        # Broad black coat torso (head meets coat directly — no neck).
        pyxel.rect(x + 3, y + 16, 14, 18, BLACK)

        # Two long thin scarf ends with grey-and-black stripes.
        for i in range(8):
            col = GRAY if i % 2 == 0 else BLACK
            pyxel.rect(x + 6, y + 14 + i * 2, 2, 2, col)
            pyxel.rect(x + 12, y + 14 + i * 2, 2, 2, col)

        # Thin arms with hands at the sides.
        pyxel.rect(x + 1, y + 16, 2, 14, BLACK)
        pyxel.rect(x + 17, y + 16, 2, 14, BLACK)
        pyxel.rect(x + 1, y + 30, 2, 2, SKIN)
        pyxel.rect(x + 17, y + 30, 2, 2, SKIN)

        # Thin legs apart — confident planted stance.
        pyxel.rect(x + 5, y + 34, 3, 10, BLACK)
        pyxel.rect(x + 12, y + 34, 3, 10, BLACK)

        # Shoes.
        pyxel.rect(x + 3, y + 44, 6, 2, BLACK)
        pyxel.rect(x + 11, y + 44, 6, 2, BLACK)

    def draw_banana_item(self, x, y):
        """Curved banana — elongated yellow body bulging left, green stem
        on top right, tip on bottom right."""
        pyxel.rect(x + 3, y + 0, 3, 1, 11)  # green stem (top)
        pyxel.rect(x + 2, y + 1, 4, 1, 10)
        pyxel.rect(x + 1, y + 2, 4, 1, 10)  # curves left
        pyxel.rect(x + 0, y + 3, 4, 1, 10)
        pyxel.rect(x + 0, y + 4, 4, 1, 10)  # bulge (leftmost)
        pyxel.rect(x + 1, y + 5, 4, 1, 10)  # curves back right
        pyxel.rect(x + 2, y + 6, 4, 1, 10)
        pyxel.rect(x + 3, y + 7, 3, 1, 10)  # tip (bottom)

    def draw_key_item(self, x, y, col):
        """Key with a 5x4 rectangular handle, shaft, and bit (teeth)."""
        pyxel.rectb(x, y, 5, 4, col)         # bigger hollow handle
        pyxel.pset(x + 2, y + 2, col)         # centre detail
        pyxel.rect(x + 2, y + 4, 1, 3, col)   # shaft
        pyxel.pset(x + 3, y + 6, col)         # tooth

    def draw_sword_item(self, x, y):
        pyxel.rect(x + 3, y, 1, 7, 6)
        pyxel.rect(x + 1, y + 7, 5, 1, 9)
        pyxel.rect(x + 3, y + 8, 1, 2, 4)

    def draw_conveyor(self, x, y, tile):
        """8x8 conveyor floor with chevrons scrolling in the push direction."""
        pyxel.rect(x, y, 8, 8, 5)   # dark grey belt base
        col = 6                     # light grey chevrons
        t = (pyxel.frame_count // 4) % 4
        if tile == TILE_CONVEYOR_DOWN:
            for off in (0, 4):
                cy = y + (off + t) % 8
                pyxel.line(x + 1, cy, x + 4, cy + 2, col)
                pyxel.line(x + 7, cy, x + 4, cy + 2, col)
        elif tile == TILE_CONVEYOR_UP:
            for off in (0, 4):
                cy = y + (off - t) % 8
                pyxel.line(x + 1, cy + 2, x + 4, cy, col)
                pyxel.line(x + 7, cy + 2, x + 4, cy, col)
        elif tile == TILE_CONVEYOR_RIGHT:
            for off in (0, 4):
                cx = x + (off + t) % 8
                pyxel.line(cx, y + 1, cx + 2, y + 4, col)
                pyxel.line(cx, y + 7, cx + 2, y + 4, col)
        elif tile == TILE_CONVEYOR_LEFT:
            for off in (0, 4):
                cx = x + (off - t) % 8
                pyxel.line(cx + 2, y + 1, cx, y + 4, col)
                pyxel.line(cx + 2, y + 7, cx, y + 4, col)

    def draw_lab_wall(self, x, y, tx, ty):
        """Industrial machinery wall. Navy outer frame + light-gray panel +
        white rivets so it reads as a wall against the dark-gray floor.
        Roughly 1 in 8 tiles is a wall-mounted CRT monitor."""
        if (tx * 7 + ty * 13 + 3) % 8 == 0:
            # Wall-mounted CRT monitor.
            pyxel.rect(x, y, 8, 8, 1)        # navy frame
            pyxel.rect(x + 1, y + 1, 6, 6, 0)  # black screen
            sy = (pyxel.frame_count // 6 + tx + ty * 3) % 5 + 1
            pyxel.line(x + 1, y + sy, x + 6, y + sy, 11)
            if (pyxel.frame_count // 25 + tx + ty) % 2 == 0:
                pyxel.pset(x + 6, y + 6, 8)
        else:
            # Plain riveted panel.
            pyxel.rect(x, y, 8, 8, 1)        # navy outer frame
            pyxel.rect(x + 1, y + 1, 6, 6, 6)  # light-gray panel
            pyxel.pset(x + 1, y + 1, 7)        # white rivets
            pyxel.pset(x + 6, y + 1, 7)
            pyxel.pset(x + 1, y + 6, 7)
            pyxel.pset(x + 6, y + 6, 7)
            pyxel.pset(x + 3, y + 3, 13)       # subtle indigo accent
            pyxel.pset(x + 4, y + 4, 13)

    def draw_chemical(self, x, y, tx, ty):
        """Toxic green chemical pool with shifting surface highlights
        and occasional bubbles."""
        pyxel.rect(x, y, 8, 8, 3)
        for i in range(2):
            wy = ((pyxel.frame_count // 5 + tx + ty * 2) + i * 4) % 8
            pyxel.line(x, y + wy, x + 7, y + wy, 11)
        if (pyxel.frame_count // 15 + tx * 3 + ty * 5) % 8 == 0:
            bx = (tx + pyxel.frame_count // 10) % 6 + 1
            pyxel.pset(x + bx, y + 4, 7)

    def draw_hazard(self, x, y):
        """Scrolling yellow-and-black diagonal caution stripes."""
        offset = (pyxel.frame_count // 6) % 4
        for py in range(8):
            for px in range(8):
                if ((px + py + offset) // 2) % 2 == 0:
                    pyxel.pset(x + px, y + py, 10)
                else:
                    pyxel.pset(x + px, y + py, 0)

    def draw_gate(self, x, y):
        """24x24 yellow keyed gate with three black bars and a title."""
        pyxel.rect(x, y, 24, 24, 10)
        pyxel.rectb(x, y, 24, 24, 0)
        pyxel.text(x + 4, y + 2, "GATE", 0)
        # Vertical bars below the label.
        for bx in (4, 11, 18):
            pyxel.rect(x + bx, y + 10, 2, 12, 0)
        # Top and bottom horizontal crossbars.
        pyxel.rect(x + 2, y + 10, 20, 2, 0)
        pyxel.rect(x + 2, y + 20, 20, 2, 0)

    def draw_exit(self, x, y):
        """24x24 green emergency exit sign with an arrow."""
        pyxel.rect(x, y, 24, 24, 11)
        pyxel.rectb(x, y, 24, 24, 0)
        pyxel.rectb(x + 1, y + 1, 22, 22, 7)
        pyxel.text(x + 4, y + 2, "EXIT", 0)
        # Right-pointing arrow body and head.
        pyxel.rect(x + 4, y + 14, 12, 2, 0)
        pyxel.tri(x + 14, y + 11, x + 14, y + 18, x + 21, y + 14, 0)

    def draw_warning_sign(self, x, y):
        """Yellow triangle warning with a black exclamation mark inside —
        marks a trap conveyor whose end is a wall (= game over)."""
        # Yellow filled triangle.
        pyxel.tri(x + 3, y, x, y + 6, x + 7, y + 6, 10)
        # Black outline.
        pyxel.line(x + 3, y, x, y + 6, 0)
        pyxel.line(x + 3, y, x + 7, y + 6, 0)
        pyxel.line(x, y + 6, x + 7, y + 6, 0)
        # Exclamation mark: short stroke + dot.
        pyxel.pset(x + 3, y + 2, 0)
        pyxel.pset(x + 3, y + 3, 0)
        pyxel.pset(x + 3, y + 5, 0)

    def draw_drink_station(self, x, y):
        bub = (pyxel.frame_count // 10) % 2
        pyxel.rect(x, y + 2, 8, 6, 5)
        pyxel.rectb(x, y + 2, 8, 6, 0)
        pyxel.rect(x + 1, y + 2, 6, 2, 12)
        pyxel.pset(x + 3, y + 1 - bub, 7)
        pyxel.pset(x + 5, y + 2 - bub, 7)

    def draw_big_text(self, text, x, y, col):
        """Normal-size text with a 1-pixel drop shadow for legibility."""
        pyxel.text(x + 1, y + 1, text, 0)
        pyxel.text(x, y, text, col)

    def draw_chunky_text(self, text, x, y, size, col):
        """Draw `text` using CHUNKY_FONT, each font-pixel as a size x size block."""
        cx = x
        for ch in text:
            if ch == " ":
                cx += 4 * size
                continue
            pattern = CHUNKY_FONT.get(ch)
            if not pattern:
                cx += 6 * size
                continue
            for py, row in enumerate(pattern):
                for px, c in enumerate(row):
                    if c == "X":
                        pyxel.rect(cx + px * size, y + py * size, size, size, col)
            cx += (len(pattern[0]) + 1) * size

    def draw_dialog_box(self, x, y, w, h):
        pyxel.rect(x, y, w, h, 0)
        pyxel.rectb(x, y, w, h, 10)
        pyxel.rectb(x + 1, y + 1, w - 2, h - 2, 10)

    def update_dialog(self):
        self.dialog_timer += 1
        if self.dialog_timer >= 3:
            self.dialog_timer = 0
            if self.dialog_line < len(self.dialog_lines):
                line = self.dialog_lines[self.dialog_line]
                self.dialog_char += 1
                if self.dialog_char > len(line):
                    self.dialog_char = 0
                    self.dialog_line += 1

    # ----------------------------------------------------------------- update
    # Input goes through these so the Autopilot can drive the same code paths.
    def held(self, key):
        if self.autoplay:
            return key in self._bot_held
        return pyxel.btn(key)

    def pressed(self, key):
        if self.autoplay:
            return key in self._bot_pressed
        return pyxel.btnp(key)

    def update(self):
        # The P toggle always reads the real keyboard so a human can take over.
        if pyxel.btnp(pyxel.KEY_P):
            self.autoplay = not self.autoplay
            self.bot.reset()

        if self.autoplay:
            self._bot_held, self._bot_pressed = self.bot.decide(self)

        if self.status == "START":
            self.update_dialog()
            if self.pressed(pyxel.KEY_RETURN) or self.pressed(pyxel.KEY_SPACE):
                if self.intro_page == 0:
                    if self.dialog_line < len(self.dialog_lines):
                        self.dialog_line = len(self.dialog_lines)
                        self.dialog_char = 0
                    else:
                        self.intro_page = 1
                else:
                    self.new_game()
            return

        if self.status == "LEVELSTART":
            if self.pressed(pyxel.KEY_RETURN) or self.pressed(pyxel.KEY_SPACE):
                self.status = "RUNNING"
            return

        if self.status != "RUNNING":
            if self.pressed(pyxel.KEY_R):
                self.new_game()
            return

        self.update_running()

    def update_running(self):
        p = self.player

        if self.combo_timer > 0:
            self.combo_timer -= 1
        else:
            self.combo = 1
        if self.shake > 0:
            self.shake -= 1

        p.is_moving = False
        collision = False

        # Movement: stepping onto a conveyor LOCKS WASD and auto-transports
        # the player to the end of the strip. Otherwise normal WASD + tile
        # collision applies.
        cdir = self._conveyor_dir_under(p)
        if cdir is not None:
            vec = CONVEYOR_VEC[cdir]
            dx = vec[0] * CONVEYOR_TRANSPORT_SPEED
            dy = vec[1] * CONVEYOR_TRANSPORT_SPEED
            if not self.blocked(p.x + dx, p.y + dy):
                p.x += dx
                p.y += dy
            p.is_moving = True
        else:
            new_x, new_y = p.x, p.y
            if self.held(pyxel.KEY_W):
                new_y -= Player.SPEED; p.is_moving = True
            if self.held(pyxel.KEY_S):
                new_y += Player.SPEED; p.is_moving = True
            if self.held(pyxel.KEY_A):
                new_x -= Player.SPEED; p.is_moving = True
            if self.held(pyxel.KEY_D):
                new_x += Player.SPEED; p.is_moving = True

            for px, py in p.hitbox_points(new_x, new_y):
                tx, ty = px // 8, py // 8
                tile = self.get_tile(tx, ty)
                if tile == 14 or tile == 4:
                    collision = True
                elif tile == 12:
                    # Yellow gate: opens with 2 yellow keys.
                    if p.inventory >= 2:
                        pyxel.tilemaps[0].pset(tx, ty, (0, 20))
                    else:
                        collision = True
                elif tile == 3:
                    if p.inventory < 2:
                        collision = True

            if not collision:
                p.x, p.y = new_x, new_y

        # Trap check: a conveyor only blocks against tile 14/4, but the
        # player's hitbox is wider than that single sample point. If the
        # conveyor pushed the player's centre into a wall tile, they are
        # clipped in with no way out — game over.
        if self.get_tile((p.x + 6) // 8, (p.y + 8) // 8) == 14:
            self.status = "GAMEOVER"
            self.play(SFX_LOSE)
            self.shake = 8
            return

        # Sword swing damages res-sprite enemies in range.
        if self.pressed(pyxel.KEY_SPACE) and p.has_sword:
            p.attack_timer = 10
            self.play(SFX_SWORD)
            for e in self.enemies:
                if e.gate_x is None or p.x > e.gate_x:
                    e.take_hit(p)
        if p.attack_timer > 0:
            p.attack_timer -= 1

        if p.is_moving and not collision and pyxel.frame_count % 12 == 0:
            self.play(SFX_STEP, ch=2)

        # Level-4: hand a magic banana to any purple minion you touch -> yellow.
        if self.level == 4 and p.magic_bananas > 0:
            for m in self.minions:
                if m.team == "purple" and abs(p.x - m.x) < 11 and abs(p.y - m.y) < 11:
                    p.magic_bananas -= 1
                    m.team = "yellow"
                    self.add_score(50)
                    self.play(SFX_CONVERT)
                    self.shake = 6
                    break

        # Level-3: auto-dunk carried bananas as soon as the player is near
        # the cauldron — no key press required (the B-key step was easy to
        # miss and made the level feel broken).
        if self.level == 3 and p.bananas > 0 and self.near_cauldron(p):
            p.magic_bananas += p.bananas
            self.add_score(20 * p.bananas)
            p.bananas = 0
            self.play(SFX_DUNK)
            self.shake = 4
            if p.magic_bananas >= self.goal_magic:
                self.advance_level()
                return

        if self.level == 1:
            self.update_levers(p)

        self.update_minions()

        # Enemies + Gru hunter contact.
        actors = list(self.enemies)
        if self.gru:
            actors.append(self.gru)
        for e in actors:
            if e.chase(self, p):
                p.hp -= 1
                p.x, p.y = self.player_start
                self.shake = 8
                if e.reset_on_hit:
                    e.reset()
                if self.level == 1:
                    self.reset_levers()
                if p.hp <= 0:
                    self.status = "GAMEOVER"
                    self.play(SFX_LOSE)
                    return

        self.update_pickups(p)

    def update_levers(self, p):
        tm = pyxel.tilemaps[0]
        cx = (p.x + 7) // 8
        cy = (p.y + 8) // 8
        # 3x3 area: flip any lever next to the player.
        for dy in (0, -1, 1):
            for dx in (0, -1, 1):
                if self.get_tile(cx + dx, cy + dy) == 5:
                    tm.pset(cx + dx, cy + dy, (6, 0))
                    self.cables_activated += 1
                    break
            else:
                continue
            break

        if self.cables_activated == 2 and not self.bridge_exists:
            if p.x > 240 and 170 < p.y < 260:
                if self.bridge_width < 80:
                    self.bridge_width += 1
                else:
                    self.bridge_exists = True
                    for bx in range(35, 45):
                        for by in range(24, 31):
                            tm.pset(bx, by, (7, 0))

        if p.x > 45 * 8 and self.bridge_exists:
            self.bridge_exists = False
            for bx in range(35, 45):
                for by in range(24, 31):
                    tm.pset(bx, by, (4, 0))

    def near_cauldron(self, p):
        cx, cy = (p.x + 7) // 8, (p.y + 8) // 8
        for dx in range(-2, 3):
            for dy in range(-2, 3):
                if self.get_tile(cx + dx, cy + dy) == 8:
                    return True
        return False

    def update_minions(self):
        if self.level != 4:
            return

        for m in self.minions:
            if m.team == "yellow":
                tgt = self.nearest_purple(m)
                m.seek(self, tgt)
                if tgt and tgt.team == "purple" and \
                        abs(m.x - tgt.x) < 10 and abs(m.y - tgt.y) < 10:
                    tgt.team = "yellow"
                    self.add_score(30)
                    self.play(SFX_CONVERT)
            else:
                m.wander(self)

        # Timed duplication of the surviving purple horde.
        self.spawn_timer -= 1
        if self.spawn_timer <= 0:
            self.spawn_timer = self.spawn_interval
            purples = [m for m in self.minions if m.team == "purple"]
            if purples:
                for m in purples:
                    if sum(1 for k in self.minions if k.team == "purple") >= self.lose_threshold:
                        break
                    nx, ny = self.find_open_near(m.x, m.y)
                    self.minions.append(Minion(nx, ny, "purple"))
                self.play(SFX_SPAWN)
                self.shake = 8

        purple_count = sum(1 for m in self.minions if m.team == "purple")
        if purple_count >= self.lose_threshold:
            # Too many purple minions overran the level.
            self.status = "GAMEOVER"
            self.play(SFX_LOSE)
        elif purple_count == 0:
            self.advance_level()

    def update_pickups(self, p):
        tm = pyxel.tilemaps[0]
        cx = (p.x + 7) // 8
        cy = (p.y + 8) // 8

        def find_nearby(target):
            """Locate `target` tile within a 3x3 area around the player."""
            for dy in (0, -1, 1):
                for dx in (0, -1, 1):
                    if self.get_tile(cx + dx, cy + dy) == target:
                        return cx + dx, cy + dy
            return None

        if self.level == 1:
            loc = find_nearby(2)   # yellow key
            if loc and p.inventory < self.max_keys:
                p.inventory += 1
                self.add_score(10)
                self.play(SFX_KEY)
                tm.pset(loc[0], loc[1], (0, 20))
            loc = find_nearby(9)   # sword
            if loc:
                p.has_sword = True
                tm.pset(loc[0], loc[1], (0, 20))
            loc = find_nearby(3)   # exit
            if loc and p.inventory >= 2:
                self.advance_level()
            return

        if self.level == 2:
            loc = find_nearby(2)   # banana
            if loc:
                p.bananas += 1
                self.add_score(10)
                self.play(SFX_EAT)
                tm.pset(loc[0], loc[1], (0, 20))
                if p.bananas >= self.goal_bananas:
                    self.advance_level()
            return

        if self.level == 3:
            loc = find_nearby(2)
            if loc:
                p.bananas += 1
                self.add_score(10)
                self.play(SFX_EAT)
                tm.pset(loc[0], loc[1], (0, 20))

    # ------------------------------------------------------------------- draw
    def draw(self):
        pyxel.cls(0)
        if self.status == "START":
            self.draw_start_screen()
        elif self.status == "LEVELSTART":
            self.draw_level_start()
        elif self.status == "GAMEOVER":
            self.draw_gameover()
        elif self.status == "WIN":
            self.draw_win()
        else:
            self.draw_world()

        if self.autoplay:
            pyxel.camera(0, 0)
            pyxel.text(CANVAS - 34, CANVAS - 8, "AUTO", 11)

    def draw_world(self):
        p = self.player
        cam_x = max(0, min(int(p.x) - 124, MAP_PX - CANVAS))
        cam_y = max(0, min(int(p.y) - 124, MAP_PX - CANVAS))
        if self.shake > 0:
            cam_x += pyxel.rndi(-2, 2)
            cam_y += pyxel.rndi(-2, 2)
        pyxel.camera(cam_x, cam_y)

        tx0 = max(0, cam_x // 8 - 1)
        ty0 = max(0, cam_y // 8 - 1)
        for ty in range(ty0, min(MAP_TILES, ty0 + 35)):
            for tx in range(tx0, min(MAP_TILES, tx0 + 35)):
                tile = self.get_tile(tx, ty)

                # Walls and chemical pools fully cover the tile — no floor
                # underneath. Everything else gets the lab checker floor.
                if tile == 14:
                    self.draw_lab_wall(tx * 8, ty * 8, tx, ty)
                    continue
                if tile == 4 or tile == 7:
                    self.draw_chemical(tx * 8, ty * 8, tx, ty)
                    continue

                # Quiet solid floor backdrop — leaves visual focus for walls,
                # items and hazard markings.
                pyxel.rect(tx * 8, ty * 8, 8, 8, 5)

                if tile == 5:
                    pyxel.blt(tx * 8, ty * 8, 0, 1, 59, 9, 7, 0)
                elif tile == 6:
                    pyxel.blt(tx * 8, ty * 8, 0, 0, 67, 9, 7, 0)
                elif tile == 2:
                    if self.level == 1:
                        self.draw_key_item(tx * 8 + 1, ty * 8 + 1, 10)
                    else:
                        self.draw_banana_item(tx * 8, ty * 8)
                elif tile == 8:
                    self.draw_drink_station(tx * 8, ty * 8)
                elif tile in CONVEYOR_VEC:
                    self.draw_conveyor(tx * 8, ty * 8, tile)
                elif tile == TILE_WARNING:
                    self.draw_warning_sign(tx * 8, ty * 8)
                elif tile == TILE_HAZARD:
                    self.draw_hazard(tx * 8, ty * 8)
                elif tile == 9:
                    pyxel.blt(tx * 8 - 8, ty * 8 - 8, 0, 16, 24, 16, 16, 0)

        # Second pass: big overlays for gate and exit so the surrounding
        # tile floors can't overwrite them.
        for ty in range(ty0, min(MAP_TILES, ty0 + 35)):
            for tx in range(tx0, min(MAP_TILES, tx0 + 35)):
                tile = self.get_tile(tx, ty)
                if tile == 12:
                    self.draw_gate(tx * 8, ty * 8)
                elif tile == 3:
                    self.draw_exit(tx * 8 - 8, ty * 8 - 8)

        if self.level == 1 and self.cables_activated == 2:
            pyxel.blt(280, 192, 0, 0, 118, self.bridge_width, 50, 0)

        for m in self.minions:
            self.draw_minion_small(int(m.x), int(m.y), m.team == "purple")

        self.player.draw()

        for e in self.enemies:
            if e.alive:
                bank, sx, sy, w, h = e.sprite
                pyxel.blt(e.x, e.y, bank, sx, sy, w, h, 0)
                if e.show_hp:
                    pyxel.text(int(e.x) - 2, int(e.y) - 8, f"{e.hp}", 7)
        if self.gru and self.gru.alive:
            self.draw_gru_small(int(self.gru.x), int(self.gru.y))

        pyxel.camera(0, 0)
        self.draw_hud()

    def draw_hud(self):
        # Slim full-width top bar so it never hides a corner block of the map.
        p = self.player
        pyxel.rect(0, 0, CANVAS, 9, 0)
        pyxel.text(2, 2, f"HP{p.hp} {self.level_title}", 7)
        pyxel.text(66, 2, f"SC{self.score}", 10)
        if self.combo > 1:
            pyxel.text(120, 2, f"x{self.combo}", 8)

        purple = sum(1 for m in self.minions if m.team == "purple")
        yellow = sum(1 for m in self.minions if m.team == "yellow")
        if self.level == 1:
            label, cur, goal, col = "GELB", p.inventory, 2, 10
        elif self.level == 2:
            label, cur, goal, col = "BAN", p.bananas, self.goal_bananas, 10
        elif self.level == 3:
            label, cur, goal, col = "MAG", p.magic_bananas, self.goal_magic, 12
        else:
            label, cur, goal, col = "RET", yellow, max(1, purple + yellow), 10

        pyxel.text(150, 2, f"{label}{cur}/{goal}", col)
        bx, bw = 204, 40
        pyxel.rect(bx, 3, bw, 3, 5)
        fill = int(bw * min(cur, goal) / goal) if goal else 0
        pyxel.rect(bx, 3, fill, 3, col)

        # Level 1: yellow-key gate tracker and sword status.
        if self.level == 1:
            pyxel.rect(0, 9, CANVAS, 8, 0)
            pyxel.text(2, 10, f"GELB {p.inventory}/2 (Tor)", 10)
            if p.has_sword:
                pyxel.text(176, 10, "SCHWERT", 9)

        # Level 4: ammo + threat (toward the lose threshold) + spawn countdown.
        if self.level == 4:
            pyxel.rect(0, 9, CANVAS, 8, 0)
            pyxel.text(2, 10, f"MAGIE {p.magic_bananas}  LILA {purple}/{self.lose_threshold}", 12)
            secs = max(0, self.spawn_timer // 60)
            pyxel.text(208, 10, f"{secs}s", 8 if secs <= 2 else 7)

    def draw_level_start(self):
        pyxel.camera(0, 0)
        pyxel.rect(0, 0, CANVAS, CANVAS, 1)
        self.draw_dialog_box(24, 64, 208, 128)
        self.draw_big_text(self.level_title, 96, 78, 10)
        y = 104
        for line in self.level_goal:
            pyxel.text(40, y, line, 7)
            y += 12
        if (pyxel.frame_count // 20) % 2 == 0:
            pyxel.text(78, 172, "[ ENTER ] START", 10)
        else:
            pyxel.text(78, 172, "[ ENTER ] START", 6)

    def draw_gameover(self):
        pyxel.camera(0, 0)
        pyxel.rect(60, 88, 136, 72, 0)
        pyxel.rectb(60, 88, 136, 72, 8)
        pyxel.text(96, 98, "GAME OVER", 8)
        self.draw_player_sprite_purple(122, 116)
        pyxel.text(78, 134, f"SCORE: {self.score}", 7)
        pyxel.text(86, 146, "DRUECKE 'R'", 7)

    def draw_win(self):
        pyxel.camera(0, 0)
        pyxel.rect(50, 80, 156, 88, 0)
        pyxel.rectb(50, 80, 156, 88, 11)
        self.draw_big_text("SIEG!", 104, 92, 11)
        self.draw_minion_yellow(112, 110)
        pyxel.text(74, 140, f"SCORE: {self.score}", 10)
        pyxel.text(80, 152, "DRUECKE 'R'", 7)

    def draw_start_screen(self):
        pyxel.camera(0, 0)
        pyxel.rect(0, 0, CANVAS, CANVAS, 1)

        for sx, sy in [(15, 10), (40, 8), (75, 20), (100, 6), (135, 16), (175, 9),
                       (210, 14), (235, 7), (30, 40), (65, 36), (150, 46), (195, 40),
                       (225, 32), (10, 52), (250, 16), (120, 70), (200, 66)]:
            pyxel.pset(sx, sy, 7)

        self.draw_chunky_text("THE LOST MINION", 42, 12, 2, 10)   # yellow
        pyxel.text(78, 29, "- a gru mission -", 6)

        self.draw_gru(16, 42)

        bob = (pyxel.frame_count // 15) % 2
        pyxel.circ(120, 70, 14, 9)
        pyxel.circ(120, 70, 12, 1)
        pyxel.blt(114, 56 - bob * 2, 0, 0, 0, 13, 17, 0)

        # Speech bubble: minion says "Hi Gru!"
        bx, by, bw, bh = 92, 36, 46, 14
        pyxel.rect(bx, by, bw, bh, 7)
        pyxel.rectb(bx, by, bw, bh, 0)
        pyxel.rect(bx + 22, by + bh - 1, 6, 1, 7)   # erase border under tail
        pyxel.tri(bx + 22, by + bh - 1, bx + 27, by + bh - 1,
                  bx + 25, by + bh + 4, 7)
        pyxel.line(bx + 22, by + bh - 1, bx + 25, by + bh + 4, 0)
        pyxel.line(bx + 27, by + bh - 1, bx + 25, by + bh + 4, 0)
        pyxel.text(bx + 9, by + 4, "Hi Gru!", 0)

        # Footprint trail — the lost minion walked off this way.
        prints = [(132, 96, 0), (150, 92, 1), (168, 96, 0), (186, 92, 1),
                  (204, 96, 0), (222, 92, 1), (240, 96, 0)]
        for fx, fy, side in prints:
            pyxel.rect(fx, fy, 4, 2, 0)        # sole
            if side == 0:
                pyxel.pset(fx, fy - 1, 0)      # heel sticks out left
                pyxel.pset(fx + 1, fy - 1, 0)
            else:
                pyxel.pset(fx + 2, fy - 1, 0)  # heel sticks out right
                pyxel.pset(fx + 3, fy - 1, 0)

        pyxel.line(8, 108, 248, 108, 10)

        if self.intro_page == 0:
            self.draw_intro_story()
        else:
            self.draw_intro_controls()

    def draw_intro_story(self):
        self.draw_dialog_box(8, 114, 240, 130)
        pyxel.text(14, 120, "GRU:", 10)
        y_pos = 132
        for i in range(self.dialog_line):
            if i < len(self.dialog_lines):
                pyxel.text(14, y_pos, self.dialog_lines[i], 7)
                y_pos += 10
            if y_pos > 216:
                break
        if self.dialog_line < len(self.dialog_lines):
            current = self.dialog_lines[self.dialog_line][:self.dialog_char]
            pyxel.text(14, y_pos, current, 7)
            if (pyxel.frame_count // 15) % 2 == 0:
                pyxel.text(14 + len(current) * 4, y_pos, "_", 10)
        if (pyxel.frame_count // 20) % 2 == 0:
            pyxel.text(80, 232, "[ ENTER ] WEITER", 10)
        else:
            pyxel.text(80, 232, "[ ENTER ] WEITER", 6)

    def draw_intro_controls(self):
        self.draw_dialog_box(8, 114, 240, 130)
        self.draw_big_text("STEUERUNG", 14, 120, 10)
        pyxel.text(14, 134, "W A S D", 7)
        pyxel.text(96, 134, "- Bewegen", 6)
        pyxel.text(14, 145, "LEERTASTE", 7)
        pyxel.text(96, 145, "- Schwert (Lvl 1-2)", 6)
        pyxel.text(14, 156, "R", 7)
        pyxel.text(96, 156, "- Neustart", 6)

        ly = 182
        self.draw_banana_item(16, ly)
        pyxel.text(30, ly + 2, "Banane = sammeln & werfen", 10)
        self.draw_key_item(16, ly + 13, 10)
        pyxel.text(30, ly + 13, "Gelber Schluessel = Tor (Lvl 1)", 10)
        self.draw_drink_station(16, ly + 23)
        pyxel.text(30, ly + 25, "Kessel = Zauberbananen brauen", 12)

        if (pyxel.frame_count // 20) % 2 == 0:
            pyxel.text(70, 232, "[ ENTER ] LOS GEHT'S!", 10)
        else:
            pyxel.text(70, 232, "[ ENTER ] LOS GEHT'S!", 6)


App(autoplay="--auto" in sys.argv)