import pyxel
import random
 
# ──────────────────────────────────────────────
#  KONSTANTEN
# ──────────────────────────────────────────────
WIDTH   = 256
HEIGHT  = 192
FPS     = 30
 
BOARD_W = 8    # Spalten (schmaler für 2 Boards nebeneinander)
BOARD_H = 18   # Zeilen
CELL    = 8    # Pixelgröße einer Zelle
 
# Board-Positionen
P1_BX, P1_BY = 14, 8
P2_BX, P2_BY = 256 - 14 - BOARD_W * CELL, 8
 
GARBAGE_DELAY = FPS // 2   # Frames bis ausstehender Garbage einschlägt
 
# ──────────────────────────────────────────────
#  TETROMINOS
# ──────────────────────────────────────────────
PIECES = {
    "I": {"color": 12, "rot": [
        [(0,1),(1,1),(2,1),(3,1)],
        [(2,0),(2,1),(2,2),(2,3)],
        [(0,2),(1,2),(2,2),(3,2)],
        [(1,0),(1,1),(1,2),(1,3)],
    ]},
    "O": {"color": 10, "rot": [
        [(1,0),(2,0),(1,1),(2,1)],
    ]},
    "T": {"color": 2, "rot": [
        [(1,0),(0,1),(1,1),(2,1)],
        [(1,0),(1,1),(2,1),(1,2)],
        [(0,1),(1,1),(2,1),(1,2)],
        [(1,0),(0,1),(1,1),(1,2)],
    ]},
    "S": {"color": 3, "rot": [
        [(1,0),(2,0),(0,1),(1,1)],
        [(1,0),(1,1),(2,1),(2,2)],
        [(1,1),(2,1),(0,2),(1,2)],
        [(0,0),(0,1),(1,1),(1,2)],
    ]},
    "Z": {"color": 8, "rot": [
        [(0,0),(1,0),(1,1),(2,1)],
        [(2,0),(1,1),(2,1),(1,2)],
        [(0,1),(1,1),(1,2),(2,2)],
        [(1,0),(0,1),(1,1),(0,2)],
    ]},
    "J": {"color": 9, "rot": [
        [(0,0),(0,1),(1,1),(2,1)],
        [(1,0),(2,0),(1,1),(1,2)],
        [(0,1),(1,1),(2,1),(2,2)],
        [(1,0),(1,1),(0,2),(1,2)],
    ]},
    "L": {"color": 14, "rot": [
        [(2,0),(0,1),(1,1),(2,1)],
        [(1,0),(1,1),(1,2),(2,2)],
        [(0,1),(1,1),(2,1),(0,2)],
        [(0,0),(1,0),(1,1),(1,2)],
    ]},
}
PIECE_NAMES = list(PIECES.keys())
 
SHADE = {12: 1, 10: 11, 2: 1, 3: 11, 8: 2, 9: 4, 14: 10}
 
 
# ──────────────────────────────────────────────
#  EIN SPIELFELD (für 1 Spieler im VS-Modus)
# ──────────────────────────────────────────────
class Board:
    def __init__(self, keymap, bx, by, name, color, sound_channel=0):
        self.keymap = keymap     # dict: left,right,rot_cw,rot_ccw,soft,hard,hold
        self.bx, self.by = bx, by
        self.name  = name
        self.pcol  = color       # Farbe des Spielernamens / Rahmens
        self.sound_channel = sound_channel
 
        self.board  = [[0] * BOARD_W for _ in range(BOARD_H)]
        self.colors = [[0] * BOARD_W for _ in range(BOARD_H)]
        self.lines  = 0
        self.level  = 1
        self.bag    = []
        self._refill_bag()
        self.current   = self._spawn()
        self.next_name = self._pick()
        self.over      = False
        self.winner    = False
        self.opponent  = None
 
        self.drop_timer    = 0
        self.lock_timer    = 0
        self.lock_delay    = FPS // 2
        self.das_timer     = 0
        self.das_charged   = False
        self.last_move_dir = 0
        self.line_flash    = []
        self.flash_timer   = 0
        self.combo         = 0
        self.frame_count   = 0
        self.pieces_placed = 0
        self.clears_count  = 0
 
        # Garbage-System
        self.pending_garbage = 0     # Anzahl ausstehender Reihen
        self.garbage_timer   = 0
        self.shake           = 0     # Bildschirm-Shake beim Einschlag
        self.last_send       = 0     # Wie viele Zeilen zuletzt gesendet (für HUD)
 
    # ── Bag / Spawn ──
    def _refill_bag(self):
        self.bag = list(PIECE_NAMES)
        random.shuffle(self.bag)
 
    def _pick(self):
        if not self.bag:
            self._refill_bag()
        return self.bag.pop()
 
    def _spawn(self, name=None):
        if name is None:
            name = self._pick()
        data = PIECES[name]
        return {"name": name, "color": data["color"], "rot": 0,
                "x": BOARD_W // 2 - 2, "y": 0}
 
    def cells(self, piece=None):
        if piece is None:
            piece = self.current
        data  = PIECES[piece["name"]]
        rot   = piece["rot"] % len(data["rot"])
        return [(piece["x"] + dx, piece["y"] + dy) for dx, dy in data["rot"][rot]]
 
    def _valid(self, piece):
        for cx, cy in self.cells(piece):
            if cx < 0 or cx >= BOARD_W or cy >= BOARD_H:
                return False
            if cy >= 0 and self.board[cy][cx] != 0:
                return False
        return True
 
    def _lock(self):
        col = self.current["color"]
        for cx, cy in self.cells():
            if cy >= 0:
                self.board[cy][cx]  = 1
                self.colors[cy][cx] = col
        cleared = self._clear_lines()
        self.pieces_placed += 1
        self.current   = self._spawn(self.next_name)
        self.next_name = self._pick()
        if not self._valid(self.current):
            self.over = True
        return cleared
 
    def _clear_lines(self):
        full = [r for r in range(BOARD_H) if all(self.board[r][c] for c in range(BOARD_W))]
        if full:
            self.line_flash  = full
            self.flash_timer = 10
            for r in sorted(full, reverse=True):
                del self.board[r]
                del self.colors[r]
                self.board.insert(0,  [0] * BOARD_W)
                self.colors.insert(0, [0] * BOARD_W)
            n = len(full)
            self.combo += 1
            self.lines += n
            self.level  = self.lines // 10 + 1
        else:
            self.combo = 0
        return len(full)
 
    def drop_interval(self):
        # Wird mit jedem abgelegten Block ein kleines bisschen schneller
        base = FPS - (self.level - 1) * 2 - self.pieces_placed // 2
        return max(2, base)
 
    # ── Garbage einfügen ──
    def receive_garbage(self, n):
        self.pending_garbage += n
 
    def _apply_garbage(self):
        n = self.pending_garbage
        if n <= 0:
            return
        self.pending_garbage = 0
        self.shake = 8
        hole = random.randint(0, BOARD_W - 1)
        for _ in range(n):
            del self.board[0]
            del self.colors[0]
            row = [1] * BOARD_W
            crow = [5] * BOARD_W
            row[hole]  = 0
            crow[hole] = 0
            self.board.append(row)
            self.colors.append(crow)
        # Aktuelles Stück nach oben verschieben falls überlappend
        tries = 0
        while not self._valid(self.current) and tries < BOARD_H + 4:
            self.current["y"] -= 1
            tries += 1
        # Falls selbst ganz oben kein Platz mehr ist: Spiel ist vorbei
        if not self._valid(self.current):
            self.over = True
 
    # ── Input ──
    def handle_input(self):
        if self.over or self.flash_timer > 0:
            return
        km = self.keymap
 
        if pyxel.btnp(km["rot"]):
            self._rotate(1)
 
        if pyxel.btn(km["soft"]):
            moved = self._move(0, 1)
            if moved:
                self.drop_timer = 0
            else:
                cleared = self._lock()
                self._send_garbage(cleared)
                pyxel.play(self.sound_channel, 0)
                self.drop_timer = 0
                self.lock_timer = 0
                return
 
        left  = pyxel.btn(km["left"])
        right = pyxel.btn(km["right"])
        if left or right:
            d = -1 if left else 1
            if pyxel.btnp(km["left"]) or pyxel.btnp(km["right"]):
                self._move(d, 0)
                self.das_timer = 0
                self.das_charged = False
                self.last_move_dir = d
            else:
                if d == self.last_move_dir:
                    self.das_timer += 1
                    if not self.das_charged and self.das_timer >= 10:
                        self.das_charged = True
                    if self.das_charged and self.das_timer % 2 == 0:
                        self._move(d, 0)
        else:
            self.das_timer = 0
            self.das_charged = False
 
    def _move(self, dx, dy):
        t = dict(self.current); t["x"] += dx; t["y"] += dy
        if self._valid(t):
            self.current = t
            if dy == 0:
                self.lock_timer = 0
            return True
        return False
 
    def _rotate(self, d):
        t = dict(self.current)
        rots = len(PIECES[t["name"]]["rot"])
        t["rot"] = (t["rot"] + d) % rots
        for ox in [0, -1, 1, -2, 2]:
            k = dict(t); k["x"] += ox
            if self._valid(k):
                self.current = k
                self.lock_timer = 0
                return
 
    def _send_garbage(self, cleared_lines):
        """Alle 2 erfolgreichen Zeilen-Löschungen wird 1 Garbage-Zeile zum Gegner geschickt."""
        if cleared_lines <= 0:
            self.last_send = 0
            return
        self.clears_count += 1
        if self.clears_count % 2 != 0:
            self.last_send = 0
            return
        send = 1
        # Cancel: eigener ausstehender Garbage wird zuerst abgebaut
        if self.pending_garbage > 0:
            cancel = min(send, self.pending_garbage)
            self.pending_garbage -= cancel
            send -= cancel
        self.last_send = send
        if send > 0 and self.opponent is not None:
            self.opponent.receive_garbage(send)
 
    # ── Update ──
    def update(self):
        if self.over:
            return
        self.frame_count += 1
 
        if self.flash_timer > 0:
            self.flash_timer -= 1
            if self.flash_timer == 0:
                self._apply_garbage()
            return
 
        if self.shake > 0:
            self.shake -= 1
 
        if self.pending_garbage > 0 and self.flash_timer == 0:
            self.garbage_timer += 1
            if self.garbage_timer >= GARBAGE_DELAY:
                self.garbage_timer = 0
                self._apply_garbage()
 
        self.drop_timer += 1
        if self.drop_timer >= self.drop_interval():
            self.drop_timer = 0
            if not self._move(0, 1):
                cleared = self._lock()
                self._send_garbage(cleared)
                pyxel.play(self.sound_channel, 0)
            self.lock_timer = 0
 
 
# Keymaps
KEYMAP_P1 = {
    "left": pyxel.KEY_A, "right": pyxel.KEY_D,
    "rot": pyxel.KEY_W,
    "soft": pyxel.KEY_S,
}
KEYMAP_P2 = {
    "left": pyxel.KEY_LEFT, "right": pyxel.KEY_RIGHT,
    "rot": pyxel.KEY_UP,
    "soft": pyxel.KEY_DOWN,
}
 
 
# ──────────────────────────────────────────────
#  ZEICHNEN
# ──────────────────────────────────────────────
def draw_cell(bx, by, col, row, color):
    px, py = bx + col * CELL, by + row * CELL
    pyxel.rect(px, py, CELL, CELL, color)
    pyxel.line(px, py, px + CELL - 2, py, 7)
    pyxel.line(px, py, px, py + CELL - 2, 7)
    shade_c = SHADE.get(color, 1)
    pyxel.line(px + CELL - 1, py + 1, px + CELL - 1, py + CELL - 1, shade_c)
    pyxel.line(px + 1, py + CELL - 1, px + CELL - 1, py + CELL - 1, shade_c)
 
def draw_mini_piece(name, px, py):
    data  = PIECES[name]
    color = data["color"]
    cells = data["rot"][0]
    min_x = min(dx for dx, dy in cells)
    min_y = min(dy for dy, dy in cells)
    for dx, dy in cells:
        cx = px + (dx - min_x) * 5
        cy = py + (dy - min_y) * 5
        pyxel.rect(cx, cy, 4, 4, color)
 
def draw_board(b: Board):
    shake_x = random.randint(-1, 1) if b.shake > 0 else 0
    bx = b.bx + shake_x
    by = b.by
 
    # Hintergrund
    pyxel.rect(bx - 1, by - 1, BOARD_W * CELL + 2, BOARD_H * CELL + 2, 1)
    pyxel.rectb(bx - 1, by - 1, BOARD_W * CELL + 2, BOARD_H * CELL + 2, b.pcol)
 
    for r in range(BOARD_H):
        for c in range(BOARD_W):
            if b.board[r][c]:
                flash = r in b.line_flash and b.flash_timer > 0
                col = 7 if flash and b.flash_timer % 4 < 2 else b.colors[r][c]
                draw_cell(bx, by, c, r, col)
            else:
                pyxel.pset(bx + c * CELL + CELL // 2, by + r * CELL + CELL // 2, 1)
 
    if not b.over and b.flash_timer == 0:
        for cx, cy in b.cells():
            if cy >= 0:
                draw_cell(bx, by, cx, cy, b.current["color"])
 
    # Garbage-Anzeige: roter Balken links/rechts vom Board
    if b.pending_garbage > 0:
        gx = bx - 4 if b.bx < WIDTH // 2 else bx + BOARD_W * CELL + 1
        for i in range(min(b.pending_garbage, BOARD_H)):
            gy = by + BOARD_H * CELL - (i + 1) * CELL
            pyxel.rect(gx, gy, 3, CELL - 1, 8)
 
    # Name über Board
    pyxel.text(bx, 1, b.name, b.pcol)
 
 
def draw_minis(b: Board, side: str):
    """side = 'left' (P1 Minis rechts vom Board) oder 'right' (P2 Minis links vom Board)."""
    if side == "left":
        mx = b.bx + BOARD_W * CELL + 4
    else:
        mx = b.bx - 24
 
    pyxel.text(mx, b.by, "N", 6)
    pyxel.rect(mx - 1, b.by + 6, 22, 18, 1)
    draw_mini_piece(b.next_name, mx + 2, b.by + 9)
 
 
# ──────────────────────────────────────────────
#  APP
# ──────────────────────────────────────────────
class App:
    def __init__(self):
        pyxel.init(WIDTH, HEIGHT, title="🧱 Tetris VS – Pyxel Studio", fps=FPS)
 
        # Sound-Effekt fürs Landen eines Blocks (kurzer perkussiver "Plop")
        pyxel.sounds[0].set(
            notes="c2 c1",
            tones="n n",
            volumes="6 3",
            effects="n f",
            speed=7,
        )
 
        self.state  = "menu"
        self.b1 = None
        self.b2 = None
        pyxel.run(self.update, self.draw)
 
    def _new_match(self):
        self.b1 = Board(KEYMAP_P1, P1_BX, P1_BY, "P1", 8, sound_channel=0)
        self.b2 = Board(KEYMAP_P2, P2_BX, P2_BY, "P2", 11, sound_channel=1)
        self.b1.opponent = self.b2
        self.b2.opponent = self.b1
        self.winner_name = None
 
    def update(self):
        if self.state == "menu":
            if pyxel.btnp(pyxel.KEY_RETURN) or pyxel.btnp(pyxel.KEY_SPACE):
                self._new_match()
                self.state = "game"
 
        elif self.state == "game":
            self.b1.handle_input()
            self.b2.handle_input()
            self.b1.update()
            self.b2.update()
 
            if self.b1.over or self.b2.over:
                if self.b1.over and self.b2.over:
                    self.winner_name = "UNENTSCHIEDEN"
                elif self.b1.over:
                    self.winner_name = self.b2.name
                else:
                    self.winner_name = self.b1.name
                self.state = "over"
 
        elif self.state == "over":
            if pyxel.btnp(pyxel.KEY_RETURN) or pyxel.btnp(pyxel.KEY_SPACE):
                self.state = "menu"
 
    def draw(self):
        pyxel.cls(0)
        if self.state == "menu":
            self._draw_menu()
        elif self.state in ("game", "over"):
            # Mittellinie
            pyxel.line(WIDTH//2, 0, WIDTH//2, HEIGHT, 1)
            draw_board(self.b1)
            draw_board(self.b2)
            draw_minis(self.b1, "left")
            draw_minis(self.b2, "right")
            if self.state == "over":
                self._draw_over()
 
    def _draw_menu(self):
        pyxel.cls(1)
        tx = WIDTH // 2 - 36
        pyxel.text(tx, 24, "T E T R I S   V S", pyxel.COLOR_YELLOW)
        pyxel.text(WIDTH//2 - 44, 34, "Pyxel Studio - Duell-Modus", 6)
 
        cols = [8, 11, 10, 2, 14, 12, 9, 3]
        for i, c in enumerate(cols):
            x = 20 + i * 28
            pyxel.rect(x, 50, 9, 9, c)
 
        pyxel.text(20, 66, "SPIELER 1 (rot)", 8)
        pyxel.text(20, 76, "A D  Bewegen", 7)
        pyxel.text(20, 84, "W    Drehen", 7)
        pyxel.text(20, 92, "S    Schneller fallen", 7)
 
        pyxel.text(140, 66, "SPIELER 2 (blau)", 11)
        pyxel.text(140, 76, "<- ->  Bewegen", 7)
        pyxel.text(140, 84, "UP     Drehen", 7)
        pyxel.text(140, 92, "DOWN   Schneller fallen", 7)
 
        pyxel.text(WIDTH//2 - 76, 124, "Blöcke fallen automatisch und", pyxel.COLOR_YELLOW)
        pyxel.text(WIDTH//2 - 70, 134, "werden mit jedem Block schneller!", pyxel.COLOR_YELLOW)
        pyxel.text(WIDTH//2 - 76, 144, "Zeilen löschen schickt GARBAGE", pyxel.COLOR_YELLOW)
        pyxel.text(WIDTH//2 - 58, 154, "zum Gegner.", pyxel.COLOR_YELLOW)
 
        if (pyxel.frame_count // 15) % 2 == 0:
            pyxel.text(WIDTH//2 - 52, 168, "ENTER / SPACE = Starten", 13)
 
    def _draw_over(self):
        pyxel.rect(48, 64, 160, 64, 0)
        pyxel.rectb(48, 64, 160, 64, pyxel.COLOR_YELLOW)
        pyxel.text(WIDTH//2 - 24, 76, "RUNDE ENDE", pyxel.COLOR_YELLOW)
        msg = f"{self.winner_name} GEWINNT!" if self.winner_name != "UNENTSCHIEDEN" else "UNENTSCHIEDEN!"
        pyxel.text(WIDTH//2 - len(msg)*2, 92, msg, 7)
        if (pyxel.frame_count // 15) % 2 == 0:
            pyxel.text(WIDTH//2 - 44, 116, "ENTER = Neues Duell", 13)
 
 
# ──────────────────────────────────────────────
#  START
# ──────────────────────────────────────────────
App()