from random import randint
from math import sin, cos, sqrt
import pyxel
 
# ── constants ────────────────────────────────────────────────────────────────
MAX_LAPS = 3
SW, SH   = 160, 100
HW       = 80           # each player's viewport width
 
SX, SY   = 35.0, 34.14  # world start position
CDIST    = 0.9         # distance from start that triggers "checkpoint cleared"
LDIST    = 0.5       # distance from start that triggers lap count
 
 
def fmt(f):
    """Frames → M:SS.cc"""
    s  = f // 30
    cs = (f % 30) * 100 // 30
    return f"{s // 60}:{s % 60:02d}.{cs:02d}"
 
 
# ── Player ───────────────────────────────────────────────────────────────────
class Player:
    def __init__(self, x, y, veh, side):
        self.x, self.y = float(x), float(y)
        self.dir = self.vel = self.vdir = 0.0
        self.veh  = veh          # "car" | "bike"
        self.side = side         # 0 = left, 1 = right
        self.xo   = side * HW   # x pixel offset for this viewport
 
        # race state
        self.lap   = 0
        self.chk   = False       # checkpoint passed this lap?
        self.lt    = 0           # current lap timer (frames)
        self.best  = None        # best lap (frames)
        self.last  = 0           # last completed lap time
        self.done  = False
        self.place = 0           # 1 or 2 after finish
 
        # animation helpers
        self.ts  = 0             # turn state −1/0/1
        self.acc = False
        self.bt  = 0             # bob timer
        self.et  = 0             # exhaust timer
        self.ep  = []            # exhaust particles  {x,y,l,c}
        self.pd      = 999.0     # previous dist from start
        self.outside = False     # hat die LDIST-Zone verlassen?
 
        # visual flash timers
        self.cf = 0              # checkpoint-cleared flash
        self.sf = 0              # start-line / lap-done flash
 
    # ── physics & lap logic ─────────────────────────────────────────────────
    def update(self, L, R, U, D, go):
        if self.veh == "bike":
            a, t, d, r = 0.014, 0.0083, 0.984, 0.88
        else:
            a, t, d, r = 0.013, 0.0079, 0.99,  0.84
 
        if go and not self.done:
            self.vel  += a * (D - U)
            self.vdir += t * (R - L)
        self.vel  *= d
        self.vdir *= r
        self.dir  += self.vdir
        self.x    += self.vel * sin(self.dir - 80.11061)
        self.y    += self.vel * cos(self.dir - 80.11061)
 
        self.ts  = (1 if R and not L else -1 if L and not R else 0)
        self.acc = bool(U)
        self.bt += 1
        if self.cf: self.cf -= 1
        if self.sf: self.sf -= 1
 
        # exhaust particles
        self.et += 1
        if self.acc and self.et >= 3:
            self.et = 0
            cx = self.xo + HW // 2
            self.ep.append({"x": cx + randint(-2, 2),
                            "y": 91 + randint(-1, 1),
                            "l": randint(6, 12),
                            "c": 7 if randint(0, 1) else 13})
        for p in self.ep:
            p["y"] += 1
            p["l"] -= 1
        self.ep = [p for p in self.ep if p["l"] > 0]
 
        if go and not self.done:
            self.lt += 1
            self._check_lap()
 
    def _check_lap(self):
        dx   = self.x - SX
        dy   = self.y - SY
        dist = sqrt(dx * dx + dy * dy)
 
        # Checkpoint: einmal klar von der Startlinie entfernt gewesen
        if dist > CDIST and not self.chk:
            self.chk = True
            self.cf  = 25
 
        # Ziellinie: nur auslösen wenn Spieler die Zone wirklich verlassen hatte
        if dist < LDIST and self.chk and self.outside:
            self.last = self.lt
            if self.best is None or self.lt < self.best:
                self.best = self.lt
            self.lt      = 0
            self.chk     = False
            self.outside = False
            self.lap    += 1
            self.sf      = 25
            if self.lap >= MAX_LAPS:
                self.done = True
 
        if dist >= LDIST:
            self.outside = True
 
        self.pd = dist
 
    # ── rendering ────────────────────────────────────────────────────────────
    def render(self):
        xo  = self.xo
        dx  = self.x - SX
        dy  = self.y - SY
        dss = sqrt(dx * dx + dy * dy)   # distance from start
 
        # sky gradient — fills all 40 rows above the road (rows 0-39)
        pyxel.rect(xo,  0, HW, 14, 12)   # light blue
        pyxel.rect(xo, 14, HW, 14, 7)    # pale / white horizon
        pyxel.rect(xo, 28, HW, 12, 6)    # warm horizon haze
 
        # ── raycasting road ─────────────────────────────────────────────────
        mul = 64.0
        for ry in range(60):
            mul *= (100 - mul) / 100
            cx = (self.x + 4 * sin(self.dir - 80.11061)
                  + 2.25 * mul * sin(self.dir - 90))
            cy = (self.y + 4 * cos(self.dir - 80.11061)
                  + 2.25 * mul * cos(self.dir - 90))
 
            for rx in range(HW):
                pyxel.bltm(xo + rx, ry + 40, 0,
                            int(cy * 30), int(cx * 30), 1, 1)
                cx += (mul / 40) * sin(self.dir)
                cy += (mul / 40) * cos(self.dir)
 
            if 34 <= ry <= 37 and not self.chk and dss > 1.0:
                for rx in range(HW):
                    pyxel.pset(xo + rx, ry + 40, 10)
 
            if ry >= 57 and (dss < LDIST * 3 or self.sf):
                for rx in range(HW):
                    c = 7 if (rx // 5 + ry // 2) % 2 == 0 else 0
                    pyxel.pset(xo + rx, ry + 40, c)
 
        # ── vehicle sprite ───────────────────────────────────────────────────
        spd  = abs(self.vel)
        by   = int((1.5 if spd > 0.05 else 0.5) *
                    sin(self.bt * (0.18 if spd > 0.05 else 0.07)))
        lx   = self.ts * 2
        sy2  = 32 if self.ts == 0 else 64
        fw   = 32 if self.ts >= 0 else -32
        if self.veh == "bike":
            sy2 += 64
        shk = randint(-1, 1) if self.acc and spd > 0.1 else 0
 
        for p in self.ep:
            pyxel.pset(p["x"] + shk, p["y"] + by, p["c"])
 
        pyxel.blt(xo + 24 + lx + shk, 65 + by, 0, 0, sy2, fw, 32, 15)
 
        # ── HUD ─────────────────────────────────────────────────────────────
        pn = "P1" if self.side == 0 else "P2"
        vn = "MOTO" if self.veh == "bike" else "AUTO"
        pyxel.text(xo + 2, 2, f"{pn} {vn}", 7)
 
        ls = min(self.lap + 1, MAX_LAPS)
        pyxel.text(xo + HW - 22, 2, f"L{ls}/{MAX_LAPS}", 7)
 
        pyxel.text(xo + 2, 92, fmt(self.lt), 11)
 
        if self.best is not None:
            pyxel.text(xo + 2, 84, f"B:{fmt(self.best)}", 6)
 
        if self.cf:
            for i in range(HW):
                pyxel.pset(xo + i, 96, 10 if (i // 4) % 2 == 0 else 9)
 
        if self.done:
            pl = "1ST!" if self.place == 1 else "2ND"
            pyxel.rect (xo + 5, 44, HW - 10, 24, 0)
            pyxel.rectb(xo + 5, 44, HW - 10, 24, 10)
            pyxel.text(xo + 10, 48, f"FINISH {pl}", 10)
            if self.best is not None:
                pyxel.text(xo + 10, 58, f"BEST:{fmt(self.best)}", 11)
 
 
# ── Game ─────────────────────────────────────────────────────────────────────
class Game:
    def __init__(self):
        self.phase  = "mp1"
        self.msel   = 0
        self.v1     = "car"
        self.v2     = "car"
        self.p1 = self.p2 = None
        self.cd     = 0
        self.go_f   = 0
        self.finc   = 0
 
        pyxel.init(SW, SH, title="2P RACE – 4 LAPS", fps=30)
        pyxel.load("resx.pyxres")
        pyxel.run(self.upd, self.drw)
 
    # ── update ───────────────────────────────────────────────────────────────
    def upd(self):
        if self.phase in ("mp1", "mp2"):
            if pyxel.btnp(pyxel.KEY_LEFT) or pyxel.btnp(pyxel.KEY_RIGHT):
                self.msel ^= 1
            if pyxel.btnp(pyxel.KEY_RETURN) or pyxel.btnp(pyxel.KEY_SPACE):
                v = "car" if self.msel == 0 else "bike"
                if self.phase == "mp1":
                    self.v1 = v
                    self.msel = 0
                    self.phase = "mp2"
                else:
                    self.v2 = v
                    self.p1 = Player(SX - 0.08, SY, self.v1, 0)
                    self.p2 = Player(SX + 0.08, SY, self.v2, 1)
                    self.cd    = 90
                    self.phase = "cd"
            return
 
        if self.phase == "cd":
            self.cd -= 1
            if self.cd <= 0:
                self.phase = "race"
                self.go_f  = 25
            return
 
        if self.go_f:
            self.go_f -= 1
 
        go = (self.phase == "race")
 
        # P1: WASD
        self.p1.update(pyxel.btn(pyxel.KEY_A),
                       pyxel.btn(pyxel.KEY_D),
                       pyxel.btn(pyxel.KEY_W),
                       pyxel.btn(pyxel.KEY_S),    go)
        # P2: arrow keys
        self.p2.update(pyxel.btn(pyxel.KEY_LEFT),
                       pyxel.btn(pyxel.KEY_RIGHT),
                       pyxel.btn(pyxel.KEY_UP),
                       pyxel.btn(pyxel.KEY_DOWN), go)
 
        if go:
            for p in (self.p1, self.p2):
                if p.done and p.place == 0:
                    self.finc += 1
                    p.place = self.finc
            if self.p1.done and self.p2.done:
                self.phase = "done"
 
    # ── draw ─────────────────────────────────────────────────────────────────
    def drw(self):
        pyxel.cls(0)
 
        if self.phase in ("mp1", "mp2"):
            tag   = "P1" if self.phase == "mp1" else "P2"
            title = f"{tag} - FAHRZEUG WAHLEN"
            pyxel.text(SW // 2 - len(title) * 2, 8, title, 7)
            pyxel.line(20, 16, 140, 16, 5)
            self._px_auto(15, 24, self.msel == 0)
            self._px_bike(90, 24, self.msel == 1)
            pyxel.line(0, 82, 160, 82, 5)
            pyxel.text(20, 86, "< >  WAHLEN   ENTER  WEITER", 6)
            return
 
        self.p1.render()
        self.p2.render()
 
        pyxel.line(HW, 0, HW, SH - 1, 0)
 
        if self.phase == "cd":
            n   = (self.cd - 1) // 30 + 1
            lbl = str(n)
            col = 8
            for hx in (HW // 2, HW + HW // 2):
                tw = len(lbl) * 4
                pyxel.rect(hx - tw // 2 - 3, 42, tw + 6, 13, 0)
                pyxel.text(hx - tw // 2, 46, lbl, col)
 
        if self.go_f:
            for hx in (HW // 2, HW + HW // 2):
                pyxel.rect(hx - 7, 42, 18, 13, 0)
                pyxel.text(hx - 5, 46, "GO!", 11)
 
        # controls reminder
        if self.phase == "race" and self.p1.lt < 90:
            pyxel.text(2,      93, "P1:WASD",   13)
            pyxel.text(HW + 2, 93, "P2:ARROWS", 13)
 
    # ── pixel-art menu drawings ───────────────────────────────────────────────
    def _px_auto(self, ax, ay, sel):
        c = 11 if sel else 5
        for dx in range(3, 9):
            pyxel.rect(ax + dx*2, ay, 2, 2, 12)
        pyxel.rect(ax, ay+2, 2, 2, c)
        for dx in range(1, 8):
            pyxel.rect(ax + dx*2, ay+2, 2, 2, 7 if 2 <= dx <= 5 else 12)
        pyxel.rect(ax+16, ay+2, 2, 2, c)
        for dx in range(10):
            pyxel.rect(ax + dx*2, ay+4, 2, 2, c)
        pyxel.rect(ax, ay+6, 2, 2, 1)
        pyxel.rect(ax+4, ay+6, 2, 2, 0)
        for dx in [1, 3, 4, 5, 7, 9]:
            pyxel.rect(ax + dx*2, ay+6, 2, 2, 1)
        pyxel.rect(ax+12, ay+6, 2, 2, 0)
        for wx in [2, 8]:
            pyxel.rect(ax + wx*2,   ay+8, 2, 2, 5)
            pyxel.rect(ax + wx*2+2, ay+8, 2, 2, 13)
            pyxel.rect(ax + wx*2+4, ay+8, 2, 2, 5)
            pyxel.pset(ax + wx*2+2, ay+8, 7)
        pyxel.rect(ax+18, ay+2, 2, 1, 10)
        pyxel.rectb(ax-2, ay-2, 24, 14, c)
        pyxel.text(ax+2, ay+13, "AUTO", c)
 
    def _px_bike(self, mx, my, sel):
        c = 11 if sel else 5
        for dx, dy in [(1,4),(2,3),(3,2),(4,3),(5,4),(2,4),(3,4),(4,4)]:
            pyxel.rect(mx + dx*2, my + dy*2, 2, 2, 13 if dy < 4 else 5)
        pyxel.pset(mx+6, my+6, 7)
        for dx, dy in [(6,4),(7,3),(8,2),(9,2),(10,3)]:
            pyxel.rect(mx + dx*2, my + dy*2, 2, 2, 8)
        for dx in [8, 9]:
            pyxel.rect(mx + dx*2, my, 2, 2, 2)
        pyxel.rect(mx+20, my+2, 2, 2, 2)
        pyxel.rect(mx+16, my+2, 4, 1, 7)
        pyxel.rect(mx+14, my+1, 4, 1, 13)
        pyxel.rect(mx+24, my,   1, 3, 6)
        pyxel.rect(mx+22, my,   4, 1, 7)
        for dx, dy in [(12,4),(13,3),(14,4)]:
            pyxel.rect(mx + dx*2, my + dy*2, 2, 2, 5)
        pyxel.rect(mx+26, my+6, 2, 2, 13)
        pyxel.pset(mx+26, my+7, 7)
        pyxel.rect(mx+24, my+4, 1, 3, 6)
        pyxel.rect(mx+26, my+4, 1, 3, 6)
        pyxel.rect(mx+2,  my+9, 18, 1, 7)
        pyxel.rect(mx,    my+9,  2, 1, 6)
        pyxel.rectb(mx-2, my-2, 34, 14, c)
        pyxel.text(mx, my+13, "MOTORRAD", c)
 
 
Game()