Skip to content

Instantly share code, notes, and snippets.

@tarruda
Created August 4, 2026 10:37
Show Gist options
  • Select an option

  • Save tarruda/20421dbfa612ebf71ed20361320b1281 to your computer and use it in GitHub Desktop.

Select an option

Save tarruda/20421dbfa612ebf71ed20361320b1281 to your computer and use it in GitHub Desktop.
DeepSeek V4 0731 Terminal tetris
#!/usr/bin/env python3
"""
TETRIS — a beautiful, fully-featured Tetris clone for the terminal.
Controls
────────
← / A, D move left / right
↓ / S soft drop
SPACE hard drop
Z rotate counter-clockwise
X / ↑ / W rotate clockwise
C / H hold piece
P pause
R restart
Q / ESC quit
Features
────────
• SRS rotation with wall kicks
• 7-bag randomizer, 4-piece next queue, hold piece
• ghost piece, lock delay (with move resets)
• T-spin detection (single / double / triple / no-clear)
• combos, back-to-back bonuses, perfect-clear bonus
• guideline-style scoring, levels and speed curve
• persistent high score (~/.tetris_highscore)
• 256-color shaded blocks (8-color fallback), Unicode borders
"""
from __future__ import annotations
import argparse
import curses
import os
import random
import time
VERSION = "1.0.0"
COLS = 10
VISIBLE = 20
HIDDEN = 2
ROWS = VISIBLE + HIDDEN
KINDS = ("I", "O", "T", "S", "Z", "J", "L")
# ---------------------------------------------------------------------------
# Tetromino definitions (Super Rotation System)
# Each state is the set of (x, y) cell offsets inside a 4x4 / 3x3 box.
# ---------------------------------------------------------------------------
SHAPES = {
"I": {
0: [(0, 1), (1, 1), (2, 1), (3, 1)],
1: [(2, 0), (2, 1), (2, 2), (2, 3)],
2: [(0, 2), (1, 2), (2, 2), (3, 2)],
3: [(1, 0), (1, 1), (1, 2), (1, 3)],
},
"O": {r: [(1, 0), (2, 0), (1, 1), (2, 1)] for r in range(4)},
"T": {
0: [(1, 0), (0, 1), (1, 1), (2, 1)],
1: [(1, 0), (1, 1), (2, 1), (1, 2)],
2: [(0, 1), (1, 1), (2, 1), (1, 2)],
3: [(1, 0), (0, 1), (1, 1), (1, 2)],
},
"S": {
0: [(1, 0), (2, 0), (0, 1), (1, 1)],
1: [(1, 0), (1, 1), (2, 1), (2, 2)],
2: [(1, 1), (2, 1), (0, 2), (1, 2)],
3: [(0, 0), (0, 1), (1, 1), (1, 2)],
},
"Z": {
0: [(0, 0), (1, 0), (1, 1), (2, 1)],
1: [(2, 0), (1, 1), (2, 1), (1, 2)],
2: [(0, 1), (1, 1), (1, 2), (2, 2)],
3: [(1, 0), (0, 1), (1, 1), (0, 2)],
},
"J": {
0: [(0, 0), (0, 1), (1, 1), (2, 1)],
1: [(1, 0), (2, 0), (1, 1), (1, 2)],
2: [(0, 1), (1, 1), (2, 1), (2, 2)],
3: [(1, 0), (1, 1), (0, 2), (1, 2)],
},
"L": {
0: [(2, 0), (0, 1), (1, 1), (2, 1)],
1: [(1, 0), (1, 1), (1, 2), (2, 2)],
2: [(0, 1), (1, 1), (2, 1), (0, 2)],
3: [(0, 0), (1, 0), (1, 1), (1, 2)],
},
}
# SRS wall-kick tables, keyed by (from_rotation, to_rotation).
# Rotations: 0 = spawn, 1 = 90° CW, 2 = 180°, 3 = 90° CCW.
KICKS_JLSTZ = {
(0, 1): [(0, 0), (-1, 0), (-1, 1), (0, -2), (-1, -2)],
(1, 0): [(0, 0), (1, 0), (1, -1), (0, 2), (1, 2)],
(1, 2): [(0, 0), (1, 0), (1, -1), (0, 2), (1, 2)],
(2, 1): [(0, 0), (-1, 0), (-1, 1), (0, -2), (-1, -2)],
(2, 3): [(0, 0), (1, 0), (1, 1), (0, -2), (1, -2)],
(3, 2): [(0, 0), (-1, 0), (-1, -1), (0, 2), (-1, 2)],
(3, 0): [(0, 0), (-1, 0), (-1, -1), (0, 2), (-1, 2)],
(0, 3): [(0, 0), (1, 0), (1, 1), (0, -2), (1, -2)],
}
KICKS_I = {
(0, 1): [(0, 0), (-2, 0), (1, 0), (-2, -1), (1, 2)],
(1, 0): [(0, 0), (2, 0), (-1, 0), (2, 1), (-1, -2)],
(1, 2): [(0, 0), (-1, 0), (2, 0), (-1, 2), (2, -1)],
(2, 1): [(0, 0), (1, 0), (-2, 0), (1, -2), (-2, 1)],
(2, 3): [(0, 0), (2, 0), (-1, 0), (2, 1), (-1, -2)],
(3, 2): [(0, 0), (-2, 0), (1, 0), (-2, -1), (1, 2)],
(3, 0): [(0, 0), (1, 0), (-2, 0), (1, -2), (-2, 1)],
(0, 3): [(0, 0), (-1, 0), (2, 0), (-1, 2), (2, -1)],
}
KICKS_O = {(f, t): [(0, 0)] for f in range(4) for t in range(4)}
def kicks_for(kind: str, frm: int, to: int):
if kind == "I":
table = KICKS_I
elif kind == "O":
table = KICKS_O
else:
table = KICKS_JLSTZ
return table.get((frm, to), [(0, 0)])
# (bright, base, dark) color indices for 256-color terminals
PALETTE_256 = {
"I": (51, 39, 24),
"O": (228, 220, 172),
"T": (213, 135, 60),
"S": (114, 83, 29),
"Z": (203, 196, 52),
"J": (81, 75, 25),
"L": (215, 208, 130),
}
# 8-color fallback
PALETTE_8 = {
"I": (6, 6, 0),
"O": (3, 3, 0),
"T": (5, 5, 0),
"S": (2, 2, 0),
"Z": (1, 1, 0),
"J": (4, 4, 0),
"L": (3, 3, 0),
}
# ---------------------------------------------------------------------------
# Game logic (pure Python, no curses — easily testable)
# ---------------------------------------------------------------------------
class Tetris:
def __init__(self, seed=None, highscore_file=None):
self.rng = random.Random(seed)
self.highscore_file = highscore_file or self.default_highscore_file()
self.highscore = self._load_highscore()
self.reset()
@staticmethod
def default_highscore_file():
try:
home = os.path.expanduser("~")
return os.path.join(home, ".tetris_highscore")
except Exception:
return os.path.join(
os.path.dirname(os.path.abspath(__file__)), ".tetris_highscore"
)
def _load_highscore(self):
try:
with open(self.highscore_file) as f:
return max(0, int(f.read().strip()))
except Exception:
return 0
def save_highscore(self):
if self.score <= self.highscore:
return
self.highscore = self.score
for path in {self.highscore_file,
os.path.join(os.path.dirname(os.path.abspath(__file__)),
".tetris_highscore")}:
try:
with open(path, "w") as f:
f.write(str(self.highscore))
break
except Exception:
continue
# ---- setup -----------------------------------------------------------
def reset(self):
self.grid = [[0] * COLS for _ in range(ROWS)] # 0 empty, 1..7 piece
self.bag = []
self.queue = []
self.held = None
self.can_hold = True
self.active = None
self.ghost_y = 0
self.score = 0
self.lines = 0
self.level = 1
self.combo = 0
self.last_bonus = False
self.pieces = 0
self.tetrises = 0
self.tspins = 0
self.perfects = 0
self.state = "start" # start | play | pause | over
self.landed = False
self.lock_timer = 0.0
self.lock_resets = 0
self.last_action = None
self.fall_timer = 0.0
self.anim_rows = []
self.anim_t = 0.0
self._pending_clear = None
self.quit = False
self._refill_queue()
self.spawn()
def restart(self):
self.reset()
self.state = "play"
def _refill_queue(self):
while len(self.queue) < 5:
if not self.bag:
self.bag = list(KINDS)
self.rng.shuffle(self.bag)
self.queue.append(self.bag.pop(0))
def spawn(self):
self._spawn_kind(self.queue.pop(0))
self._refill_queue()
def _spawn_kind(self, kind):
self.active = {"kind": kind, "rot": 0, "x": 3, "y": 0,
"cells": SHAPES[kind][0]}
self.landed = False
self.lock_timer = 0.0
self.lock_resets = 0
self.last_action = None
self.can_hold = True
self.ghost_y = self.drop_y()
if not self.fits(self.active["cells"], 3, 0):
self.state = "over"
self.save_highscore()
# ---- geometry --------------------------------------------------------
def fits(self, cells, x, y):
for cx, cy in cells:
gx, gy = x + cx, y + cy
if gx < 0 or gx >= COLS or gy >= ROWS:
return False
if gy >= 0 and self.grid[gy][gx]:
return False
return True
def can_drop(self):
cur = self.active
return self.fits(cur["cells"], cur["x"], cur["y"] + 1)
def drop_y(self):
cur = self.active
y = cur["y"]
while self.fits(cur["cells"], cur["x"], y + 1):
y += 1
return y
def update_ghost(self):
self.ghost_y = self.drop_y()
# ---- actions ---------------------------------------------------------
def move(self, dx):
if self.state != "play" or not self.active or self.anim_rows:
return
cur = self.active
if self.fits(cur["cells"], cur["x"] + dx, cur["y"]):
cur["x"] += dx
self.last_action = "move"
was_landed = self.landed
if self.can_drop():
self.landed = False
self.lock_timer = 0.0
if was_landed:
self.lock_resets += 1
if self.lock_resets >= 15:
self.lock_now()
return
self.update_ghost()
def rotate(self, direction):
if self.state != "play" or not self.active or self.anim_rows:
return False
cur = self.active
new_rot = (cur["rot"] + direction) % 4
cells = SHAPES[cur["kind"]][new_rot]
was_landed = self.landed
for dx, dy in kicks_for(cur["kind"], cur["rot"], new_rot):
if self.fits(cells, cur["x"] + dx, cur["y"] + dy):
cur["rot"] = new_rot
cur["cells"] = cells
cur["x"] += dx
cur["y"] += dy
self.last_action = "rotate"
if self.can_drop():
self.landed = False
self.lock_timer = 0.0
if was_landed:
self.lock_resets += 1
if self.lock_resets >= 15:
self.lock_now()
return True
self.update_ghost()
return True
return False
def soft_drop(self):
if self.state != "play" or not self.active or self.anim_rows:
return
cur = self.active
if self.fits(cur["cells"], cur["x"], cur["y"] + 1):
cur["y"] += 1
self.score += 1
self.fall_timer = 0.0
self.update_ghost()
else:
self.lock_now()
def hard_drop(self):
if self.state != "play" or not self.active or self.anim_rows:
return
cur = self.active
target = self.drop_y()
self.score += 2 * (target - cur["y"])
cur["y"] = target
self.update_ghost()
self.lock_now()
def hold(self):
if (self.state != "play" or not self.active or self.anim_rows
or not self.can_hold):
return
kind = self.active["kind"]
if self.held is None:
self.held = kind
self.spawn()
else:
tmp = self.held
self.held = kind
self._spawn_kind(tmp)
self.can_hold = False
# ---- locking & clears -------------------------------------------------
def lock_now(self):
if not self.active:
return
cur = self.active
kind = cur["kind"]
for cx, cy in cur["cells"]:
gx, gy = cur["x"] + cx, cur["y"] + cy
if 0 <= gx < COLS and 0 <= gy < ROWS:
self.grid[gy][gx] = KINDS.index(kind) + 1
self.pieces += 1
tspin = self.detect_tspin(cur)
if tspin:
self.tspins += 1
self.active = None
self.landed = False
self.resolve_clear(tspin)
self.spawn()
def detect_tspin(self, cur):
if cur["kind"] != "T" or self.last_action != "rotate":
return False
cx, cy = cur["x"] + 1, cur["y"] + 1 # T pivot at local (1, 1)
corners = [(cx - 1, cy - 1), (cx + 1, cy - 1),
(cx - 1, cy + 1), (cx + 1, cy + 1)]
filled = 0
for gx, gy in corners:
if gx < 0 or gx >= COLS or gy >= ROWS or \
(gy >= 0 and self.grid[gy][gx]):
filled += 1
return filled >= 3
def resolve_clear(self, tspin):
full = [r for r in range(ROWS) if all(self.grid[r])]
if full:
self.anim_rows = full
self.anim_t = 0.0
self._pending_clear = tspin
else:
self.combo = 0
self.last_bonus = False
def finish_clear(self):
if not self.anim_rows:
return
n = len(self.anim_rows)
tspin = self._pending_clear
for r in self.anim_rows:
del self.grid[r]
self.grid.insert(0, [0] * COLS)
self.anim_rows = []
self._pending_clear = None
self.lines += n
self.level = self.lines // 10 + 1
self.combo = self.combo + 1 if n else 0
if tspin:
pts = 400 if n == 0 else (800, 1200, 1600)[min(n, 3) - 1]
elif n == 4:
pts = 800
self.tetrises += 1
else:
pts = (0, 100, 300, 500)[n] if n <= 3 else 800
pts *= self.level
is_bonus = n > 0 and (tspin or n == 4)
if is_bonus and self.last_bonus:
pts = pts * 3 // 2 # back-to-back
self.last_bonus = is_bonus
if n:
self.score += pts + 50 * self.combo * self.level
if n and not any(any(row) for row in self.grid):
self.score += 1000 * self.level # perfect clear
self.perfects += 1
# ---- timing ----------------------------------------------------------
def fall_interval(self):
return max(0.03, 0.8 * (0.85 ** (self.level - 1)))
def lock_delay(self):
return max(0.12, 0.5 * (0.9 ** (self.level - 1)))
def update(self, dt):
if self.state != "play":
return
if self.anim_rows:
self.anim_t += dt
if self.anim_t >= 0.12:
self.finish_clear()
return
if not self.active:
return
if self.landed:
self.lock_timer += dt
if self.lock_timer >= self.lock_delay():
self.lock_now()
return
self.fall_timer += dt
interval = self.fall_interval()
if self.fall_timer >= interval:
self.fall_timer = 0.0
if self.can_drop():
self.active["y"] += 1
self.update_ghost()
else:
self.landed = True
self.lock_timer = 0.0
# ---------------------------------------------------------------------------
# Rendering
# ---------------------------------------------------------------------------
HOLD_W = 16
NEXT_W = 20
GAP = 2
BOARD_W = 22 # 20 cell chars + 2 borders
TOTAL_W = HOLD_W + GAP + BOARD_W + GAP + NEXT_W # 62
TOTAL_H = 26
class Renderer:
def __init__(self, stdscr, ascii_chars=False):
self.stdscr = stdscr
self.ascii = ascii_chars
self._cache = {}
self.colors256 = False
self._init_colors()
if ascii_chars:
self.fill = "#"
self.ghost_ch = "+"
else:
self.fill = "▀"
self.ghost_ch = "▒"
# ---- colors ----------------------------------------------------------
def _pair(self, fg, bg):
key = (fg, bg)
if key in self._cache:
return self._cache[key]
n = len(self._cache) + 1
try:
curses.init_pair(n, fg, bg)
except curses.error:
return 0
self._cache[key] = n
return n
def _init_colors(self):
try:
curses.start_color()
curses.use_default_colors()
self.colors256 = curses.COLORS >= 256 and curses.COLOR_PAIRS >= 40
except curses.error:
self.colors256 = False
if self.colors256:
bg, panel = 235, 234
self.c_bg, self.c_panel = bg, panel
self.p_bg = self._pair(bg, bg)
self.p_panel = self._pair(panel, panel)
self.p_frame = self._pair(244, -1)
self.p_text = self._pair(231, -1)
self.p_dim = self._pair(242, -1)
self.p_flash = self._pair(231, 250)
self.piece = {}
for k in KINDS:
bright, base, dark = PALETTE_256[k]
self.piece[k] = {
"fill": self._pair(bright, dark),
"ghost": self._pair(base, -1),
"title": self._pair(bright, -1),
}
else:
self.c_bg = self.c_panel = 0
self.p_bg = self._pair(0, 0)
self.p_panel = self._pair(0, 0)
self.p_frame = self._pair(7, -1)
self.p_text = self._pair(7, -1)
self.p_dim = self._pair(7, -1)
self.p_flash = self._pair(7, 7)
self.piece = {}
for k in KINDS:
color = PALETTE_8[k][0]
self.piece[k] = {
"fill": self._pair(color, 0),
"ghost": self._pair(color, 0),
"title": self._pair(color, -1),
}
# ---- primitives ------------------------------------------------------
def put(self, row, col, text, pair=0, attr=0):
h, w = self.stdscr.getmaxyx()
if row < 0 or row >= h or col < 0 or col >= w:
return
if col + len(text) > w:
text = text[:max(0, w - col)]
if not text:
return
attrs = attr | (curses.color_pair(pair) if pair else 0)
try:
self.stdscr.addstr(row, col, text, attrs)
except curses.error:
pass
def box(self, top, left, width, height, double=False):
if self.ascii:
hch, vch, tl, tr, bl, br = "-", "|", "+", "+", "+", "+"
else:
if double:
hch, vch, tl, tr, bl, br = "═", "║", "╔", "╗", "╚", "╝"
else:
hch, vch, tl, tr, bl, br = "─", "│", "┌", "┐", "└", "┘"
self.put(top, left, tl + hch * (width - 2) + tr, self.p_frame)
self.put(top + height - 1, left, bl + hch * (width - 2) + br,
self.p_frame)
for r in range(top + 1, top + height - 1):
self.put(r, left, vch, self.p_frame)
self.put(r, left + width - 1, vch, self.p_frame)
def draw_mini(self, top, left, kind, pair, attr=0):
"""Draw a small preview piece. `left` is the inner-area char column."""
cells = SHAPES[kind][0]
xs = [x for x, _ in cells]
ys = [y for _, y in cells]
minx, miny = min(xs), min(ys)
w = max(xs) - minx + 1
h = max(ys) - miny + 1
start_c = left + ((5 - w) // 2) * 2
start_r = top + (2 - h) // 2
for x, y in cells:
r = start_r + y - miny
c = start_c + (x - minx) * 2
self.put(r, c, self.fill * 2, pair, attr)
# ---- whole frame -----------------------------------------------------
def draw(self, game):
scr = self.stdscr
scr.erase()
h, w = scr.getmaxyx()
if h < TOTAL_H or w < TOTAL_W:
msg = "Terminal too small — resize to at least 80x26"
self.put(h // 2, max(0, (w - len(msg)) // 2), msg, self.p_text,
curses.A_BOLD)
return
x0 = max(0, (w - TOTAL_W) // 2)
self._draw_background(x0)
self._draw_frame(x0)
self._draw_title(x0)
self._draw_hold(game, x0)
self._draw_next(game, x0)
self._draw_board(game, x0)
self._draw_controls(x0)
if game.state == "start":
best = max(game.highscore, game.score)
self.draw_overlay(
"TETRIS",
[f"BEST {best:,}", "PRESS ANY KEY TO START"],
title_pair=self.piece["T"]["title"],
)
elif game.state == "pause":
self.draw_overlay("PAUSED", ["P to resume · Q to quit"])
elif game.state == "over":
best = max(game.highscore, game.score)
self.draw_overlay(
"GAME OVER",
[f"SCORE {game.score:,}", f"BEST {best:,}"],
note="R restart · Q quit",
)
def _draw_background(self, x0):
row = " " * TOTAL_W
for r in range(1, TOTAL_H - 1):
self.put(r, x0, row, self.p_panel)
def _draw_frame(self, x0):
w = TOTAL_W + 2
if self.ascii:
self.put(0, x0 - 1, "+" + "-" * (w - 2) + "+", self.p_frame)
self.put(TOTAL_H - 1, x0 - 1, "+" + "-" * (w - 2) + "+",
self.p_frame)
v = "|"
else:
self.put(0, x0 - 1, "╔" + "═" * (w - 2) + "╗", self.p_frame)
self.put(TOTAL_H - 1, x0 - 1, "╚" + "═" * (w - 2) + "╝",
self.p_frame)
v = "║"
for r in range(1, TOTAL_H - 1):
self.put(r, x0 - 1, v, self.p_frame)
self.put(r, x0 + TOTAL_W, v, self.p_frame)
def _draw_title(self, x0):
_, w = self.stdscr.getmaxyx()
s = "◆ T E T R I S ◆"
colors = [self.piece[k]["title"] for k in ("I", "O", "T", "S", "Z", "J")]
col = (w - len(s)) // 2
ci = 0
for ch in s:
if ch in "TETRIS":
pair = colors[ci % 6]
ci += 1
elif ch == "◆":
pair = self.p_frame
else:
pair = self.p_dim
self.put(1, col, ch, pair, curses.A_BOLD)
col += 1
def _draw_hold(self, game, x0):
self.put(2, x0 + 2, "HOLD", self.p_dim)
self.box(3, x0 + 2, 12, 4)
if game.held:
attr = curses.A_DIM if not game.can_hold else 0
self.draw_mini(4, x0 + 3, game.held,
self.piece[game.held]["fill"], attr)
best = max(game.highscore, game.score)
self.put(8, x0 + 2, "BEST", self.p_dim)
self.put(9, x0 + 2, f"{best:>10,}", self.p_text, curses.A_BOLD)
self.put(11, x0 + 2, "PIECES", self.p_dim)
self.put(12, x0 + 2, f"{game.pieces:>10}", self.p_text)
self.put(14, x0 + 2, "TETRISES", self.p_dim)
self.put(15, x0 + 2, f"{game.tetrises:>10}", self.p_text)
self.put(17, x0 + 2, "T-SPINS", self.p_dim)
self.put(18, x0 + 2, f"{game.tspins:>10}", self.p_text)
self.put(20, x0 + 2, "PERFECTS", self.p_dim)
self.put(21, x0 + 2, f"{game.perfects:>10}", self.p_text)
def _draw_next(self, game, x0):
nleft = x0 + HOLD_W + GAP + BOARD_W + GAP
self.put(2, nleft + 2, "NEXT", self.p_dim)
for i in range(4):
top = 3 + i * 4
self.box(top, nleft + 2, 12, 4)
if i < len(game.queue):
kind = game.queue[i]
self.draw_mini(top + 1, nleft + 3, kind,
self.piece[kind]["fill"])
self.put(19, nleft + 2, f"SCORE {game.score:>9,}", self.p_text,
curses.A_BOLD)
self.put(20, nleft + 2, f"LEVEL {game.level:>9}", self.p_text)
self.put(21, nleft + 2, f"LINES {game.lines:>9}", self.p_text)
if game.combo > 1:
combo = f"COMBO x{game.combo}" + (" B2B" if game.last_bonus else "")
else:
combo = "COMBO -" + (" B2B" if game.last_bonus else "")
self.put(22, nleft + 2, combo, self.p_text)
def _draw_board(self, game, x0):
left = x0 + HOLD_W + GAP
inner = left + 1
self.put(2, left, "┌" + "─" * 20 + "┐", self.p_frame)
self.put(23, left, "└" + "─" * 20 + "┘", self.p_frame)
for r in range(3, 23):
self.put(r, left, "│", self.p_frame)
self.put(r, left + 21, "│", self.p_frame)
anim = set(game.anim_rows)
for r in range(VISIBLE):
row = r + HIDDEN
for c in range(COLS):
col = inner + c * 2
if row in anim:
self.put(3 + r, col, self.fill * 2, self.p_flash)
elif game.grid[row][c]:
kind = KINDS[game.grid[row][c] - 1]
self.put(3 + r, col, self.fill * 2,
self.piece[kind]["fill"])
else:
self.put(3 + r, col, " ", self.p_bg)
if game.active and not anim:
kind = game.active["kind"]
# ghost piece
for cx, cy in game.active["cells"]:
gx, gy = game.active["x"] + cx, game.ghost_y + cy
if 0 <= gx < COLS and HIDDEN <= gy < ROWS:
self.put(3 + gy - HIDDEN, inner + gx * 2,
self.ghost_ch * 2, self.piece[kind]["ghost"])
# active piece
for cx, cy in game.active["cells"]:
gx, gy = game.active["x"] + cx, game.active["y"] + cy
if 0 <= gx < COLS and HIDDEN <= gy < ROWS:
self.put(3 + gy - HIDDEN, inner + gx * 2,
self.fill * 2, self.piece[kind]["fill"])
def _draw_controls(self, x0):
_, w = self.stdscr.getmaxyx()
s = ("←→ move ↓ soft SPACE hard Z/X rotate "
"C hold P pause R restart Q quit")
self.put(24, max(0, (w - len(s)) // 2), s, self.p_dim)
def draw_overlay(self, title, lines, note=None, title_pair=None):
h, w = self.stdscr.getmaxyx()
box_w = 36
box_h = 3 + len(lines) + (1 if note else 0)
top = max(1, (TOTAL_H - box_h) // 2 - 2)
left = max(0, (w - box_w) // 2)
self.box(top, left, box_w, box_h, double=True)
self.put(top + 1, left + (box_w - len(title)) // 2, title,
title_pair or self.p_text, curses.A_BOLD)
for i, ln in enumerate(lines):
self.put(top + 2 + i, left + (box_w - len(ln)) // 2, ln,
self.p_dim)
if note:
self.put(top + box_h - 2, left + (box_w - len(note)) // 2, note,
self.p_dim)
# ---------------------------------------------------------------------------
# Input & main loop
# ---------------------------------------------------------------------------
def handle_key(game, key):
if key in (ord("q"), ord("Q"), 27): # ESC
game.quit = True
return
if key in (-1, 0) or key == curses.KEY_RESIZE:
return
if game.state == "start":
game.state = "play"
game.fall_timer = 0.0
return
if game.state == "pause":
if key in (ord("p"), ord("P")):
game.state = "play"
return
if game.state == "over":
if key in (ord("r"), ord("R")):
game.restart()
return
# play state
if key in (ord("p"), ord("P")):
game.state = "pause"
elif key in (ord("r"), ord("R")):
game.restart()
elif key in (curses.KEY_LEFT, ord("a"), ord("A")):
game.move(-1)
elif key in (curses.KEY_RIGHT, ord("d"), ord("D")):
game.move(1)
elif key in (curses.KEY_DOWN, ord("s"), ord("S")):
game.soft_drop()
elif key in (curses.KEY_UP, ord("w"), ord("W"), ord("x"), ord("X")):
game.rotate(1)
elif key in (ord("z"), ord("Z")):
game.rotate(-1)
elif key == ord(" "):
game.hard_drop()
elif key in (ord("c"), ord("C"), ord("h"), ord("H")):
game.hold()
def parse_args(argv=None):
p = argparse.ArgumentParser(
prog="tetris",
description="A beautiful, fully-featured Tetris clone for the terminal.",
)
p.add_argument("--seed", type=int, default=None,
help="random seed (for reproducible games)")
p.add_argument("--highscore", default=None,
help="path to high-score file")
p.add_argument("--ascii", action="store_true",
help="render with ASCII characters only")
p.add_argument("--frames", type=int, default=0,
help="quit automatically after N frames (testing)")
p.add_argument("--version", action="version", version=f"tetris {VERSION}")
return p.parse_args(argv)
def main(stdscr):
args = parse_args()
renderer = Renderer(stdscr, ascii_chars=args.ascii)
game = Tetris(seed=args.seed, highscore_file=args.highscore)
stdscr.nodelay(True)
stdscr.keypad(True)
try:
curses.curs_set(0)
except curses.error:
pass
last = time.monotonic()
frame = 0
while True:
now = time.monotonic()
dt = min(now - last, 0.05)
last = now
key = stdscr.getch()
handle_key(game, key)
if game.quit:
break
game.update(dt)
renderer.draw(game)
stdscr.refresh()
frame += 1
if args.frames and frame >= args.frames:
break
time.sleep(0.004)
if __name__ == "__main__":
try:
curses.wrapper(main)
except KeyboardInterrupt:
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment