โ
Retro Tetris โ
๐ฆ Game code, download & prompt info
๐ฅ๏ธ Desktop app (python) โ play it live above, right in your browser, or download it below
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.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