← Arcade ๐Ÿ† Players My player card

Retro Tetris

โ˜… Retro Tetris โ˜…
Retro Tetris gameplay screenshot
๐Ÿ“ฆ Game code, download & prompt info
๐Ÿ–ฅ๏ธ Desktop app (python) โ€” play it live above, right in your browser, or download it below
  • Model: openrouter:stealth/union-alpha
  • Test: Retro: Tetris (playable, pygame UI)
  • Category: retrogames
  • Kind: code ยท scored as score (higher is better)
  • Run: โ€”
  • Benchmark score: 98.0
  • Benchmark date: 2026-09-17
  • Published: 2026-09-25
Original prompt
Write a complete, runnable Python program using `pygame` that implements a Tetris game. Open a window, spawn falling tetromino pieces that move down each tick, LEFT/RIGHT to move and UP to rotate, clear a row when it is completely filled, and end the game when pieces stack to the top. Display the score. Include a game loop handling QUIT. Use only primitive shapes - no external assets. Design note (interpret freely): the falling-piece puzzle where seven tetromino shapes drop into a 10-wide well and must be rotated and slid to pack complete rows that clear, with gravity and speed increasing each level. Precise rotation, stacking, and line-clear satisfaction are the defining trait. SCORING & PERSISTENT HIGH-SCORE BOARD (REQUIRED): The game MUST award points for gameplay actions and maintain a persistent high-score table. When a run finishes, prompt the player to enter their name/initials and record it with the score. Save the top 5 scores (name + score) to a local JSON file in the working directory (e.g. high_scores.json), and also write the latest finished run as {"initials": name, "score": number} to /tmp/alpaca_score.json (overwrite it every run) so the arcade scoreboard can pick it up automatically, load and display them on startup and on the game-over screen, and write a new entry whenever a run finishes. Scores must survive restarting the program. Every new game MUST start with the score reset to 0 - never carry over the previous game's score into the next session. The program must run top-to-bottom and open a visible window. OUTPUT REQUIREMENT: Respond with ONLY the raw, runnable source code in a single fenced code block, with the first line starting at column 0 (no leading indentation). Do not include any thinking, explanation, summary, or prose before or after the code, and keep comments inside the code minimal.
โฌ‡ Download game.py
import json
import os
import random
from pathlib import Path

import pygame


COLS, ROWS = 10, 20
CELL = 28
BOARD_X, BOARD_Y = 24, 64
WIDTH, HEIGHT = 640, 692
PANEL_X = 330

BG = (15, 18, 29)
WELL = (22, 27, 41)
GRID = (34, 40, 57)
TEXT = (229, 234, 245)
MUTED = (149, 161, 184)
ACCENT = (103, 224, 204)

COLORS = {
    "I": (65, 204, 230),
    "J": (84, 126, 235),
    "L": (246, 163, 65),
    "O": (240, 215, 76),
    "S": (99, 210, 133),
    "T": (178, 111, 230),
    "Z": (236, 101, 115),
}

BASE_SHAPES = {
    "I": ("....", "IIII", "....", "...."),
    "J": ("J..", "JJJ", "..."),
    "L": ("..L", "LLL", "..."),
    "O": ("OO", "OO"),
    "S": (".SS", "SS.", "..."),
    "T": (".T.", "TTT", "..."),
    "Z": ("ZZ.", ".ZZ", "..."),
}


def rotate_matrix(matrix):
    return tuple("".join(row) for row in zip(*matrix[::-1]))


SHAPES = {}
for kind, base in BASE_SHAPES.items():
    rotations = [base]
    for _ in range(3):
        rotations.append(rotate_matrix(rotations[-1]))
    SHAPES[kind] = rotations

# Clockwise SRS offsets, expressed with positive y pointing upward.
JLSTZ_KICKS = {
    0: ((0, 0), (-1, 0), (-1, 1), (0, -2), (-1, -2)),
    1: ((0, 0), (1, 0), (1, -1), (0, 2), (1, 2)),
    2: ((0, 0), (1, 0), (1, 1), (0, -2), (1, -2)),
    3: ((0, 0), (-1, 0), (-1, -1), (0, 2), (-1, 2)),
}
I_KICKS = {
    0: ((0, 0), (-2, 0), (1, 0), (-2, -1), (1, 2)),
    1: ((0, 0), (-1, 0), (2, 0), (-1, 2), (2, -1)),
    2: ((0, 0), (2, 0), (-1, 0), (2, 1), (-1, -2)),
    3: ((0, 0), (1, 0), (-2, 0), (1, -2), (-2, 1)),
}


class ScoreStore:
    def __init__(self):
        self.path = Path.cwd() / "high_scores.json"
        self.entries = []
        self.error = ""
        self.load()

    def load(self):
        try:
            data = json.loads(self.path.read_text(encoding="utf-8"))
            if isinstance(data, list):
                for entry in data:
                    if not isinstance(entry, dict):
                        continue
                    name = entry.get("initials", entry.get("name", ""))
                    score = entry.get("score")
                    if (
                        isinstance(name, str)
                        and isinstance(score, int)
                        and not isinstance(score, bool)
                        and score >= 0
                    ):
                        self.entries.append({
                            "initials": name.strip()[:12] or "PLAYER",
                            "score": score,
                        })
                self.entries.sort(key=lambda item: item["score"], reverse=True)
                self.entries = self.entries[:5]
        except FileNotFoundError:
            pass
        except (OSError, ValueError, TypeError):
            self.error = "Could not load previous scores."

    @staticmethod
    def atomic_write(path, data):
        temporary = path.with_name(path.name + ".tmp")
        with temporary.open("w", encoding="utf-8") as handle:
            json.dump(data, handle, indent=2)
            handle.write("\n")
        os.replace(temporary, path)

    def record(self, name, score):
        entry = {"initials": name.strip()[:12] or "PLAYER", "score": int(score)}
        self.entries.append(entry)
        self.entries.sort(key=lambda item: item["score"], reverse=True)
        self.entries = self.entries[:5]
        failures = []
        for path, data in (
            (self.path, self.entries),
            (Path("/tmp/alpaca_score.json"), entry),
        ):
            try:
                self.atomic_write(path, data)
            except OSError:
                failures.append(path.name)
        self.error = "Save failed: " + ", ".join(failures) if failures else ""
        if self.error:
            print(self.error)


class Tetris:
    def __init__(self, scores):
        self.scores = scores
        self.reset()

    @property
    def level(self):
        return self.lines // 10 + 1

    def reset(self):
        self.board = [[None for _ in range(COLS)] for _ in range(ROWS)]
        self.score = 0
        self.lines = 0
        self.state = "playing"
        self.initials = ""
        self.saved = False
        self.queue = []
        self.bag = []
        self.clear_rows = []
        self.flash_time = 0
        self.repeat_direction = 0
        self.repeat_time = 0
        pygame.key.stop_text_input()
        self.spawn()

    def take_piece(self):
        if not self.bag:
            self.bag = list(BASE_SHAPES)
            random.shuffle(self.bag)
        return self.bag.pop()

    def spawn(self):
        while len(self.queue) < 4:
            self.queue.append(self.take_piece())
        self.kind = self.queue.pop(0)
        self.rotation = 0
        self.x = (COLS - len(SHAPES[self.kind][0])) // 2
        self.y = -1
        self.fall_time = 0
        self.lock_time = 0
        self.lock_resets = 0
        if not self.valid():
            self.finish()

    def cells(self, x=None, y=None, rotation=None):
        x = self.x if x is None else x
        y = self.y if y is None else y
        rotation = self.rotation if rotation is None else rotation
        matrix = SHAPES[self.kind][rotation]
        for row, values in enumerate(matrix):
            for col, value in enumerate(values):
                if value != ".":
                    yield x + col, y + row

    def valid(self, x=None, y=None, rotation=None):
        for col, row in self.cells(x, y, rotation):
            if col < 0 or col >= COLS or row >= ROWS:
                return False
            if row >= 0 and self.board[row][col] is not None:
                return False
        return True

    def grounded(self):
        return not self.valid(y=self.y + 1)

    def reset_lock(self, was_grounded):
        if was_grounded and self.lock_resets < 15:
            self.lock_time = 0
            self.lock_resets += 1

    def move(self, dx):
        was_grounded = self.grounded()
        if self.valid(x=self.x + dx):
            self.x += dx
            self.reset_lock(was_grounded)

    def rotate(self):
        if self.kind == "O":
            return
        was_grounded = self.grounded()
        target = (self.rotation + 1) % 4
        kicks = I_KICKS if self.kind == "I" else JLSTZ_KICKS
        for dx, dy in kicks[self.rotation]:
            if self.valid(self.x + dx, self.y - dy, target):
                self.x += dx
                self.y -= dy
                self.rotation = target
                self.reset_lock(was_grounded)
                return

    def hard_drop(self):
        distance = 0
        while self.valid(y=self.y + 1):
            self.y += 1
            distance += 1
        self.score += distance * 2
        self.lock_piece()

    def lock_piece(self):
        blocks = list(self.cells())
        if any(row < 0 for _, row in blocks):
            self.finish()
            return
        for col, row in blocks:
            self.board[row][col] = self.kind
        full = [
            row for row in range(ROWS)
            if all(value is not None for value in self.board[row])
        ]
        if full:
            self.score += (0, 100, 300, 500, 800)[len(full)] * self.level
            self.lines += len(full)
            self.clear_rows = full
            self.flash_time = 0.22
            self.state = "clearing"
        else:
            self.spawn()

    def finish(self):
        self.state = "entering"
        self.initials = ""
        pygame.key.start_text_input()

    def submit_score(self):
        if not self.saved:
            self.scores.record(self.initials, self.score)
            self.saved = True
        self.state = "gameover"
        pygame.key.stop_text_input()

    def update(self, dt):
        if self.state == "clearing":
            self.flash_time -= dt
            if self.flash_time <= 0:
                remaining = [
                    row for index, row in enumerate(self.board)
                    if index not in self.clear_rows
                ]
                self.board = [
                    [None for _ in range(COLS)] for _ in self.clear_rows
                ] + remaining
                self.clear_rows = []
                self.state = "playing"
                self.spawn()
            return
        if self.state != "playing":
            return

        keys = pygame.key.get_pressed()
        direction = int(keys[pygame.K_RIGHT]) - int(keys[pygame.K_LEFT])
        if direction != self.repeat_direction:
            self.repeat_direction = direction
            self.repeat_time = 0.16
        elif direction:
            self.repeat_time -= dt
            while self.repeat_time <= 0:
                self.move(direction)
                self.repeat_time += 0.045

        interval = max(0.065, 0.8 * (0.8 ** (self.level - 1)))
        soft_drop = keys[pygame.K_DOWN]
        if soft_drop:
            interval = min(interval, 0.035)
        self.fall_time += dt
        while self.fall_time >= interval:
            self.fall_time -= interval
            if self.valid(y=self.y + 1):
                self.y += 1
                self.lock_time = 0
                if soft_drop:
                    self.score += 1
            else:
                self.fall_time = 0
                break

        if self.grounded():
            self.lock_time += dt
            if self.lock_time >= 0.45:
                self.lock_piece()
        else:
            self.lock_time = 0

    def handle_event(self, event):
        if self.state == "entering":
            if event.type == pygame.TEXTINPUT:
                addition = "".join(
                    char for char in event.text if char.isprintable()
                )
                self.initials = (self.initials + addition)[:12]
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_BACKSPACE:
                    self.initials = self.initials[:-1]
                elif event.key == pygame.K_RETURN:
                    self.submit_score()
            return

        if event.type != pygame.KEYDOWN:
            return
        if self.state == "gameover":
            if event.key in (pygame.K_RETURN, pygame.K_r):
                self.reset()
        elif self.state == "playing":
            if event.key == pygame.K_LEFT:
                self.move(-1)
                self.repeat_direction = -1
                self.repeat_time = 0.16
            elif event.key == pygame.K_RIGHT:
                self.move(1)
                self.repeat_direction = 1
                self.repeat_time = 0.16
            elif event.key == pygame.K_UP:
                self.rotate()
            elif event.key == pygame.K_SPACE:
                self.hard_drop()


def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("Tetris - Seven Shapes")
    clock = pygame.time.Clock()
    fonts = {
        "small": pygame.font.Font(None, 21),
        "normal": pygame.font.Font(None, 27),
        "large": pygame.font.Font(None, 38),
        "title": pygame.font.Font(None, 43),
    }
    scores = ScoreStore()
    game = Tetris(scores)

    def text(value, x, y, size="normal", color=TEXT, center=False):
        surface = fonts[size].render(str(value), True, color)
        rectangle = surface.get_rect()
        if center:
            rectangle.midtop = (x, y)
        else:
            rectangle.topleft = (x, y)
        screen.blit(surface, rectangle)

    def block(col, row, kind, ghost=False):
        if row < 0:
            return
        rectangle = pygame.Rect(
            BOARD_X + col * CELL + 1,
            BOARD_Y + row * CELL + 1,
            CELL - 2,
            CELL - 2,
        )
        color = COLORS[kind]
        if ghost:
            pygame.draw.rect(screen, color, rectangle, 2, border_radius=3)
        else:
            pygame.draw.rect(screen, color, rectangle, border_radius=3)
            highlight = tuple(min(255, component + 35) for component in color)
            pygame.draw.line(
                screen, highlight,
                (rectangle.left + 3, rectangle.top + 3),
                (rectangle.right - 4, rectangle.top + 3), 2
            )

    def draw():
        screen.fill(BG)
        text("TETRIS", BOARD_X, 16, "title", ACCENT)
        text("SEVEN SHAPES", PANEL_X, 27, "small", MUTED)

        well_rect = pygame.Rect(BOARD_X, BOARD_Y, COLS * CELL, ROWS * CELL)
        pygame.draw.rect(screen, WELL, well_rect)
        for col in range(COLS + 1):
            x = BOARD_X + col * CELL
            pygame.draw.line(screen, GRID, (x, BOARD_Y), (x, well_rect.bottom))
        for row in range(ROWS + 1):
            y = BOARD_Y + row * CELL
            pygame.draw.line(screen, GRID, (BOARD_X, y), (well_rect.right, y))

        for row in range(ROWS):
            for col in range(COLS):
                kind = game.board[row][col]
                if kind is not None:
                    block(col, row, kind)

        if game.state == "playing":
            ghost_y = game.y
            while game.valid(y=ghost_y + 1):
                ghost_y += 1
            for col, row in game.cells(y=ghost_y):
                block(col, row, game.kind, True)
            for col, row in game.cells():
                block(col, row, game.kind)

        if game.state == "clearing":
            alpha = int(220 * max(0, game.flash_time) / 0.22)
            flash = pygame.Surface((COLS * CELL, CELL), pygame.SRCALPHA)
            flash.fill((255, 255, 255, alpha))
            for row in game.clear_rows:
                screen.blit(flash, (BOARD_X, BOARD_Y + row * CELL))

        pygame.draw.rect(screen, MUTED, well_rect, 2)
        text("SCORE", PANEL_X, 66, "small", MUTED)
        text(f"{game.score:,}", PANEL_X, 88, "large")
        text(f"LEVEL  {game.level}", PANEL_X, 136)
        text(f"LINES  {game.lines}", PANEL_X, 166)

        text("NEXT", PANEL_X, 212, "small", MUTED)
        for index, kind in enumerate(game.queue[:3]):
            size = 17
            base_x = PANEL_X + index * 91
            base_y = 243
            occupied = [
                (col, row)
                for row, values in enumerate(SHAPES[kind][0])
                for col, value in enumerate(values) if value != "."
            ]
            min_col = min(col for col, _ in occupied)
            min_row = min(row for _, row in occupied)
            for col, row in occupied:
                rectangle = pygame.Rect(
                    base_x + (col - min_col) * size,
                    base_y + (row - min_row) * size,
                    size - 2, size - 2,
                )
                pygame.draw.rect(
                    screen, COLORS[kind], rectangle, border_radius=2
                )

        text("HIGH SCORES - TOP 5", PANEL_X, 310, "normal", ACCENT)
        for index in range(5):
            y = 349 + index * 34
            if index < len(scores.entries):
                entry = scores.entries[index]
                name = entry["initials"]
                value = f'{entry["score"]:,}'
            else:
                name, value = "---", "-"
            text(f"{index + 1}. {name}", PANEL_X, y, "small")
            surface = fonts["small"].render(value, True, TEXT)
            screen.blit(surface, (WIDTH - 22 - surface.get_width(), y))

        text("LEFT / RIGHT   Move", PANEL_X, 537, "small", MUTED)
        text("UP   Rotate clockwise", PANEL_X, 561, "small", MUTED)
        text("DOWN   Soft drop (+1)", PANEL_X, 585, "small", MUTED)
        text("SPACE   Hard drop (+2)", PANEL_X, 609, "small", MUTED)
        text("Rows: 100 / 300 / 500 / 800 x level",
             BOARD_X, 645, "small", MUTED)
        if scores.error:
            text("Score file error - see console.",
                 BOARD_X, 669, "small", (250, 130, 130))

        if game.state in ("entering", "gameover"):
            veil = pygame.Surface(well_rect.size, pygame.SRCALPHA)
            veil.fill((8, 11, 20, 218))
            screen.blit(veil, well_rect.topleft)
            center = BOARD_X + COLS * CELL // 2
            text("GAME OVER", center, 221, "large", ACCENT, True)
            text(f"Score: {game.score:,}", center, 268, center=True)
            if game.state == "entering":
                text("Enter name / initials:", center, 322,
                     "normal", TEXT, True)
                field = pygame.Rect(BOARD_X + 20, 358, COLS * CELL - 40, 42)
                pygame.draw.rect(screen, WELL, field, border_radius=5)
                pygame.draw.rect(screen, ACCENT, field, 1, border_radius=5)
                cursor = "|" if pygame.time.get_ticks() % 1000 < 500 else ""
                text(game.initials + cursor, center, 369,
                     "normal", TEXT, True)
                text("ENTER to save your score", center, 426,
                     "small", MUTED, True)
                text("Blank name becomes PLAYER", center, 451,
                     "small", MUTED, True)
            else:
                message = "Score saved!" if not scores.error else "Save error"
                text(message, center, 330, "normal", TEXT, True)
                text("ENTER or R to play again", center, 381,
                     "small", MUTED, True)
                text("New game starts at zero", center, 408,
                     "small", MUTED, True)

        pygame.display.flip()

    running = True
    try:
        while running:
            dt = min(clock.tick(60) / 1000.0, 0.05)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    if not game.saved:
                        game.submit_score()
                    running = False
                    break
                game.handle_event(event)
            if running:
                game.update(dt)
                draw()
    finally:
        pygame.quit()


if __name__ == "__main__":
    main()

๐ŸŽฎ Press โ–ถ Play above to run it live in your browser, or download it below and run locally (pip install pygame), then enter your score by hand