from graphics import Canvas
import random
import time

# ======================================================
# MADE BY   : YESSENIA CHOQUEHUANCA MASIAS - COUNTRY: PERÚ
# PROJECT   : Code Catcher - Catch the Codes
# UNIVERSITY: STANFORD - Peru
# COLORS    : Peru Gold/Red
# ======================================================

CANVAS_WIDTH  = 600
CANVAS_HEIGHT = 400

BALL_SIZE     = 25
GRAVITY       = 0.9
JUMP_STRENGTH = -22
BOUNCE_FACTOR = 0.6
MIN_BOUNCE_VY = 3.0

CODE_W        = 110
CODE_H        = 26
CODE_COUNT    = 4
CODES_TO_WIN  = 10

COLOR_STANFORD_RED = "#800b0bff"
COLOR_PERU_RED     = "#BF0000"
COLOR_GOLD         = "#c49620ff"
COLOR_BRIGHT_GOLD  = "#938224ff"
COLOR_DARK_NAVY    = "#1a1a2e"
COLOR_WHITE        = "WHITE"
COLOR_GRAY         = "#AAAAAA"
COLOR_BLACK        = "black"

CODE_SNIPPETS = [
    "move()",
    "pick_beeper()",
    "turn_left()",
    "turn_right()",
    "put_beeper()",
]


class KarelBall:
    """
    Pelota que se convierte en Karel parte a parte.
    Nivel 0      → pelota blanca
    Nivel 1-2    → cuerpo
    Nivel 3-4    → piernas
    Nivel 5-6    → brazos
    Nivel 7-8    → cabeza/pantalla
    Nivel 9-10   → Karel completo con antena
    """

    def __init__(self, canvas, x, y):
        self.canvas = canvas
        self.x  = x
        self.y  = y
        self.vx = 0
        self.vy = 0
        self.is_grounded = False
        self.on_platform = None
        self.level       = 0      # how many codes has he caught

        # IDs of each drawn part
        self.parts = {}
        self._draw_level()

    # --------------------------------------------------
    # DRAWN
    # --------------------------------------------------
    def _clear(self):
        for pid in self.parts.values():
            self.canvas.delete(pid)
        self.parts = {}

    def _draw_level(self):
        self._clear()
        x, y = self.x, self.y   # top-left corner of the sprite (25x25)

        # --- LEVEL 0: single ball ---
        self.parts['body_oval'] = self.canvas.create_oval(
            x, y, x + 25, y + 25,
            COLOR_WHITE, COLOR_STANFORD_RED
        )

        if self.level >= 2:
            # Gray square (replaces the oval with a rectangle on top)
            self.parts['body'] = self.canvas.create_rectangle(
                x + 4, y + 8, x + 21, y + 22,
                COLOR_GRAY, COLOR_BLACK
            )

        if self.level >= 4:
            # Left leg
            self.parts['leg_l'] = self.canvas.create_rectangle(
                x + 5, y + 22, x + 10, y + 30,
                COLOR_GRAY, COLOR_BLACK
            )
            # Right leg
            self.parts['leg_r'] = self.canvas.create_rectangle(
                x + 15, y + 22, x + 20, y + 30,
                COLOR_GRAY, COLOR_BLACK
            )
            # Left foot
            self.parts['foot_l'] = self.canvas.create_rectangle(
                x + 3, y + 28, x + 11, y + 32,
                COLOR_BLACK, COLOR_BLACK
            )
            # Ceiling height
            self.parts['foot_r'] = self.canvas.create_rectangle(
                x + 14, y + 28, x + 22, y + 32,
                COLOR_BLACK, COLOR_BLACK
            )

        if self.level >= 6:
            # Left arm
            self.parts['arm_l'] = self.canvas.create_rectangle(
                x - 5, y + 10, x + 4, y + 15,
                COLOR_GRAY, COLOR_BLACK
            )
            # Right-hand man
            self.parts['arm_r'] = self.canvas.create_rectangle(
                x + 21, y + 10, x + 30, y + 15,
                COLOR_GRAY, COLOR_BLACK
            )
            # Left hand
            self.parts['hand_l'] = self.canvas.create_oval(
                x - 8, y + 8, x - 2, y + 17,
                COLOR_BLACK, COLOR_BLACK
            )
            # Right hand
            self.parts['hand_r'] = self.canvas.create_oval(
                x + 27, y + 8, x + 33, y + 17,
                COLOR_BLACK, COLOR_BLACK
            )

        if self.level >= 8:
            # Head
            self.parts['head'] = self.canvas.create_rectangle(
                x + 5, y - 10, x + 20, y + 8,
                COLOR_GRAY, COLOR_BLACK
            )
            # Screen (eye)
            self.parts['screen'] = self.canvas.create_rectangle(
                x + 8, y - 8, x + 17, y + 2,
                COLOR_DARK_NAVY, COLOR_BLACK
            )
            # Screen resolution
            self.parts['pixel'] = self.canvas.create_rectangle(
                x + 10, y - 5, x + 15, y - 1,
                COLOR_BRIGHT_GOLD, COLOR_BRIGHT_GOLD
            )

        if self.level >= 10:
            # AntenNa
            self.parts['antenna'] = self.canvas.create_rectangle(
                x + 11, y - 18, x + 14, y - 10,
                COLOR_BLACK, COLOR_BLACK
            )
            # Antenna tip
            self.parts['antenna_tip'] = self.canvas.create_oval(
                x + 9, y - 22, x + 16, y - 16,
                COLOR_BRIGHT_GOLD, COLOR_BLACK
            )

    def _move_parts(self, dx, dy):
        for pid in self.parts.values():
            self.canvas.move(pid, dx, dy)

    # --------------------------------------------------
    # DEVELOPMENT
    # --------------------------------------------------
    def evolve(self, codes_caught):
        self.level = codes_caught
        self._draw_level()

    # --------------------------------------------------
    # PHYSICS
    # --------------------------------------------------
    def jump(self):
        if self.is_grounded:
            self.vy = JUMP_STRENGTH
            self.is_grounded = False
            self.on_platform = None

    def update_physics(self, platforms):
        if self.is_grounded and self.on_platform is not None:
            dx = self.on_platform.drift
            self.x += dx
            plat_top = CANVAS_HEIGHT - self.on_platform.height
            self.y   = plat_top - BALL_SIZE
            self._move_parts(dx, 0)

        self.vy += GRAVITY
        old_x, old_y = self.x, self.y
        self.y  += self.vy
        self.x  += self.vx

        self.is_grounded = False
        self.on_platform = None

        ball_bottom   = self.y + BALL_SIZE
        ball_center_x = self.x + BALL_SIZE / 2

        for platform in platforms:
            plat_top = CANVAS_HEIGHT - platform.height
            if (self.vy >= 0 and plat_top - 8 <= ball_bottom <= plat_top + self.vy + 2):
                if platform.x <= ball_center_x <= platform.x + platform.width:
                    self.y = plat_top - BALL_SIZE
                    if abs(self.vy) > MIN_BOUNCE_VY:
                        self.vy = -self.vy * BOUNCE_FACTOR
                    else:
                        self.vy = 0
                        self.is_grounded = True
                        self.on_platform = platform
                    break

        if self.x < 0:
            self.x  = 0
            self.vx = abs(self.vx) * BOUNCE_FACTOR
        elif self.x + BALL_SIZE > CANVAS_WIDTH:
            self.x  = CANVAS_WIDTH - BALL_SIZE
            self.vx = -abs(self.vx) * BOUNCE_FACTOR

        if self.y < 0:
            self.y  = 0
            self.vy = abs(self.vy) * BOUNCE_FACTOR

        if self.y > CANVAS_HEIGHT:
            p        = platforms[0]
            self.x   = p.x + p.width / 2 - BALL_SIZE / 2
            self.y   = CANVAS_HEIGHT - p.height - BALL_SIZE
            self.vy  = 0
            self.vx  = 0
            self.is_grounded = True
            self.on_platform = p

        dx = self.x - old_x
        dy = self.y - old_y
        self._move_parts(dx, dy)

    def get_center(self):
        return self.x + BALL_SIZE / 2, self.y + BALL_SIZE / 2


class GamePlatform:
    COLORS = [
        COLOR_STANFORD_RED,
        COLOR_PERU_RED,
        COLOR_GOLD,
        COLOR_DARK_NAVY,
        "#C0392B"
    ]

    def __init__(self, canvas, x, width, height):
        self.canvas  = canvas
        self.x       = x
        self.width   = width
        self.height  = height
        self.color   = random.choice(self.COLORS)
        self.drift   = random.choice([-1, 1])
        self.rect_id = None
        self.draw()

    def draw(self):
        top_y = CANVAS_HEIGHT - self.height
        self.rect_id = self.canvas.create_rectangle(
            self.x, top_y,
            self.x + self.width, CANVAS_HEIGHT,
            self.color, COLOR_BRIGHT_GOLD
        )

    def update(self):
        self.x += self.drift
        self.canvas.move(self.rect_id, self.drift, 0)
        if self.x <= 0:
            self.drift = abs(self.drift)
        elif self.x + self.width >= CANVAS_WIDTH:
            self.drift = -abs(self.drift)

    def move(self, dx):
        self.x += dx
        self.canvas.move(self.rect_id, dx, 0)


class CodeToken:
    SLOTS = [
        (100, 80),  (280, 80),  (440, 80),
        (100, 160), (280, 160), (440, 160),
        (180, 220), (340, 220),
    ]
    used_slots = []
    @classmethod
    def reset_slots(cls):
        cls.used_slots = []

    @classmethod
    def get_free_slot(cls):
        free = [s for s in cls.SLOTS if s not in cls.used_slots]
        if not free:
            cls.used_slots = []
            free = cls.SLOTS[:]
        slot = random.choice(free)
        cls.used_slots.append(slot)
        return slot

    def __init__(self, canvas):
        self.canvas  = canvas
        self.active  = True
        self.snippet = random.choice(CODE_SNIPPETS)
        self.rect_id = None
        self.text_id = None
        self.slot    = None
        self._place()

    def _place(self):
        self.slot      = CodeToken.get_free_slot()
        self.x, self.y = self.slot
        self.rect_id   = self.canvas.create_rectangle(
            self.x, self.y,
            self.x + CODE_W, self.y + CODE_H,
            COLOR_GOLD, COLOR_BRIGHT_GOLD
        )
        self.text_id = self.canvas.create_text(
            self.x + CODE_W / 2,
            self.y + CODE_H / 2,
            text=self.snippet,
            font="Courier", font_size=24, color=COLOR_STANFORD_RED
        )

    def check_catch(self, ball_cx, ball_cy):
        if not self.active:
            return False
        cx = self.x + CODE_W / 2
        cy = self.y + CODE_H / 2
        if (abs(ball_cx - cx) < CODE_W / 2 + BALL_SIZE / 2 and
                abs(ball_cy - cy) < CODE_H / 2 + BALL_SIZE / 2):
            self.canvas.delete(self.rect_id)
            self.canvas.delete(self.text_id)
            if self.slot in CodeToken.used_slots:
                CodeToken.used_slots.remove(self.slot)
            self.active = False
            return True
        return False

    def respawn(self):
        self.snippet = random.choice(CODE_SNIPPETS)
        self._place()
        self.active  = True


def show_win_screen(canvas):
    canvas.create_rectangle(
        0, 0, CANVAS_WIDTH, CANVAS_HEIGHT,
        COLOR_DARK_NAVY, COLOR_DARK_NAVY
    )
    canvas.create_rectangle(
        50, 100, 550, 310,
        COLOR_STANFORD_RED, COLOR_BRIGHT_GOLD
    )
    canvas.create_text(150, 135,
        text="YOU WIN!",
        font="Arial", font_size=38, color=COLOR_BRIGHT_GOLD)
    canvas.create_text(150, 175,
        text="Karel is fully built!",
        font="Arial", font_size=16, color=COLOR_WHITE)
    canvas.create_text(150, 205,
        text="You collected all 10 Karel codes!",
        font="Arial", font_size=13, color=COLOR_WHITE)
    canvas.create_text(150, 240,
        text="Final Score: 100 points",
        font="Arial", font_size=15, color=COLOR_BRIGHT_GOLD)
    canvas.create_text(150, 268,
        text="Keep coding!  PERU @ STANFORD - CODE IN PLACE",
        font="Arial", font_size=11, color=COLOR_BRIGHT_GOLD)
    canvas.create_text(150, 290,
        text="YESSENIA CH. M.",
        font="Arial", font_size=10, color=COLOR_WHITE)


def main():
    canvas = Canvas(CANVAS_WIDTH, CANVAS_HEIGHT)

    CodeToken.reset_slots()

    # Franja superior
    canvas.create_rectangle(
        0, 0, CANVAS_WIDTH, 50,
        COLOR_STANFORD_RED, COLOR_STANFORD_RED
    )

    canvas.create_text(10, 15,
        text="CONTROLS: [ N ] Forward  [ M ] Back  [ B ] JUMP",
        font="Arial", font_size=11, color=COLOR_BRIGHT_GOLD)
    canvas.create_text(10, 35,
        text="CODE CATCHER — Collect 10 Karel codes to build Karel!",
        font="Arial", font_size=11, color=COLOR_WHITE)

    score_label = canvas.create_text(480, 25,
        text="Codes: 0 / 10",
        font="Arial", font_size=13, color=COLOR_BRIGHT_GOLD)

    canvas.create_text(300, CANVAS_HEIGHT - 8,
        text="YESSENIA CH.M. | PERU @ STANFORD UNIVERSITY",
        font="Arial", font_size=11, color=COLOR_STANFORD_RED)

    platforms = [
        GamePlatform(canvas, 30,  140, 80),
        GamePlatform(canvas, 230, 130, 130),
        GamePlatform(canvas, 420, 120, 100),
    ]

    codes = [CodeToken(canvas) for _ in range(CODE_COUNT)]

    p0   = platforms[0]
    ball = KarelBall(canvas, p0.x + 50, CANVAS_HEIGHT - p0.height - BALL_SIZE)
    ball.is_grounded = True
    ball.on_platform = p0

    speed        = 12
    codes_caught = 0
    game_over    = False

    while True:
        if game_over:
            time.sleep(0.1)
            continue

        key = canvas.get_last_key_press()

        if key:
            key = key.lower()
            move_speed = speed * 2 if not ball.is_grounded else speed
            if key == 'n':
                for platform in platforms:
                    platform.move(-move_speed)
            elif key == 'm':
                for platform in platforms:
                    platform.move(move_speed)
            elif key == 'b':
                ball.jump()

        for platform in platforms:
            platform.update()

        ball.update_physics(platforms)

        bx, by = ball.get_center()
        for code in codes:
            if code.check_catch(bx, by):
                codes_caught += 1

                # Karel evolves with every code he captures
                ball.evolve(codes_caught)

                canvas.delete(score_label)
                score_label = canvas.create_text(480, 25,
                    text=f"Codes: {codes_caught} / {CODES_TO_WIN}",
                    font="Arial", font_size=13, color=COLOR_BRIGHT_GOLD)

                if codes_caught >= CODES_TO_WIN:
                    show_win_screen(canvas)
                    game_over = True
                    break

                code.respawn()

        time.sleep(0.04)


if __name__ == '__main__':
    main()