# games.py -- the machines in the studio, playable on the badge.
#
# Each game is a class with update(pressed, held) -> None while playing, ("done", score) when
# the player leaves the game-over screen, ("quit", None) when he walks away (hold B). They draw
# the whole screen every frame; the app repaints the room when they return.
#
#   pinball  A/C flippers, B launches. bumpers score, three balls.
#   runner   B jumps. your own lil guy runs through the studio dodging coffee and cables.
#   echo     watch the four pads light up, repeat with UP / DOWN / A / C. one more each round.

import math
import random
import time
from picovector import color, shape, io

W, H = 320, 240
INK = color.rgb(245, 240, 225)
DIM = color.rgb(150, 150, 150)
YEL = color.rgb(255, 210, 60)
ORG = color.rgb(255, 154, 46)
BG = color.rgb(14, 14, 16)
PANEL = color.rgb(30, 30, 34)


def start(game, ctx):
    return {"pinball": Pinball, "runner": Runner, "echo": Echo}[game](ctx)


class Base:
    title = "?"
    hint = ""

    def __init__(self, ctx):
        self.ctx = ctx
        self.s = ctx["screen"]
        self.look = ctx["look"]
        self.t_prev = time.ticks_ms()
        self.score = 0
        self.over = False
        self.over_t = 0
        self.hold_b = 0
        self.t = 0.0

    def dt(self):
        now = time.ticks_ms()
        d = time.ticks_diff(now, self.t_prev) / 1000.0
        self.t_prev = now
        d = min(0.05, max(0.0, d))
        self.t += d
        return d

    def text(self, s, x, y, pen=INK, font=None):
        self.s.font = font or self.ctx["font_small"]
        self.s.pen = pen
        self.s.text(s, int(x), int(y))

    def ctext(self, s, cx, y, pen=INK, font=None):
        self.s.font = font or self.ctx["font_small"]
        tw = self.s.measure_text(s)[0]
        self.s.pen = pen
        self.s.text(s, int(cx - tw / 2), int(y))

    def quitting(self, held):
        """Hold B for 0.8 s to walk away from the machine."""
        if io.BUTTON_B in held:
            if self.hold_b == 0:
                self.hold_b = time.ticks_ms()
            elif time.ticks_diff(time.ticks_ms(), self.hold_b) > 800:
                return True
        else:
            self.hold_b = 0
        return False

    def end(self):
        if not self.over:
            self.over = True
            self.over_t = time.ticks_ms()

    def chrome(self):
        self.s.pen = color.rgb(0, 0, 0, 150)
        self.s.rectangle(0, 0, W, 13)
        self.s.rectangle(0, H - 12, W, 12)
        self.text(self.title, 4, 2, YEL)
        self.s.font = self.ctx["font_small"]
        tw = self.s.measure_text(self.hint)[0]
        self.text(self.hint, W - 4 - tw, H - 11, DIM)

    def over_screen(self, pressed):
        self.s.pen = color.rgb(0, 0, 0, 170)
        self.s.rectangle(0, 0, W, H)
        self.s.pen = PANEL
        self.s.rectangle(50, 62, W - 100, 110)
        self.ctext("game over", W / 2, 72, YEL, self.ctx["font_big"])
        self.ctext("score  %d" % self.score, W / 2, 98, INK, self.ctx["font_big"])
        top = self.ctx.get("top")
        if top and top.get("score", 0) > self.score:
            self.ctext("top: %s with %d" % (top.get("name", "?"), top["score"]), W / 2, 124, DIM)
        elif top and self.score > 0:
            self.ctext("that beats %s. new top score!" % top.get("name", "the room"), W / 2, 124, ORG)
        elif self.score > 0:
            self.ctext("first score on this machine!", W / 2, 124, ORG)
        self.ctext("B  back to the room", W / 2, 150, DIM)
        if io.BUTTON_B in pressed and time.ticks_diff(time.ticks_ms(), self.over_t) > 600:
            return ("done", self.score)
        return None


# ---------------------------------------------------------------- pinball
TX0, TX1, TY0 = 70, 250, 22
BUMPERS = ((112, 72, 13), (160, 52, 13), (208, 72, 13), (160, 112, 10))
LFLIP = (104, 214)          # pivots
RFLIP = (216, 214)
FLIP_LEN = 42
GUIDES = (((TX0, 160), (104, 214)), ((TX1, 160), (216, 214)))


class Pinball(Base):
    title = "pinball"
    hint = "A/C flip   B launch   hold B leave"

    def __init__(self, ctx):
        super().__init__(ctx)
        self.balls = 3
        self.ball = None
        self.flip = [0, 0]              # ms when each flipper was last pressed
        self.hit = [0, 0, 0, 0]         # bumper flash timers
        self.msg = "B to launch"
        self.pop = 0

    def launch(self):
        self.ball = [TX1 - 18.0, 36.0, -(110.0 + random.random() * 70), 0.0]
        self.msg = ""

    def segment_bounce(self, b, a, c, r):
        ax, ay = a
        cx, cy = c
        dx, dy = cx - ax, cy - ay
        L2 = dx * dx + dy * dy
        u = max(0.0, min(1.0, ((b[0] - ax) * dx + (b[1] - ay) * dy) / L2))
        px, py = ax + u * dx, ay + u * dy
        nx, ny = b[0] - px, b[1] - py
        d = math.sqrt(nx * nx + ny * ny)
        if d < r and d > 0.001:
            nx, ny = nx / d, ny / d
            dot = b[2] * nx + b[3] * ny
            if dot < 0:
                b[2] -= 2 * dot * nx * 0.85
                b[3] -= 2 * dot * ny * 0.85
            b[0], b[1] = px + nx * r, py + ny * r
            return True
        return False

    def flipper_pts(self, side, up):
        px, py = LFLIP if side == 0 else RFLIP
        ang = (-0.55 if up else 0.42)
        if side == 1:
            ang = math.pi - ang
        return (px, py, px + math.cos(ang) * FLIP_LEN, py + math.sin(ang) * FLIP_LEN)

    def update(self, pressed, held):
        dt = self.dt()
        if self.over:
            return self.over_screen(pressed)
        if self.quitting(held):
            return ("quit", None)
        now = time.ticks_ms()
        if io.BUTTON_A in pressed:
            self.flip[0] = now
        if io.BUTTON_C in pressed:
            self.flip[1] = now
        if io.BUTTON_B in pressed and self.ball is None and self.balls > 0:
            self.launch()
        up = [io.BUTTON_A in held or time.ticks_diff(now, self.flip[0]) < 140,
              io.BUTTON_C in held or time.ticks_diff(now, self.flip[1]) < 140]
        b = self.ball
        if b:
            r = 5
            steps = 2
            for _ in range(steps):
                h = dt / steps
                b[3] += 560 * h
                b[0] += b[2] * h
                b[1] += b[3] * h
                if b[0] < TX0 + r:
                    b[0], b[2] = TX0 + r, abs(b[2]) * 0.8
                if b[0] > TX1 - r:
                    b[0], b[2] = TX1 - r, -abs(b[2]) * 0.8
                if b[1] < TY0 + r:
                    b[1], b[3] = TY0 + r, abs(b[3]) * 0.8
                for i, (bx, by, br) in enumerate(BUMPERS):
                    dx, dy = b[0] - bx, b[1] - by
                    d = math.sqrt(dx * dx + dy * dy)
                    if d < br + r and d > 0.001:
                        nx, ny = dx / d, dy / d
                        dot = b[2] * nx + b[3] * ny
                        b[2] -= 2 * dot * nx
                        b[3] -= 2 * dot * ny
                        sp = math.sqrt(b[2] ** 2 + b[3] ** 2)
                        k = max(240.0, sp * 1.06) / max(1.0, sp)
                        b[2] *= k
                        b[3] *= k
                        b[0], b[1] = bx + nx * (br + r + 0.5), by + ny * (br + r + 0.5)
                        self.score += 100 if i < 3 else 250
                        self.hit[i] = now
                        self.pop = now
                for a, c in GUIDES:
                    self.segment_bounce(b, a, c, r)
                for side in (0, 1):
                    fx0, fy0, fx1, fy1 = self.flipper_pts(side, up[side])
                    if self.segment_bounce(b, (fx0, fy0), (fx1, fy1), r + 2) and up[side] and b[3] > -50:
                        b[3] = -(400.0 + random.random() * 90)
                        b[2] += 110 if side == 0 else -110
                        self.score += 10
            if b[1] > H + 12:
                self.ball = None
                self.balls -= 1
                self.msg = "B to launch" if self.balls > 0 else ""
                if self.balls <= 0:
                    self.end()
        # ---- draw
        s = self.s
        s.pen = BG
        s.clear()
        s.pen = color.rgb(255, 210, 60)
        s.rectangle(TX0 - 4, TY0 - 4, TX1 - TX0 + 8, H - TY0 + 4)
        s.pen = color.rgb(24, 22, 30)
        s.rectangle(TX0, TY0, TX1 - TX0, H - TY0)
        s.pen = color.rgb(40, 36, 48)
        for y in range(TY0 + 10, H, 18):
            s.rectangle(TX0, y, TX1 - TX0, 1)
        for i, (bx, by, br) in enumerate(BUMPERS):
            lit = time.ticks_diff(now, self.hit[i]) < 160
            s.pen = color.rgb(255, 230, 120) if lit else (ORG if i < 3 else YEL)
            s.circle(bx, by, br + (2 if lit else 0))
            s.pen = color.rgb(60, 40, 20)
            s.circle(bx, by, max(2, br - 6))
        s.pen = color.rgb(120, 120, 130)
        for a, c in GUIDES:
            s.shape(shape.line(a[0], a[1], c[0], c[1], 3))
        for side in (0, 1):
            fx0, fy0, fx1, fy1 = self.flipper_pts(side, up[side])
            s.pen = YEL if up[side] else color.rgb(200, 170, 60)
            s.shape(shape.line(fx0, fy0, fx1, fy1, 6))
            s.pen = color.rgb(60, 60, 70)
            s.circle(int(fx0), int(fy0), 4)
        if b:
            s.pen = color.rgb(230, 230, 240)
            s.circle(int(b[0]), int(b[1]), 5)
            s.pen = color.rgb(255, 255, 255)
            s.circle(int(b[0] - 1), int(b[1] - 2), 2)
        # side panels
        self.text("score", 6, 30, DIM)
        self.text(str(self.score), 6, 42, YEL, self.ctx["font_big"])
        self.text("balls", 6, 72, DIM)
        for i in range(self.balls):
            s.pen = INK
            s.circle(12 + i * 14, 92, 5)
        top = self.ctx.get("top")
        if top:
            self.text("top", 6, 112, DIM)
            self.text(str(top.get("score", 0)), 6, 124, ORG)
            self.text(str(top.get("name", ""))[:9], 6, 136, DIM)
        if self.msg:
            self.ctext(self.msg, (TX0 + TX1) / 2, 150, INK)
        if time.ticks_diff(now, self.pop) < 300:
            self.ctext("+" , TX1 + 30, 60, YEL, self.ctx["font_big"])
        self.chrome()
        return None


# ---------------------------------------------------------------- runner
class Runner(Base):
    title = "WES RUN"
    hint = "B jump   hold B leave"

    def __init__(self, ctx):
        super().__init__(ctx)
        self.gy = 196
        self.x = 72
        self.y = 0.0            # height above the floor
        self.vy = 0.0
        self.speed = 190.0
        self.dist = 0.0
        self.obs = []           # [x, w, h, kind]
        self.next_gap = 260
        self.stars = [(random.randrange(0, W), random.randrange(20, 120)) for _ in range(16)]
        self.dead_t = 0

    def update(self, pressed, held):
        dt = self.dt()
        if self.over:
            return self.over_screen(pressed)
        if self.quitting(held):
            return ("quit", None)
        if io.BUTTON_B in pressed and self.y <= 0.01:
            self.vy = 540.0
        self.vy -= 1500.0 * dt
        self.y = max(0.0, self.y + self.vy * dt)
        if self.y <= 0:
            self.vy = 0.0
        self.speed = min(560.0, self.speed + 9.0 * dt)
        self.dist += self.speed * dt
        self.score = int(self.dist / 10)
        for o in self.obs:
            o[0] -= self.speed * dt
        self.obs = [o for o in self.obs if o[0] + o[1] > -10]
        last = max([o[0] + o[1] for o in self.obs]) if self.obs else -1000.0
        if last < W - self.next_gap and self.t > 1.8:          # a breath before the first one
            kind = random.randrange(3)
            w, h = ((18, 20), (30, 14), (14, 32))[kind]
            self.obs.append([W + 10.0, w, h, kind])
            self.next_gap = random.randrange(170, 330) + int(self.speed * 0.25)
        # collision: feet box vs obstacle box
        R = 15
        gx0, gx1 = self.x - R * 0.7, self.x + R * 0.7
        gy1 = self.gy - self.y
        gy0 = gy1 - R * 3.2
        for o in self.obs:
            ox0, ox1, oy0 = o[0], o[0] + o[1], self.gy - o[2]
            if gx1 > ox0 + 3 and gx0 < ox1 - 3 and gy1 > oy0 + 3 and gy0 < self.gy:
                self.end()
        # ---- draw
        s = self.s
        s.pen = BG
        s.clear()
        s.pen = color.rgb(40, 36, 44)
        for i, (sx, sy) in enumerate(self.stars):
            x = (sx - self.dist * (0.15 + 0.05 * (i % 3))) % W
            s.rectangle(int(x), sy, 2, 2)
        s.pen = color.rgb(44, 32, 23)
        s.rectangle(0, self.gy, W, H - self.gy)
        s.pen = color.rgb(28, 20, 14)
        off = int(self.dist) % 40
        for x in range(-off, W, 40):
            s.rectangle(x, self.gy, 1, H - self.gy)
        s.pen = color.rgb(90, 60, 40)
        s.rectangle(0, self.gy, W, 2)
        for o in self.obs:
            x, w, h, kind = int(o[0]), o[1], o[2], o[3]
            if kind == 0:        # coffee mug
                s.pen = YEL
                s.rectangle(x, self.gy - h, w, h)
                s.pen = color.rgb(60, 40, 20)
                s.rectangle(x + 2, self.gy - h + 2, w - 4, 4)
                s.pen = YEL
                s.circle(x + w + 3, self.gy - h + 9, 4)
                s.pen = BG
                s.circle(x + w + 3, self.gy - h + 9, 2)
            elif kind == 1:      # cable box
                s.pen = color.rgb(110, 110, 120)
                s.rectangle(x, self.gy - h, w, h)
                s.pen = ORG
                s.rectangle(x + 4, self.gy - h + 4, 4, 4)
            else:                # mic stand
                s.pen = color.rgb(60, 60, 66)
                s.rectangle(x + w // 2 - 1, self.gy - h, 3, h)
                s.rectangle(x, self.gy - 3, w, 3)
                s.pen = color.rgb(212, 175, 55)
                s.circle(x + w // 2, self.gy - h, 5)
        phase = (self.dist / 40.0) % 1.0
        self.ctx["draw_guy"](s, self.x, self.gy - self.y, R, self.look, phase, 1, self.y <= 0.01, "wander", self.t)
        self.text("distance", 6, 18, DIM)
        self.text(str(self.score), 6, 30, YEL, self.ctx["font_big"])
        top = self.ctx.get("top")
        if top:
            self.text("top %d  %s" % (top.get("score", 0), str(top.get("name", ""))[:10]), 6, 50, DIM)
        self.chrome()
        return None


# ---------------------------------------------------------------- echo
PADS = (("UP", 160, 70, io.BUTTON_UP), ("DOWN", 160, 170, io.BUTTON_DOWN), ("A", 80, 120, io.BUTTON_A), ("C", 240, 120, io.BUTTON_C))
PAD_C = ((255, 210, 60), (255, 154, 46), (230, 230, 230), (140, 140, 150))


class Echo(Base):
    title = "ECHO"
    hint = "UP/DOWN/A/C repeat it   hold B leave"

    def __init__(self, ctx):
        super().__init__(ctx)
        self.seq = []
        self.phase = "pause"
        self.phase_t = time.ticks_ms()
        self.i = 0
        self.lit = -1
        self.lit_t = 0
        self.rounds = 0

    def update(self, pressed, held):
        self.dt()
        if self.over:
            return self.over_screen(pressed)
        if self.quitting(held):
            return ("quit", None)
        now = time.ticks_ms()
        el = time.ticks_diff(now, self.phase_t)
        if self.phase == "pause":
            if el > 700:
                self.seq.append(random.randrange(4))
                self.phase, self.phase_t, self.i = "show", now, 0
        elif self.phase == "show":
            step = max(260, 560 - len(self.seq) * 25)
            k = el // step
            if k >= len(self.seq):
                self.phase, self.phase_t, self.i, self.lit = "input", now, 0, -1
            else:
                self.lit = self.seq[k] if (el % step) < step * 0.65 else -1
        elif self.phase == "input":
            for pi, (_n, _x, _y, btn) in enumerate(PADS):
                if btn in pressed:
                    self.lit, self.lit_t = pi, now
                    if pi == self.seq[self.i]:
                        self.i += 1
                        self.score += 1
                        if self.i >= len(self.seq):
                            self.rounds += 1
                            self.score += 10
                            self.phase, self.phase_t = "pause", now
                    else:
                        self.lit = self.seq[self.i]
                        self.end()
            if el > 6000:
                self.end()
            if self.lit >= 0 and self.phase == "input" and time.ticks_diff(now, self.lit_t) > 220:
                self.lit = -1
        # ---- draw
        s = self.s
        s.pen = BG
        s.clear()
        s.pen = PANEL
        s.circle(160, 120, 92)
        s.pen = BG
        s.circle(160, 120, 24)
        for pi, (nm, x, y, _b) in enumerate(PADS):
            c = PAD_C[pi]
            on = self.lit == pi
            s.pen = color.rgb(c[0], c[1], c[2]) if on else color.rgb(c[0] // 3, c[1] // 3, c[2] // 3)
            s.circle(x, y, 30 if on else 27)
            self.ctext(nm, x, y - 4, color.rgb(20, 18, 24) if on else DIM)
        self.ctext("round %d" % max(1, len(self.seq)), 160, 112, YEL)
        self.ctext({"show": "watch...", "input": "your turn", "pause": "ready"}[self.phase], 160, 124, DIM)
        self.text("score", 6, 18, DIM)
        self.text(str(self.score), 6, 30, YEL, self.ctx["font_big"])
        top = self.ctx.get("top")
        if top:
            self.text("top %d  %s" % (top.get("score", 0), str(top.get("name", ""))[:10]), 6, 50, DIM)
        self.chrome()
        return None
