import pyxel
import random
import math

# --- Constants ---
WIDTH  = 256
HEIGHT = 256
FPS    = 60

TITLE = "Wilma the Provider"

GROUND_Y   = 210          # ground surface y
LANE_COUNT = 3
LANE_XS    = [64, 128, 192]

PLAYER_W = 14             # hitbox width  (club is purely visual)
PLAYER_H = 22             # hitbox height

OBS_W = 20                # obstacle hitbox width

# Jumping
JUMP_VY   = -8.5
GRAVITY   = 0.38

# Speed
SCROLL_SPEED_INIT = 2.0
SCROLL_SPEED_MAX  = 7.5
SPEED_INCREMENT   = 0.0006

# Max scroll speed allowed per level
LEVEL_SPEED_MAX = {1: 2.8, 2: 3.5, 3: 4.5, 4: 5.0, 5: 7.5}

WIN_SCORE = 5000    # ~4 minutes of play

# Pyxel PICO-8 palette reference:
# 0=black  1=dark-blue  2=dark-maroon  3=dark-green
# 4=brown  5=dark-grey  6=light-grey   7=cream/white
# 8=red    9=orange    10=yellow      11=bright-green
# 12=sky   13=lavender 14=pink        15=peach/skin

COL_SKY      = 12   # sky blue
COL_SUN      = 10   # yellow sun
COL_CLOUD    = 7    # white clouds
COL_MTN_FAR  = 6    # light grey - far snow mountains
COL_MTN_MID  = 1    # dark blue - mid mountains
COL_ROCK     = 5    # dark grey rocks
COL_TREE_SIL = 3    # dark green treeline silhouette
COL_TREE_HI  = 11   # bright green highlight
COL_TRUNK    = 4    # brown trunk

COL_GROUND      = 4   # brown forest floor
COL_GROUND_DARK = 5   # dark ground
COL_GROUND_LINE = 11  # bright green grass top

COL_SKIN      = 15   # player skin (peach)
COL_FUR       = 4    # player fur / hair (brown)
COL_BROW      = 5    # dark brow ridge
COL_HAIR      = 5    # dark hair
COL_WOOD      = 4    # club handle
COL_CLUB      = 6    # club stone head (light grey)

COL_BOULDER   = 6    # obstacle boulder
COL_BOULDER_D = 5    # boulder dark side
COL_PTERO     = 2    # pterodactyl body (dark maroon)
COL_PTERO_EYE = 8    # pterodactyl eye (red)

COL_BONE      = 7    # bone handle (cream)
COL_BONE_SHD  = 13   # bone shadow
# Meat colours
COL_MEAT_RAW  = 8    # red raw meat
COL_MEAT_PINK = 14   # pink cooked/fat
COL_MEAT_FAT  = 7    # white fat/marbling
COL_MEAT_ROAST= 9    # orange roasted crust
COL_MEAT_DARK = 2    # dark maroon char marks

# Meat point values per kind
MEAT_POINTS = {"leg": 10, "steak": 25, "chop": 50, "roast": 100}

COL_SCORE     = 7    # white text
COL_HUD_BG    = 0    # black HUD panel
COL_ACCENT    = 9    # orange accent


# ---------------------------------------------------------------------------
class Player:
    def __init__(self):
        self.lane      = 1
        self.x         = float(LANE_XS[self.lane])
        self.y         = float(GROUND_Y - PLAYER_H)
        self.vy        = 0.0
        self.is_jumping = False
        self.is_ducking = False
        self.alive      = True
        self.target_x   = self.x
        self.walk_frame = 0   # 0-3 animation frame
        self.walk_tick  = 0   # ticks since last frame change

    def jump(self):
        if not self.is_jumping:
            self.vy = JUMP_VY
            self.is_jumping  = True
            self.is_ducking  = False

    def move_left(self):
        if self.lane > 0:
            self.lane    -= 1
            self.target_x = LANE_XS[self.lane]

    def move_right(self):
        if self.lane < LANE_COUNT - 1:
            self.lane    += 1
            self.target_x = LANE_XS[self.lane]

    def duck(self):
        if not self.is_jumping:
            self.is_ducking = True

    def stop_duck(self):
        self.is_ducking = False

    def update(self):
        diff = self.target_x - self.x
        self.x = self.target_x if abs(diff) < 2 else self.x + diff * 0.25

        if self.is_jumping:
            self.vy += GRAVITY
            self.y  += self.vy
            floor = GROUND_Y - PLAYER_H
            if self.y >= floor:
                self.y          = floor
                self.vy         = 0.0
                self.is_jumping = False

        # Walk animation – always advance while not ducking
        if not self.is_ducking:
            self.walk_tick += 1
            if self.walk_tick >= 10:
                self.walk_tick  = 0
                self.walk_frame = (self.walk_frame + 1) % 4

    @property
    def rect(self):
        w  = PLAYER_W
        h  = PLAYER_H // 2 if self.is_ducking else PLAYER_H
        yy = self.y + (PLAYER_H - h)
        return (int(self.x) - w // 2, int(yy), w, h)

    def draw(self):
        cx  = int(self.x)
        top = int(self.y)
        if self.is_ducking:
            self._draw_ducking(cx, top + PLAYER_H // 2)
        else:
            self._draw_standing(cx, top)

    def _draw_standing(self, cx, by):
        H  = 4   # brown hair
        Hd = 2   # dark hair depth / waistband

        # --- Hair: wide, voluminous, curly ---
        pyxel.rect(cx - 5, by,      11, 3, H)       # crown
        pyxel.rect(cx - 7, by + 2,  15, 4, H)       # wide volume
        pyxel.pset(cx - 5, by + 2,  Hd)             # curl depth L
        pyxel.pset(cx + 4, by + 2,  Hd)             # curl depth R
        # Side strands falling alongside face to neck
        for dy in range(5, 11):
            pyxel.pset(cx - 6, by + dy, H)
            pyxel.pset(cx + 5, by + dy, H)

        # --- Face ---
        pyxel.rect(cx - 4, by + 3,  9, 8, COL_SKIN)
        pyxel.pset(cx - 2, by + 6,  0)              # left eye
        pyxel.pset(cx + 2, by + 6,  0)              # right eye
        pyxel.pset(cx - 3, by + 8,  14)             # left blush
        pyxel.pset(cx + 3, by + 8,  14)             # right blush
        pyxel.pset(cx - 1, by + 10, 14)             # lips
        pyxel.pset(cx,     by + 10, 14)
        pyxel.pset(cx + 1, by + 10, 14)

        # --- Bare shoulders / upper torso ---
        pyxel.rect(cx - 4, by + 11, 9, 3, COL_SKIN)

        # --- Fur wrap skirt ---
        pyxel.rect(cx - 5, by + 13, 10, 9, H)       # brown base
        pyxel.rect(cx - 4, by + 13,  8, 2, Hd)      # dark waistband
        # Spotted leopard pattern (orange dots)
        for sx, sy in [
            (cx - 3, by + 15), (cx + 2, by + 15),
            (cx - 1, by + 16), (cx + 3, by + 17),
            (cx - 4, by + 17), (cx - 3, by + 18),
            (cx + 1, by + 19), (cx + 3, by + 19),
            (cx,     by + 20),
        ]:
            pyxel.pset(sx, sy, 9)
        pyxel.pset(cx,     by + 16, Hd)
        pyxel.pset(cx - 2, by + 19, Hd)

        # --- Arms & Legs: sideways stride animation ---
        # Frames 0-1: left arm/leg swing forward (toward direction of travel = rightward on screen)
        # Frames 2-3: right arm/leg swing forward — classic alternating stride
        f = self.walk_frame
        # x-axis stride offsets: +2 = step forward (right), -2 = step back (left)
        stride_fwd =  2
        stride_bck = -2

        # Left arm swings forward on 0-1, back on 2-3 (hangs slightly below shoulder)
        la_ox = stride_fwd if f in (0, 1) else stride_bck
        la_oy = 1          if f in (0, 1) else 0          # slight drop when forward
        pyxel.rect(cx - 7 + la_ox, by + 11 + la_oy, 3, 4, COL_SKIN)

        # Right arm opposite (club arm): back on 0-1, forward on 2-3
        ra_ox = stride_bck if f in (0, 1) else stride_fwd
        ra_oy = 0          if f in (0, 1) else 1
        pyxel.rect(cx + 5 + ra_ox, by + 11 + ra_oy, 3, 4, COL_SKIN)

        # --- Legs: stride in x, slight lift when stepping forward ---
        # Left leg forward on 0-1, back on 2-3
        ll_ox = stride_fwd if f in (0, 1) else stride_bck
        ll_oy = -1         if f in (0, 1) else 0   # lift when stepping forward
        pyxel.rect(cx - 4 + ll_ox, by + 20 + ll_oy, 3, 3, COL_SKIN)
        # Right leg opposite
        rl_ox = stride_bck if f in (0, 1) else stride_fwd
        rl_oy = 0          if f in (0, 1) else -1
        pyxel.rect(cx + 2 + rl_ox, by + 20 + rl_oy, 3, 3, COL_SKIN)

        # Club follows right arm
        self._draw_club_raised(cx + ra_ox, by + ra_oy)

    def _draw_ducking(self, cx, by):
        H  = 4
        Hd = 2
        # --- Hair (flat / pressed down when ducking) ---
        pyxel.rect(cx - 6, by,      13, 3, H)
        pyxel.pset(cx - 7, by + 1,  H)
        pyxel.pset(cx + 6, by + 1,  H)
        # --- Crouched face ---
        pyxel.rect(cx - 4, by + 2,  9, 5, COL_SKIN)
        pyxel.pset(cx - 2, by + 4,  0)              # eye
        pyxel.pset(cx + 2, by + 4,  0)
        pyxel.pset(cx - 3, by + 6,  14)             # blush
        pyxel.pset(cx + 3, by + 6,  14)
        pyxel.pset(cx,     by + 6,  14)             # lips
        # --- Crouched body + skirt ---
        pyxel.rect(cx - 5, by + 7,  11, 4, H)
        pyxel.rect(cx - 4, by + 7,   9, 2, Hd)     # waistband
        pyxel.pset(cx - 2, by + 9,  9)
        pyxel.pset(cx + 1, by + 9,  9)
        pyxel.pset(cx + 3, by + 10, 9)
        # Club swung forward horizontally while ducking
        self._draw_club_swing(cx, by)


    # --- club helpers ---
    def _draw_club_raised(self, cx, by):
        """Club held raised diagonally at right shoulder."""
        # grip (hand) → handle top, angled ~55° from horizontal
        gx, gy = cx + 7,  by + 14   # hand grip bottom of handle
        tx, ty = cx + 17, by - 2    # handle top where stone attaches

        # Handle – 3 lines = 3 px thick with highlight + shadow
        pyxel.line(gx + 1, gy, tx + 1, ty, 2)   # dark maroon shadow edge
        pyxel.line(gx,     gy, tx,     ty, 4)   # brown main
        pyxel.line(gx - 1, gy, tx - 1, ty, 9)   # orange sunlit edge

        # Rope binding (3 rows of alternating brown/orange)
        for i in range(3):
            pyxel.pset(tx - 1 + (i % 2), ty + i,     9)
            pyxel.pset(tx     + (i % 2), ty + i,     4)
            pyxel.pset(tx + 1 - (i % 2), ty + i + 1, 9)

    def _draw_club_swing(self, cx, by):
        """Club swung forward horizontally while ducking."""
        # Handle runs left→right from body
        gx, gy = cx + 4, by + 4    # grip end (near hand)
        ex, ey = cx + 20, by + 2   # far end (meets stone)

        pyxel.line(gx, gy + 1, ex, ey + 1, 2)  # shadow
        pyxel.line(gx, gy,     ex, ey,     4)  # brown main
        pyxel.line(gx, gy - 1, ex, ey - 1, 9)  # highlight

        # Rope binding
        for i in range(3):
            pyxel.pset(ex - i, ey - 1, 9)
            pyxel.pset(ex - i, ey,     4)
            pyxel.pset(ex - i, ey + 1, 9)


# Level config: {level: (score_threshold, [obstacle_kinds])}
# 'action' tells how a kind is avoided:
#   jump_low  -> small jump (over thin stick/small rock)
#   jump_high -> full jump  (over bush/big rock)
#   duck      -> duck       (under log/branch)
#   lane      -> change lane only (wide tree trunk blocks one lane)
LEVEL_CONFIG = {
    1: (   0,  ["rock", "rock", "rock"]),                              # mostly rocks
    2: ( 400,  ["rock", "rock", "ptero", "log"]),                     # + duck obstacles
    3: (1200,  ["rock", "rock", "ptero", "log", "bush"]),             # + bush
    4: (2500,  ["rock", "ptero", "log", "bush", "tree"]),             # + tree, balanced
    5: (3800,  ["rock", "ptero", "ptero", "log", "bush", "tree", "tree"]),  # max challenge
}
LEVEL_MAX = max(LEVEL_CONFIG)

# Hitbox per kind: (width, height, y_from_ground)
# y_from_ground = how far above GROUND_Y the bottom of the hitbox is
OBS_DEFS = {
    #           w    h    y_bottom_offset  action
    "rock":   (22,  18,   0,   "jump_high"),
    "ptero":  (26,  10,  12,   "duck"),      # at head height — must duck!
    "bush":   (26,  20,   0,   "jump_high"),
    "log":    (28,  14,  14,   "duck"),      # elevated — must duck
    "tree":   (14,  32,   0,   "jump_high"), # tall, must jump
}


# ---------------------------------------------------------------------------
class Obstacle:
    # Rock sizes: (hitbox_w, hitbox_h, visual_r)
    ROCK_SIZES = [(14, 12, 6), (22, 18, 9), (30, 24, 13)]

    def __init__(self, speed, kind="rock"):
        defn       = OBS_DEFS[kind]
        self.kind  = kind
        self.speed = speed
        self.alive = True
        ybot       = defn[2]
        if kind == "rock":
            rw, rh, self.rock_r = random.choice(self.ROCK_SIZES)
            self.w = rw
            self.h = rh
        else:
            self.w = defn[0]
            self.h = defn[1]
            self.rock_r = 9
        self.y = GROUND_Y - self.h - ybot
        self.x = float(WIDTH + self.w)

    def update(self):
        self.x -= self.speed
        if self.x < -(self.w + 20):
            self.alive = False

    @property
    def rect(self):
        return (int(self.x) - self.w // 2, self.y, self.w, self.h)

    def draw(self):
        cx = int(self.x)
        if   self.kind == "rock":  self._draw_rock(cx)
        elif self.kind == "ptero": self._draw_ptero(cx)
        elif self.kind == "bush":  self._draw_bush(cx)
        elif self.kind == "log":   self._draw_log(cx)
        else:                      self._draw_tree(cx)

    def _draw_rock(self, cx):
        r = self.rock_r
        cy = GROUND_Y - r
        # ground shadow
        pyxel.elli(cx - r, GROUND_Y - 2, r * 2, 3, 2)
        if r <= 6:
            # small: single rounded stone
            pyxel.circ(cx, cy, r, 5)
            pyxel.circ(cx - 1, cy - 1, max(1, r - 3), 6)   # light face
            pyxel.circb(cx, cy, r, 2)
            pyxel.pset(cx - 1, cy - r + 1, 7)              # highlight
        elif r <= 9:
            # medium: main boulder + small side rock
            pyxel.circ(cx, cy, r, 5)
            pyxel.circ(cx + r - 2, cy + 2, r - 4, 5)       # side pebble
            pyxel.circ(cx - 2, cy - 2, r - 3, 6)           # lit face
            pyxel.circb(cx, cy, r, 2)
            pyxel.circb(cx + r - 2, cy + 2, r - 4, 2)
            pyxel.pset(cx - 2, cy - r + 2, 7)
            pyxel.line(cx - 1, cy + 1, cx + 3, cy - 2, 2)  # crack
        else:
            # large: cluster of three boulders
            pyxel.circ(cx,      cy,      r,      5)          # main
            pyxel.circ(cx - r + 3, cy + 4, r - 5, 5)        # left satellite
            pyxel.circ(cx + r - 2, cy + 5, r - 4, 5)        # right satellite
            pyxel.circ(cx - 3,  cy - 3,  r - 5, 6)          # lit face
            pyxel.circb(cx,     cy,      r,      2)
            pyxel.circb(cx - r + 3, cy + 4, r - 5, 2)
            pyxel.circb(cx + r - 2, cy + 5, r - 4, 2)
            pyxel.pset(cx - 3, cy - r + 3, 7)               # highlight
            pyxel.line(cx + 1, cy - 1, cx + r - 4, cy + 3, 2)  # crack

    def _draw_ptero(self, cx):
        # pterodactyl flying at torso/head height — must duck!
        mid_y = self.y + 5
        flip  = (pyxel.frame_count // 6) % 2
        wy    = -8 if flip else -2
        # shadow on ground
        pyxel.elli(cx - 14, GROUND_Y - 2, 28, 4, 2)
        # left wing
        pyxel.tri(cx - 2,  mid_y,
                  cx - 20, mid_y + wy,
                  cx - 10, mid_y + 6, COL_PTERO)
        pyxel.trib(cx - 2, mid_y,
                   cx - 20, mid_y + wy,
                   cx - 10, mid_y + 6, 2)
        # right wing
        pyxel.tri(cx + 2,  mid_y,
                  cx + 14, mid_y + wy,
                  cx + 9,  mid_y + 5, COL_PTERO)
        pyxel.trib(cx + 2, mid_y,
                   cx + 14, mid_y + wy,
                   cx + 9,  mid_y + 5, 2)
        # body
        pyxel.rect(cx - 4, mid_y - 3, 11, 6, COL_PTERO)
        # head + beak
        pyxel.rect(cx + 6,  mid_y - 3, 10, 5, COL_PTERO)
        pyxel.tri(cx + 16, mid_y - 2,
                  cx + 22, mid_y + 1,
                  cx + 16, mid_y + 3, COL_PTERO)
        # crest on back of head
        pyxel.tri(cx + 6,  mid_y - 3,
                  cx + 10, mid_y - 8,
                  cx + 14, mid_y - 3, COL_PTERO)
        # eye
        pyxel.pset(cx + 9, mid_y - 1, COL_PTERO_EYE)
        # tail
        pyxel.line(cx - 4, mid_y + 2, cx - 11, mid_y + 7, COL_PTERO)

    def _draw_bush(self, cx):
        # rounded leafy bush on ground — bright green so it's easy to spot
        by = GROUND_Y
        # base clump: bright green fill
        pyxel.circ(cx,     by - 10, 10, COL_TREE_HI)
        pyxel.circ(cx - 8, by - 7,   7, COL_TREE_HI)
        pyxel.circ(cx + 8, by - 7,   7, COL_TREE_HI)
        # dark green outline dots for depth
        pyxel.circb(cx,     by - 10, 10, COL_TREE_SIL)
        pyxel.circb(cx - 8, by - 7,   7, COL_TREE_SIL)
        pyxel.circb(cx + 8, by - 7,   7, COL_TREE_SIL)
        # yellow highlight tips
        pyxel.pset(cx,      by - 18, 10)
        pyxel.pset(cx - 6,  by - 13, 10)
        pyxel.pset(cx + 7,  by - 13, 10)
        # short stem
        pyxel.rect(cx - 2, by - 2, 4, 3, COL_TRUNK)

    def _draw_log(self, cx):
        # horizontal floating log (duck under it)
        ly = GROUND_Y - 18 - 14   # top of log
        # support stumps on each side
        pyxel.rect(cx - 14, GROUND_Y - 8, 4, 8, COL_TRUNK)
        pyxel.rect(cx + 11, GROUND_Y - 8, 4, 8, COL_TRUNK)
        # log body
        pyxel.rect(cx - 14, ly, 28, 10, COL_TRUNK)
        pyxel.rect(cx - 14, ly, 28, 3,  COL_WOOD)  # lighter top face
        # bark rings
        pyxel.line(cx - 5, ly, cx - 5, ly + 10, COL_BOULDER_D)
        pyxel.line(cx + 5, ly, cx + 5, ly + 10, COL_BOULDER_D)
        pyxel.rectb(cx - 14, ly, 28, 10, COL_BOULDER_D)

    def _draw_tree(self, cx):
        # tall pine tree blocking path — bright layers so it's distinct from bg
        self._pine(cx, GROUND_Y, 38)

    def _pine(self, cx, gnd, h):
        th = h // 4
        fh = h - th
        ty = gnd - h
        fb = gnd - th
        # trunk — dark brown
        pyxel.rect(cx - 3, fb, 6, th, 2)
        pyxel.rect(cx - 2, fb, 2, th, 4)   # highlight stripe
        hw1 = fh * 44 // 100
        hw2 = fh * 28 // 100
        hw3 = fh * 15 // 100
        # bottom tier: bright green fill + dark outline
        pyxel.tri(cx, ty + fh//2,   cx - hw1, fb,          cx + hw1, fb,          COL_TREE_HI)
        pyxel.trib(cx, ty + fh//2,  cx - hw1, fb,          cx + hw1, fb,          COL_TREE_SIL)
        # mid tier
        pyxel.tri(cx, ty + fh//4,   cx - hw2, fb - fh//3,  cx + hw2, fb - fh//3,  COL_TREE_HI)
        pyxel.trib(cx, ty + fh//4,  cx - hw2, fb - fh//3,  cx + hw2, fb - fh//3,  COL_TREE_SIL)
        # top tier
        pyxel.tri(cx, ty,           cx - hw3, fb - fh*55//100, cx + hw3, fb - fh*55//100, COL_TREE_HI)
        pyxel.trib(cx, ty,          cx - hw3, fb - fh*55//100, cx + hw3, fb - fh*55//100, COL_TREE_SIL)
        # bright tip
        pyxel.pset(cx, ty,     10)
        pyxel.pset(cx, ty + 1, 10)


# ---------------------------------------------------------------------------
class Meat:
    """
    Single drumstick sprite scaled by point tier.
      small  (r=4)  = 10 pts  on the ground
      medium (r=6)  = 25 pts  low in the air
      large  (r=8)  = 50 pts  mid air
      huge   (r=11) = 100 pts jump apex
    """
    TIERS = [
        ("small",  4,  10,  4),   # (name, radius, pts, y_offset)
        ("medium", 6,  25, 22),
        ("large",  8,  50, 42),
        ("huge",  11, 100, 62),
    ]
    WEIGHTS = (["small"] * 5 + ["medium"] * 3 +
               ["large"] * 2 + ["huge"] * 1)
    TIER_MAP = {t[0]: t for t in TIERS}

    def __init__(self, lane, speed):
        name       = random.choice(self.WEIGHTS)
        _, r, pts, yoff = self.TIER_MAP[name]
        self.r     = r
        self.pts   = pts
        self.x     = float(WIDTH + r + 4)
        # Sit just on the ground for small; higher for bigger pieces
        self.y     = float(GROUND_Y - PLAYER_H - yoff)
        self.speed = speed
        self.alive = True

    def update(self):
        self.x -= self.speed
        if self.x < -(self.r + 12):
            self.alive = False

    def draw(self):
        if not self.alive:
            return
        bx, by = int(self.x), int(self.y)
        r = self.r
        hs = max(2, r // 2)   # handle length

        # Pulsing halo — radius grows/shrinks with frame count
        halo_r = r + 3 + ((pyxel.frame_count // 6) % 3)
        pyxel.circb(bx, by, halo_r,     10)   # yellow outer ring
        pyxel.circb(bx, by, halo_r - 1, 10)   # double-pixel for visibility

        # Bone handle (below blob)
        pyxel.rect(bx - 1, by + r,     3, hs + 2, COL_BONE)
        pyxel.circ(bx,     by + r + hs + 2, max(1, r // 3), COL_BONE)

        # Outer cooked crust (brown)
        pyxel.circ(bx, by, r, COL_TRUNK)
        # Inner raw meat (red-brown, slightly smaller)
        if r > 3:
            pyxel.circ(bx, by, r - 2, COL_MEAT_RAW)
        # Dark outline
        pyxel.circb(bx, by, r, COL_MEAT_DARK)
        # Char dot on surface
        pyxel.pset(bx + max(1, r // 3), by + max(1, r // 3), COL_MEAT_DARK)


# ---------------------------------------------------------------------------
class Enemy:
    """
    A rival caveman who chases the player after a level-up.
    Spawns off-screen right, runs in the player's lane.
    - Collides with player  -> steals 3 meats + 1 heart, then flees
    - Player changes lane   -> enemy gives up and scrolls away
    """
    SPEED_BONUS = 1.2   # faster than scroll so it catches up

    def __init__(self, player_lane, scroll_speed):
        self.lane      = player_lane
        self.x         = float(WIDTH + 20)
        self.y         = float(GROUND_Y - PLAYER_H)
        self.speed     = scroll_speed + self.SPEED_BONUS
        self.alive     = True
        self.fleeing   = False   # turns True after collision or avoidance
        self.frame     = 0

    def update(self, scroll_speed, player_lane):
        self.frame += 1
        self.speed  = scroll_speed + self.SPEED_BONUS
        if self.fleeing:
            # run back off the right side
            self.x += self.speed * 1.5
            if self.x > WIDTH + 40:
                self.alive = False
        else:
            self.x -= self.speed
            # player switched lane -> give up
            if player_lane != self.lane:
                self.fleeing = True
            # scrolled past player without contact -> gone
            if self.x < -30:
                self.alive = False

    @property
    def rect(self):
        return (int(self.x) - PLAYER_W // 2, int(self.y), PLAYER_W, PLAYER_H)

    def draw(self):
        if not self.alive:
            return
        cx = int(self.x)
        by = int(self.y)
        # simple rival caveman silhouette — reddish/orange
        # head
        pyxel.rect(cx - 5, by,      10, 8, 9)          # orange head
        pyxel.rect(cx - 5, by,      10, 2, 4)          # dark hair
        pyxel.pset(cx - 2, by + 4,  0)                 # left eye
        pyxel.pset(cx + 2, by + 4,  0)                 # right eye
        # angry brows (slanted inward)
        pyxel.line(cx - 4, by + 2, cx - 1, by + 3, 2)
        pyxel.line(cx + 4, by + 2, cx + 1, by + 3, 2)
        # body
        pyxel.rect(cx - 5, by + 8,  10, 8, 4)         # brown fur vest
        # club raised menacingly
        club_x = cx - 8 if (self.frame // 8) % 2 == 0 else cx - 10
        club_y = by - 6 if (self.frame // 8) % 2 == 0 else by - 4
        pyxel.rect(club_x, club_y,  3, 10, 4)
        pyxel.circ(club_x + 1, club_y, 4, 5)          # club head
        # legs (running anim)
        step = (self.frame // 5) % 2
        if step == 0:
            pyxel.rect(cx - 4, by + 16, 4, 6, 4)
            pyxel.rect(cx + 1, by + 16, 4, 4, 4)
        else:
            pyxel.rect(cx - 4, by + 16, 4, 4, 4)
            pyxel.rect(cx + 1, by + 16, 4, 6, 4)


# ---------------------------------------------------------------------------
class HeartItem:
    """A floating heart collectible that restores 1 health when picked up."""

    def __init__(self, lane, speed):
        self.x     = float(WIDTH + 12)
        self.y     = float(GROUND_Y - PLAYER_H - 18)  # float at head height
        self.speed = speed
        self.alive = True

    def update(self):
        self.x -= self.speed
        if self.x < -20:
            self.alive = False

    def draw(self):
        if not self.alive:
            return
        bx, by = int(self.x), int(self.y)
        # pulsing white/yellow glow
        glow_r = 9 + ((pyxel.frame_count // 5) % 3)
        pyxel.circb(bx, by + 2, glow_r, 10)
        # heart shape: two circles + filled triangle
        pyxel.circ(bx - 3, by - 1, 4, 8)
        pyxel.circ(bx + 3, by - 1, 4, 8)
        pyxel.tri(bx - 6, by + 1, bx + 6, by + 1, bx, by + 8, 8)
        # bright highlight
        pyxel.pset(bx - 3, by - 3, 14)
        pyxel.pset(bx + 3, by - 3, 14)


# ---------------------------------------------------------------------------
class Background:
    """Parallax forest & mountain landscape."""

    def __init__(self):
        # Clouds [x, y, scale]
        self.clouds = [[float(random.randint(-30, WIDTH)),
                        float(random.randint(15, 65)),
                        float(random.randint(10, 22))] for _ in range(5)]
        # Far snow mountains [base_x, width, height]
        self.mtn_snow  = self._gen_mountains(8,  60, 110, 55, 90)
        # Mid dark mountains [base_x, width, height]
        self.mtn_mid   = self._gen_mountains(11, 35,  70, 38, 60)
        # Far treeline silhouette [x, tree_height]
        self.treeline  = self._gen_treeline(48, 20, 42, 4)
        # Mid pine trees [x, height]
        self.trees_mid  = self._gen_trees(16, 30, 55, 18)
        # Near pine trees [x, height]
        self.trees_near = self._gen_trees(11, 55, 88, 12)

    def _gen_mountains(self, count, min_h, max_h, min_w, max_w):
        out, x = [], 0
        for _ in range(count):
            w = random.randint(min_w, max_w)
            h = random.randint(min_h, max_h)
            out.append([float(x), float(w), float(h)])
            x += w + random.randint(-8, 20)   # slight overlap allowed
        return out

    def _gen_treeline(self, count, min_h, max_h, min_gap):
        out, x = [], -20
        for _ in range(count):
            h = random.randint(min_h, max_h)
            out.append([float(x), float(h)])
            x += random.randint(min_gap, min_gap + 10)
        return out

    def _gen_trees(self, count, min_h, max_h, gap):
        out, x = [], 0
        for _ in range(count):
            h = random.randint(min_h, max_h)
            out.append([float(x), float(h)])
            x += h // 2 + gap + random.randint(0, 24)
        return out

    def update(self, speed):
        for c in self.clouds:
            c[0] -= speed * 0.04
            if c[0] + c[2] * 3 < 0:
                c[0] = float(WIDTH + random.randint(10, 60))
                c[1] = float(random.randint(15, 65))
                c[2] = float(random.randint(10, 22))

        for m in self.mtn_snow:
            m[0] -= speed * 0.08
            if m[0] + m[1] < 0:
                m[0] = WIDTH + random.randint(0, 80)
                m[1] = float(random.randint(55, 90))
                m[2] = float(random.randint(60, 110))

        for m in self.mtn_mid:
            m[0] -= speed * 0.20
            if m[0] + m[1] < 0:
                m[0] = WIDTH + random.randint(0, 50)
                m[1] = float(random.randint(38, 60))
                m[2] = float(random.randint(35, 70))

        for t in self.treeline:
            t[0] -= speed * 0.30
            if t[0] + 30 < 0:
                t[0] = WIDTH + random.randint(0, 20)
                t[1] = float(random.randint(20, 42))

        for t in self.trees_mid:
            t[0] -= speed * 0.52
            if t[0] + 30 < 0:
                t[0] = WIDTH + random.randint(0, 30)
                t[1] = float(random.randint(30, 55))

        for t in self.trees_near:
            t[0] -= speed * 0.84
            if t[0] + 40 < 0:
                t[0] = WIDTH + random.randint(0, 20)
                t[1] = float(random.randint(55, 88))

    def draw(self):
        # --- Sky ---
        pyxel.rect(0, 0, WIDTH, HEIGHT, 12)  # solid sky blue

        # --- Sun (upper-left, warm glow) ---
        sun_x, sun_y = 44, 50
        # Outer glow ring
        for i in range(8):
            a  = i * math.pi / 4
            x1 = int(sun_x + math.cos(a) * 20)
            y1 = int(sun_y + math.sin(a) * 20)
            x2 = int(sun_x + math.cos(a) * 27)
            y2 = int(sun_y + math.sin(a) * 27)
            pyxel.line(x1, y1, x2, y2, 9)
        pyxel.circ(sun_x, sun_y, 17, COL_SUN)
        pyxel.circ(sun_x, sun_y, 12, COL_SUN)
        pyxel.circb(sun_x, sun_y, 17, 9)

        # --- Clouds ---
        for c in self.clouds:
            self._draw_cloud(int(c[0]), int(c[1]), int(c[2]))

        # --- Far snow mountains ---
        for m in self.mtn_snow:
            bx = int(m[0]); bw = int(m[1]); bh = int(m[2])
            px = bx + bw // 2
            py = GROUND_Y - bh
            # Mountain body (light grey)
            pyxel.tri(px, py, bx, GROUND_Y, bx + bw, GROUND_Y, COL_MTN_FAR)
            # Right face slightly darker for 3D depth
            pyxel.tri(px, py, px, GROUND_Y, bx + bw, GROUND_Y, COL_ROCK)
            # Snow cap (white, top ~28% of height)
            sh      = bh * 28 // 100
            hw_snow = max(2, sh * (bw // 2) // max(1, bh))
            pyxel.tri(px, py,
                      px - hw_snow, py + sh,
                      px + hw_snow, py + sh, 7)

        # --- Mid dark mountains (two-tone) ---
        for m in self.mtn_mid:
            bx = int(m[0]); bw = int(m[1]); bh = int(m[2])
            px = bx + bw // 2
            py = GROUND_Y - bh
            # Left face dark blue
            pyxel.tri(px, py, bx, GROUND_Y, px, GROUND_Y, COL_MTN_MID)
            # Right face dark grey (shaded side)
            pyxel.tri(px, py, px, GROUND_Y, bx + bw, GROUND_Y, COL_ROCK)

        # --- Far treeline silhouette (dense pine tops) ---
        tbase = GROUND_Y - 10
        for t in self.treeline:
            tx = int(t[0]); th = int(t[1])
            hw = th // 2 + 4
            pyxel.tri(tx, tbase - th, tx - hw, tbase, tx + hw, tbase, COL_TREE_SIL)

        # --- Mid pine trees ---
        for t in self.trees_mid:
            self._draw_pine(int(t[0]), GROUND_Y, int(t[1]),
                            COL_TREE_SIL, COL_TRUNK, hi=False)

        # --- Near pine trees (with sunlit highlight) ---
        for t in self.trees_near:
            self._draw_pine(int(t[0]), GROUND_Y, int(t[1]),
                            COL_TREE_SIL, COL_TRUNK, hi=True)

    def _draw_cloud(self, cx, cy, s):
        if s < 2:
            return
        # Grey underside first
        pyxel.circ(cx,         cy + s // 2,     s,       6)
        pyxel.circ(cx + s,     cy + s // 2 + 2, s - 2,   6)
        # White main body on top
        pyxel.circ(cx,         cy,              s,       COL_CLOUD)
        pyxel.circ(cx + s,     cy + 3,          s - 2,   COL_CLOUD)
        pyxel.circ(cx - s + 4, cy + 4,          max(1, s - 4), COL_CLOUD)
        pyxel.circ(cx + s//2,  cy - s // 3,     s // 2,  COL_CLOUD)

    def _draw_pine(self, cx, gnd, h, col, tc, hi=False):
        """Layered pine tree. cx=center x, gnd=ground y, h=total height."""
        th = max(5, h // 4)    # trunk height
        fh = h - th             # foliage height
        ty = gnd - h            # foliage tip y
        fb = gnd - th           # foliage base y = top of trunk

        # Trunk
        pyxel.rect(cx - 2, fb, 4, th, tc)

        # 3 tiers bottom-to-top so each upper layer overlaps the lower
        hw1 = fh * 48 // 100; t1t = ty + fh // 2;    t1b = fb
        hw2 = fh * 33 // 100; t2t = ty + fh // 4;    t2b = fb - fh // 3
        hw3 = fh * 18 // 100; t3t = ty;               t3b = fb - fh * 55 // 100

        pyxel.tri(cx, t1t, cx - hw1, t1b, cx + hw1, t1b, col)
        pyxel.tri(cx, t2t, cx - hw2, t2b, cx + hw2, t2b, col)
        pyxel.tri(cx, t3t, cx - hw3, t3b, cx + hw3, t3b, col)

        if hi:
            # Sunlit left-edge highlight on topmost tier
            hiw = max(1, fh * 9 // 100)
            mid = t3t + (t3b - t3t) // 3
            pyxel.tri(cx, t3t, cx - hiw, t3b, cx - hiw, mid, COL_TREE_HI)


# ---------------------------------------------------------------------------
class Game:
    def __init__(self):
        pyxel.init(WIDTH, HEIGHT, title=TITLE, fps=FPS)
        self._init_sounds()
        self.hi_score = 0
        self.state    = "title"
        self._init_game()
        self._start_music(1)    # title screen music
        pyxel.run(self.update, self.draw)

    def _init_sounds(self):
        # --- SFX on channel 3 ---
        # JUMP (0): single crisp blip up
        pyxel.sounds[0].set("e3a3", "s", "57", "nn", 18)
        # MEAT (1): two-tone coin ding
        pyxel.sounds[1].set("c4e4", "t", "77", "ns", 14)
        # HEART (2): warm two-note chime, clearly distinct from meat
        pyxel.sounds[2].set("g3g4", "t", "77", "ns", 10)
        # HIT (3): vocal "AU" cry — descending pulse, sharp then fading
        pyxel.sounds[3].set("g4d4a3", "ppp", "764", "fnn", 8)
        # LEVEL-UP (4): three quick ascending tones
        pyxel.sounds[4].set("c4e4g4", "s", "677", "nnn", 10)

        # --- Music (Mega Man energy + caveman flavour) ---
        # In-game lead (ch0): A-minor pentatonic driving riff — 16 notes
        pyxel.sounds[6].set(
            "a3c4e4a4g4e4c4a3a3c4e4a4b3g3e3r",
            "tttttttttttttttt",
            "7777767676767650",
            "nnnnnnnnnnnnnnnn", 14)
        # In-game bass (ch1): thumping root-fifth — 16 notes
        pyxel.sounds[7].set(
            "a1ra1re2ra1ra1rg1ra1ra1r",
            "pppppppppppppppp",
            "7070707070707070",
            "nnnnnnnnnnnnnnnn", 14)
        # In-game rhythm (ch2): tribal kick+snare — 16 notes
        pyxel.sounds[8].set(
            "c0rc0rc0rc0rc0rc0rc0rc0r",
            "nnnnnnnnnnnnnnnn",
            "7050705070507050",
            "ffffffffffffffff", 14)
        # Title melody (ch0): heroic G-major fanfare — 16 notes
        pyxel.sounds[9].set(
            "g3b3d4g4rg4a4b4rg4e4c4d4rb3g3",
            "tttttttttttttttt",
            "6777677756776770",
            "nnnnnnnnnnnnnnnn", 12)
        # Title bass (ch1): steady pulse, root notes — 16 notes
        pyxel.sounds[10].set(
            "g1rg1rg1rg1rd2rd2rd2rd2r",
            "pppppppppppppppp",
            "6000600060006000",
            "nnnnnnnnnnnnnnnn", 12)
        # Game-over (ch0): sad descending — 10 notes, plays once
        pyxel.sounds[11].set(
            "a3g3e3c3ra2g2e2c2r",
            "ssssssssss",
            "7665543321",
            "ffffffffff", 12)

        # --- Music track assignments ---
        pyxel.musics[0].set([6], [7], [8], [])   # in-game
        pyxel.musics[1].set([9], [10], [], [])    # title
        pyxel.musics[2].set([11], [], [], [])     # game over

    # -----------------------------------------------------------------------
    def _play(self, snd):
        """Play a one-shot SFX on channel 3 (never conflicts with music)."""
        pyxel.play(3, snd)

    def _start_music(self, music_id, loop=True):
        """Switch to a music track."""
        pyxel.playm(music_id, loop=loop)

    def _init_game(self):
        self.player          = Player()
        self.obstacles       = []
        self.meats           = []
        self.bg              = Background()
        self.scroll_speed    = SCROLL_SPEED_INIT
        self.score           = 0.0
        self.meat_collected  = 0
        self.frame           = 0
        self.spawn_timer     = 30
        self.meat_timer      = 45
        self.level           = 1
        self.level_up_timer  = 0   # frames to show level-up banner
        self.health          = 3   # 3 hits before death
        self.invincible      = 0   # invincibility frames after a hit
        self.heart_timer     = 180  # frames until next heart can spawn
        self.hearts          = []
        self.heal_timer      = 0   # frames to show +heart banner
        self.enemy           = None  # rival caveman, one at a time
        self.enemy_spawned_for_level = 0  # track which level-up triggered spawn

    # -----------------------------------------------------------------------
    def update(self):
        if   self.state == "title":   self._update_title()
        elif self.state == "play":    self._update_play()
        elif self.state == "leaving": self._update_leaving()
        elif self.state == "dead":    self._update_dead()
        elif self.state == "ending":  self._update_ending()

    def _update_title(self):
        if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_RETURN):
            self.state = "play"
            self._init_game()
            self._start_music(0)   # in-game music

    def _update_play(self):
        self.frame       += 1
        level_cap         = LEVEL_SPEED_MAX[self.level]
        self.scroll_speed = min(level_cap,
                                SCROLL_SPEED_INIT + self.frame * SPEED_INCREMENT)
        self.score       += self.scroll_speed * 0.05

        # --- level progression ---
        if self.level_up_timer > 0:
            self.level_up_timer -= 1
        next_lvl = self.level + 1
        if next_lvl in LEVEL_CONFIG:
            threshold = LEVEL_CONFIG[next_lvl][0]
            if int(self.score) >= threshold:
                self.level          = next_lvl
                self.level_up_timer = 120
                self._play(4)  # level-up fanfare
                # spawn rival enemy on level-up (once per level)
                if self.enemy_spawned_for_level < self.level:
                    self.enemy_spawned_for_level = self.level
                    self.enemy = Enemy(self.player.lane, self.scroll_speed)

        self._handle_input()

        self.player.update()
        self.bg.update(self.scroll_speed)

        # --- obstacle spawn ---
        self.spawn_timer -= 1
        if self.spawn_timer <= 0:
            kinds_pool = LEVEL_CONFIG[self.level][1]
            kind = random.choice(kinds_pool)
            self.obstacles.append(Obstacle(self.scroll_speed, kind))
            # doubles only in level 5, with a wide gap so they're readable
            if self.level == 5 and random.random() < 0.22:
                kind2  = random.choice(kinds_pool)
                gap    = random.randint(70, 110)
                double = Obstacle(self.scroll_speed, kind2)
                double.x += gap
                self.obstacles.append(double)
            # generous interval: shrinks only at high speed / high level
            base_interval    = max(65, int(130 - self.scroll_speed * 6))
            self.spawn_timer = random.randint(base_interval, base_interval + 80)
            self.meat_timer  = max(self.meat_timer, 40)

        # --- meat spawn (on its own cadence, never right after an obstacle) ---
        self.meat_timer -= 1
        if self.meat_timer <= 0:
            if random.random() < 0.70:
                ml = random.randint(0, LANE_COUNT - 1)
                self.meats.append(Meat(ml, self.scroll_speed))
            self.meat_timer = random.randint(30, 55)

        for obs in self.obstacles:
            obs.speed = self.scroll_speed
            obs.update()
        self.obstacles = [o for o in self.obstacles if o.alive]

        for m in self.meats:
            m.speed = self.scroll_speed
            m.update()
        self.meats = [m for m in self.meats if m.alive]

        # --- Log platform: catch descending player on top; fall off when it passes ---
        if self.player.is_jumping and self.player.vy > 0:
            for obs in self.obstacles:
                if obs.kind == "log":
                    ox, oy, ow, oh = obs.rect
                    if abs(int(self.player.x) - int(obs.x)) < (ow + PLAYER_W) // 2:
                        player_bottom = int(self.player.y) + PLAYER_H
                        if oy <= player_bottom <= oy + 8:
                            self.player.y      = float(oy - PLAYER_H)
                            self.player.vy     = 0.0
                            self.player.is_jumping = False
                            break

        # Fall off the log when it scrolls out from under the player
        if not self.player.is_jumping and self.player.y < float(GROUND_Y - PLAYER_H):
            on_a_log = any(
                obs.kind == "log"
                and abs(int(self.player.x) - int(obs.x)) < (obs.rect[2] + PLAYER_W) // 2
                for obs in self.obstacles
            )
            if not on_a_log:
                self.player.is_jumping = True
                self.player.vy         = 0.0

        px, py, pw, ph = self.player.rect

        # invincibility countdown
        if self.invincible > 0:
            self.invincible -= 1

        for obs in self.obstacles:
            ox, oy, ow, oh = obs.rect
            if self._overlap(px, py, pw, ph, ox, oy, ow, oh):
                # Log: player can land on top — only hurt if hitting the side
                if obs.kind == "log":
                    player_bottom = py + ph
                    log_top       = oy
                    # if the player's feet are at or above log top, it's a
                    # safe landing — skip damage entirely
                    if player_bottom <= log_top + 4:
                        continue
                if self.invincible == 0:
                    self.health     -= 1
                    self.invincible  = 90   # ~1.5 s grace period
                    self._play(3)  # hit thud
                    if self.health <= 0:
                        self.player.alive = False

        for meat in self.meats:
            if meat.alive:
                mx, my = int(meat.x), int(meat.y)
                if (abs(px + pw//2 - mx) < pw//2 + 8 and
                        abs(py + ph//2 - my) < ph//2 + 8):
                    meat.alive           = False
                    self.meat_collected += 1
                    self.score          += meat.pts
                    self._play(1)  # meat chime

        # --- enemy update & collision ---
        if self.enemy and self.enemy.alive:
            self.enemy.update(self.scroll_speed, self.player.lane)
            if not self.enemy.fleeing and self.invincible == 0:
                ex, ey, ew, eh = self.enemy.rect
                if self._overlap(px, py, pw, ph, ex, ey, ew, eh):
                    # steal meat (min 0) and a heart
                    stolen              = min(self.meat_collected, 3)
                    self.meat_collected = max(0, self.meat_collected - stolen)
                    self.health         = max(0, self.health - 1)
                    self.invincible     = 90
                    self.enemy.fleeing  = True
                    self._play(3)  # hit thud
                    if self.health <= 0:
                        self.player.alive = False
        elif self.enemy and not self.enemy.alive:
            self.enemy = None

        # --- heart pickups ---
        self.heart_timer -= 1
        if self.heart_timer <= 0:
            # only spawn when health < 3, low probability
            if self.health < 3 and random.random() < 0.55:
                lane = random.randint(0, LANE_COUNT - 1)
                self.hearts.append(HeartItem(lane, self.scroll_speed))
            self.heart_timer = random.randint(200, 350)

        for h in self.hearts:
            h.speed = self.scroll_speed
            h.update()
        self.hearts = [h for h in self.hearts if h.alive]

        for h in self.hearts:
            if h.alive:
                hx, hy = int(h.x), int(h.y)
                if (abs(px + pw//2 - hx) < pw//2 + 10 and
                        abs(py + ph//2 - hy) < ph//2 + 10):
                    h.alive          = False
                    self.health      = min(3, self.health + 1)
                    self.heal_timer  = 90
                    self._play(2)  # heart chime

        if not self.player.alive:
            self.hi_score = max(self.hi_score, int(self.score))
            self._start_music(2, loop=False)   # game-over music
            self.state    = "dead"

        # --- Win condition ---
        if int(self.score) >= WIN_SCORE and self.state == "play":
            self.hi_score    = max(self.hi_score, int(self.score))
            self.state       = "leaving"
            self.leaving_frame = 0
            self.obstacles   = []   # clear obstacles for clean exit run
            self.meats       = []
            self.hearts      = []
            self.enemy       = None
            self._start_music(1)   # warm music starts during exit run

    def _handle_input(self):
        """Read keyboard and update player state. Override in subclasses for bot/replay."""
        import pyxel as _px
        if _px.btnp(_px.KEY_LEFT)  or _px.btnp(_px.KEY_A):   self.player.move_left()
        if _px.btnp(_px.KEY_RIGHT) or _px.btnp(_px.KEY_D):   self.player.move_right()
        if (_px.btnp(_px.KEY_UP)   or _px.btnp(_px.KEY_W)
                or _px.btnp(_px.KEY_SPACE)):                  self.player.jump(); self._play(0)
        if _px.btn(_px.KEY_DOWN)   or _px.btn(_px.KEY_S):    self.player.duck()
        else:                                                  self.player.stop_duck()

    def _update_dead(self):  # unchanged
        if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_RETURN):
            self.state = "play"
            self._init_game()
            self._start_music(0)
        if pyxel.btnp(pyxel.KEY_ESCAPE):
            self.state = "title"
            self._init_game()
            self._start_music(1)

    def _update_leaving(self):
        self.leaving_frame += 1
        lf = self.leaving_frame
        # Speed the forest scroll up
        self.scroll_speed = min(SCROLL_SPEED_MAX, self.scroll_speed + 0.08)
        self.bg.update(self.scroll_speed)
        # Player runs toward right edge
        self.player.update()
        # After ~120 frames (2 s) switch to ending
        if lf >= 120:
            self.state        = "ending"
            self.ending_frame = 0
            self.stars = [(random.randint(10, WIDTH - 10),
                           random.randint(10, 90)) for _ in range(28)]

    def _update_ending(self):
        self.ending_frame += 1
        # After full animation, allow restart
        if self.ending_frame > 480:
            if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_RETURN):
                self.state = "title"
                self._init_game()
                self._start_music(1)

    @staticmethod
    def _overlap(ax, ay, aw, ah, bx, by, bw, bh, m=2):
        return (ax + m < bx + bw - m and ax + aw - m > bx + m and
                ay + m < by + bh - m and ay + ah - m > by + m)

    # -----------------------------------------------------------------------
    def draw(self):
        pyxel.cls(COL_SKY)
        if   self.state == "title":    self._draw_title()
        elif self.state == "play":     self._draw_game()
        elif self.state == "leaving":  self._draw_leaving()
        elif self.state == "ending":   self._draw_ending()
        elif self.state == "dead":
            self._draw_game()
            self._draw_dead_overlay()

    def _draw_game(self):
        self.bg.draw()
        self._draw_ground()
        for m in self.meats:
            m.draw()
        for h in self.hearts:
            h.draw()
        for obs in self.obstacles:
            obs.draw()
        if self.enemy:
            self.enemy.draw()
        # blink player while invincible (hide every other 4-frame window)
        if self.invincible == 0 or (self.invincible // 4) % 2 == 0:
            self.player.draw()
        self._draw_hud()
        if self.level_up_timer > 0:
            self._draw_level_banner()
        if self.heal_timer > 0:
            self._draw_heal_banner()

    # -------------------------------------------------------------------
    def _draw_leaving(self):
        """Wilma runs out of the forest: trees thin, light brightens, she exits right."""
        lf = self.leaving_frame

        # Background — forest fades to bright sky as we leave the woods
        self.bg.draw()
        self._draw_ground()

        # Player runs toward the right edge: accelerate x across the screen
        # For the first 60 frames she's on-screen; then she moves off to the right
        run_x = int(self.player.x) + max(0, lf - 30) * 2
        if run_x < WIDTH + 20:
            self.player.draw()
            # override player x so the walk animation looks right
            self.player.x = float(min(run_x, WIDTH + 20))

        # Victory text
        if lf < 90:
            pyxel.text(WIDTH // 2 - 38, 20, "YOU MADE IT HOME!", 10)


    def _draw_ending(self):
        ef = self.ending_frame

        # ── Sky ────────────────────────────────────────────────────────
        if ef < 120:   sky_col = COL_SKY
        elif ef < 200: sky_col = 9       # orange dusk
        else:          sky_col = 1       # dark night
        pyxel.cls(sky_col)

        # Stars (appear one by one after frame 200)
        stars_visible = max(0, (ef - 200) // 10)
        for i, (sx, sy) in enumerate(self.stars[:stars_visible]):
            pyxel.pset(sx, sy, 10)
            if (ef // 15 + i) % 3 == 0:
                pyxel.pset(sx + 1, sy, 7)

        # ── Ground + cave on RIGHT ──────────────────────────────────────
        pyxel.rect(0,   160, WIDTH, HEIGHT - 160, COL_GROUND)
        pyxel.rect(0,   155, WIDTH, 7, 3)           # grass strip
        # ── Tent on right side ──────────────────────────────────────────
        tx, ty, tb = 222, 100, 160   # peak x/y, base y
        hw = 34                       # half-width at base
        # Left face (brown, sunlit)
        pyxel.tri(tx, ty, tx - hw, tb, tx, tb, COL_TRUNK)
        # Right face (dark, shaded)
        pyxel.tri(tx, ty, tx, tb, tx + hw, tb, 2)
        # Decorative horizontal bands
        pyxel.line(tx - 14, ty + 24, tx + 14, ty + 24, 9)
        pyxel.line(tx - 20, ty + 38, tx + 20, ty + 38, 9)
        # Door opening — dark triangle at base center
        pyxel.tri(tx, ty + 46, tx - 11, tb, tx + 11, tb, 0)
        # Outline edges
        pyxel.line(tx, ty, tx - hw, tb, 2)
        pyxel.line(tx, ty, tx + hw, tb, 2)
        # Small flag/smoke hole at peak
        pyxel.line(tx, ty, tx, ty - 7, 7)
        pyxel.pset(tx + 1, ty - 7, 8)

        fx = 120   # fire x — between family members
        fy = 158   # fire base y

        # ── Characters ─────────────────────────────────────────────────
        if ef < 210:
            # Phase 1 (0-90): woman walks from LEFT edge to settled x=75
            woman_x = max(75, -20 + ef * 2) if ef < 90 else 75
            self._ending_draw_woman(woman_x, 137)

            # Phase 2 (90+): husband + kids emerge from cave leftward
            if ef >= 90:
                emerge = min(1.0, (ef - 90) / 45)
                hub_x  = int(215 - 60 * emerge)     # 215 → 155
                self._ending_draw_husband(hub_x, 137)
            if ef >= 110:
                emerge2 = min(1.0, (ef - 110) / 35)
                k1_x    = int(210 - 115 * emerge2)  # 210 → 95
                self._ending_draw_kid(k1_x, 148)
            if ef >= 128:
                emerge3 = min(1.0, (ef - 128) / 35)
                k2_x    = int(218 - 78 * emerge3)   # 218 → 140
                self._ending_draw_kid(k2_x, 148)
        else:
            # Phase 4 (210+): family bounces around the campfire
            bt     = ef - 210
            h_off  = int(abs(math.sin(bt * 0.18)) * 9)
            k1_off = int(abs(math.sin(bt * 0.25 + 1.0)) * 11)
            k2_off = int(abs(math.sin(bt * 0.22 + 2.0)) * 11)
            self._ending_draw_woman(75, 137)
            self._ending_draw_kid(95, 148 - k1_off)   # left of fire
            self._ending_draw_husband(155, 137 - h_off)
            self._ending_draw_kid(140, 148 - k2_off)  # right of fire

        # ── Campfire (drawn after characters so it's always on top) ─────
        # logs
        pyxel.rect(fx - 10, fy,      20, 3, COL_TRUNK)
        pyxel.rect(fx -  8, fy -  1, 16, 2, 2)
        # flame flicker
        fire_col = 9 if (ef // 5) % 2 == 0 else 8
        pyxel.tri(fx, fy - 12, fx - 6, fy, fx + 6, fy, fire_col)
        pyxel.tri(fx, fy - 8,  fx - 3, fy, fx + 3, fy, 10)
        pyxel.pset(fx, fy - 10, 7)

        # Spit stick above flames
        sy = fy - 14   # spit height
        pyxel.line(fx - 14, sy, fx + 14, sy, COL_TRUNK)  # horizontal spit
        pyxel.line(fx - 14, sy, fx - 16, fy - 2, 2)       # left fork support
        pyxel.line(fx + 14, sy, fx + 16, fy - 2, 2)       # right fork support
        # Three drumstick blobs on the spit
        for mdx, mcol in [(-8, 8), (0, 9), (8, 8)]:
            mx = fx + mdx
            pyxel.circ(mx, sy, 4, COL_TRUNK)      # charred outer crust
            pyxel.circ(mx, sy, 3, mcol)            # roasted meat colour
            pyxel.pset(mx - 1, sy - 2, 10)        # hot highlight
            pyxel.line(mx, sy, mx, sy + 5, COL_BONE)  # bone handle down

        # ── Text ───────────────────────────────────────────────────────
        if ef < 60:
            pyxel.text(WIDTH//2 - 42, 28, "YOU MADE IT HOME!", 10)
        elif 90 <= ef < 150:
            pyxel.text(WIDTH//2 - 38, 28, "Your family missed you!", 14)
        elif 150 <= ef < 270:
            pyxel.text(WIDTH//2 - 34, 28, "Meat on the fire!", 9)
        elif ef >= 300:
            col = 10 if min(1.0, (ef - 300) / 60) > 0.5 else 9
            pyxel.text(WIDTH//2 - 16, 24, "* THE END *", col)
            pyxel.text(WIDTH//2 - 30, 34, f"SCORE  {self.hi_score}", 7)
        if ef > 480 and (ef // 20) % 2 == 0:
            pyxel.text(WIDTH//2 - 44, HEIGHT - 18, "SPACE to play again", 13)

    # ── Ending character helpers ────────────────────────────────────────
    def _ending_draw_woman(self, cx, by):
        # hair — wide, brown
        pyxel.rect(cx - 5, by,     11, 4, 4)
        pyxel.rect(cx - 7, by + 2, 15, 3, 4)
        for dy in range(4, 10):
            pyxel.pset(cx - 5, by + dy, 4)
            pyxel.pset(cx + 4, by + dy, 4)
        # face — peach on white-ish outline
        pyxel.rect(cx - 4, by + 3,  9, 7, 15)
        pyxel.pset(cx - 1, by + 5,  0)
        pyxel.pset(cx + 2, by + 5,  0)
        pyxel.pset(cx - 2, by + 7, 14)
        pyxel.pset(cx + 2, by + 7, 14)
        pyxel.pset(cx,     by + 8, 14)
        # bare torso — bright skin against dark shadow
        pyxel.rect(cx - 3, by + 10, 8, 4, 15)
        # skirt — brown with orange spots
        pyxel.rect(cx - 5, by + 13, 10, 9, 4)
        pyxel.rect(cx - 4, by + 13,  8, 2, 2)  # waistband
        pyxel.pset(cx - 2, by + 15, 9)
        pyxel.pset(cx + 1, by + 16, 9)
        pyxel.pset(cx + 2, by + 19, 9)
        # legs
        pyxel.rect(cx - 3, by + 20, 3, 2, 15)
        pyxel.rect(cx + 1, by + 20, 3, 2, 15)

    def _ending_draw_husband(self, cx, by):
        # dark shaggy hair
        pyxel.rect(cx - 5, by,      12, 5, 2)
        pyxel.rect(cx - 7, by + 3,  15, 4, 2)
        for dy in range(5, 14):
            pyxel.pset(cx - 7, by + dy, 2)
            pyxel.pset(cx + 7, by + dy, 2)
        # face — wider, rugged
        pyxel.rect(cx - 4, by + 4,  10, 8, 15)
        pyxel.rect(cx - 4, by + 5,  10, 2, 5)  # heavy brow
        pyxel.pset(cx - 2, by + 7,  0)
        pyxel.pset(cx + 3, by + 7,  0)
        pyxel.pset(cx,     by + 10, 2)
        # beard
        pyxel.rect(cx - 3, by + 11,  8, 2, 5)
        # body — light grey (6) so it pops against brown ground
        pyxel.rect(cx - 5, by + 13, 12, 10, 6)
        pyxel.rect(cx - 5, by + 13, 12,  2, 5)  # collar/shadow
        # arms
        pyxel.rect(cx - 8, by + 13,  4, 7, 15)  # skin-coloured arms
        pyxel.rect(cx + 6, by + 13,  4, 7, 15)
        # legs — dark
        pyxel.rect(cx - 4, by + 23,  4, 5, 5)
        pyxel.rect(cx + 1, by + 23,  4, 5, 5)

    def _ending_draw_kid(self, cx, by):
        # hair
        pyxel.rect(cx - 3, by,      7, 3, 4)
        pyxel.pset(cx - 4, by + 1,  4)
        # face — bright skin
        pyxel.rect(cx - 3, by + 2,  7, 6, 15)
        pyxel.pset(cx - 1, by + 4,  0)
        pyxel.pset(cx + 2, by + 4,  0)
        pyxel.pset(cx - 1, by + 7, 14)
        pyxel.pset(cx,     by + 7, 14)
        pyxel.pset(cx + 1, by + 7, 14)
        # body — orange top for visibility
        pyxel.rect(cx - 3, by + 7,  7, 6, 9)
        pyxel.rect(cx - 3, by + 7,  7, 2, 8)  # collar stripe
        # legs
        pyxel.rect(cx - 2, by + 13, 2, 3, 15)
        pyxel.rect(cx + 1, by + 13, 2, 3, 15)

    # -------------------------------------------------------------------
    def _draw_title(self):
        self.bg.draw()
        self._draw_ground()
        dummy   = Player()
        dummy.x = LANE_XS[1]
        dummy.y = GROUND_Y - PLAYER_H
        dummy.draw()

        bx, by, bw, bh = 40, 44, 176, 120
        pyxel.rect(bx, by, bw, bh, 0)
        pyxel.rectb(bx, by, bw, bh, COL_ACCENT)
        pyxel.rectb(bx + 2, by + 2, bw - 4, bh - 4, COL_ACCENT)

        pyxel.text(bx + 30, by + 10, TITLE, COL_ACCENT)
        pyxel.text(bx + 38, by + 22, "stone age endless run",   COL_ROCK)

        pyxel.text(bx + 10, by + 38, "LEFT / RIGHT  change lane", COL_SCORE)
        pyxel.text(bx + 10, by + 50, "UP / SPACE    jump",        COL_SCORE)
        pyxel.text(bx + 10, by + 62, "DOWN          duck",        COL_SCORE)
        pyxel.text(bx + 10, by + 76, "Rocks, sticks, bushes,",    COL_BOULDER)
        pyxel.text(bx + 10, by + 85, "logs & trees block the way!",COL_BOULDER)
        pyxel.text(bx + 10, by + 98, "Collect steaks for points!", COL_MEAT_RAW)

        if (pyxel.frame_count // 20) % 2 == 0:
            pyxel.text(bx + 34, by + 112, "PRESS SPACE TO START", COL_ACCENT)
        if self.hi_score > 0:
            pyxel.text(4, 4, f"BEST {self.hi_score:05}", COL_ROCK)

    def _draw_dead_overlay(self):
        bx, by, bw, bh = 64, 92, 128, 68
        pyxel.rect(bx, by, bw, bh, 0)
        pyxel.rectb(bx, by, bw, bh, 8)
        pyxel.text(bx + 36, by +  8, "GAME OVER",                   8)
        pyxel.text(bx +  8, by + 22, f"SCORE  {int(self.score):05}", COL_SCORE)
        pyxel.text(bx +  8, by + 34, f"MEAT   {self.meat_collected:03}  LVL {self.level}", COL_MEAT_RAW)
        pyxel.text(bx +  8, by + 46, f"BEST   {self.hi_score:05}",   COL_ACCENT)
        if (pyxel.frame_count // 20) % 2 == 0:
            pyxel.text(bx + 4, by + 60, "SPACE retry  ESC menu", 13)

    def _draw_heal_banner(self):
        self.heal_timer -= 1
        if self.heal_timer < 30 and (self.heal_timer // 5) % 2 == 0:
            return
        bx, by = WIDTH // 2 - 36, 22
        pyxel.rect(bx, by, 72, 14, 0)
        pyxel.rectb(bx, by, 72, 14, 8)
        pyxel.text(bx + 8, by + 4, "+ HEART  restored!", 8)

    def _draw_level_banner(self):
        alpha = self.level_up_timer  # 120 -> 0
        # fade: show for first 90 frames, blink last 30
        if alpha < 30 and (alpha // 6) % 2 == 0:
            return
        bx, by, bw, bh = 72, 22, 112, 26
        pyxel.rect(bx, by, bw, bh, 0)
        pyxel.rectb(bx, by, bw, bh, COL_ACCENT)
        lname = ["STONE AGE", "HUNTER", "TRIBE HERO", "LEGEND", "GRONK GOD"][self.level - 1]
        pyxel.text(bx + 8,  by + 5,  f"LEVEL {self.level}  -  {lname}", COL_ACCENT)
        obs_hint = {
            1: "Rocks!",
            2: "Rocks + Sticks!",
            3: "+ Bushes!",
            4: "+ Logs  (duck)!",
            5: "+ Trees  max!",
        }
        pyxel.text(bx + 18, by + 16, obs_hint.get(self.level, ""), COL_SCORE)

    def _draw_ground(self):
        # Earth body
        pyxel.rect(0, GROUND_Y + 2, WIDTH, HEIGHT - GROUND_Y - 2, COL_GROUND)
        # Dark dirt band
        pyxel.rect(0, GROUND_Y,     WIDTH, 4,                      COL_GROUND_DARK)
        # Grass strip on top edge
        pyxel.rect(0, GROUND_Y - 4, WIDTH, 5,                      3)   # dark green
        pyxel.line(0, GROUND_Y - 4, WIDTH, GROUND_Y - 4,           COL_GROUND_LINE)
        # Scrolling grass tufts
        offset = (self.frame * int(self.scroll_speed + 1)) % 22
        for i in range(-1, WIDTH // 22 + 2):
            tx = i * 22 - offset
            pyxel.line(tx,     GROUND_Y - 4, tx - 2, GROUND_Y - 9,  COL_GROUND_LINE)
            pyxel.line(tx,     GROUND_Y - 4, tx + 2, GROUND_Y - 9,  COL_GROUND_LINE)
            pyxel.line(tx + 9, GROUND_Y - 4, tx + 7, GROUND_Y - 8,  3)
            pyxel.line(tx + 9, GROUND_Y - 4, tx + 11, GROUND_Y - 8, 3)

    def _draw_hud(self):
        # Score + level
        pyxel.rect(0, 0, 110, 11, COL_HUD_BG)
        pyxel.text(2, 2, f"SCORE {int(self.score):05}  L{self.level}", COL_SCORE)

        # Hearts — flash when invincible
        show_hearts = (self.invincible == 0) or ((self.invincible // 6) % 2 == 0)
        if show_hearts:
            for i in range(3):
                hx = 2 + i * 12
                hy = 14
                col = 8 if i < self.health else COL_BOULDER_D
                # simple heart: two circles + triangle
                pyxel.circ(hx + 2, hy + 1, 2, col)
                pyxel.circ(hx + 6, hy + 1, 2, col)
                pyxel.tri(hx, hy + 2, hx + 8, hy + 2, hx + 4, hy + 7, col)

        pyxel.rect(WIDTH - 56, 0, 56, 11, COL_HUD_BG)
        # mini steak icon
        pyxel.rect(WIDTH - 53, 3, 7, 5, COL_MEAT_RAW)
        pyxel.rect(WIDTH - 53, 3, 7, 2, COL_MEAT_PINK)
        pyxel.text(WIDTH - 44, 2, f"x{self.meat_collected:03}", COL_MEAT_RAW)

        bar_w = int((self.scroll_speed - SCROLL_SPEED_INIT) /
                    (SCROLL_SPEED_MAX - SCROLL_SPEED_INIT) * 54)
        pyxel.rect(WIDTH//2 - 29, 1, 58, 9, COL_HUD_BG)
        pyxel.rect(WIDTH//2 - 27, 3, max(0, bar_w), 5, COL_ACCENT)
        pyxel.rectb(WIDTH//2 - 29, 1, 58, 9, COL_GROUND_DARK)
        pyxel.text(WIDTH//2 - 23, 3, "SPEED", COL_GROUND_DARK)


# ---------------------------------------------------------------------------
if __name__ == "__main__":
    Game()