import random import sys import time import termios import tty from typing import List, Tuple, Optional # Board dimensions WIDTH, HEIGHT = 10, 20 # Tetromino shapes SHAPES = [ [[1, 1, 1, 1]], # I [[1, 1], [1, 1]], # O [[0, 1, 0], [1, 1, 1]], # T [[0, 1, 1], [1, 1, 0]], # S [[1, 1, 0], [0, 1, 1]], # Z [[1, 0, 0], [1, 1, 1]], # J [[0, 0, 1], [1, 1, 1]], # L ] # Colors (ANSI) COLORS = ["\033[31m", "\033[32m", "\033[33m", "\033[34m", "\033[35m", "\033[36m", "\033[37m"] RESET = "\033[0m" class Tetromino: def __init__(self, shape: List[List[int]], color: str): self.shape = shape self.color = color self.x = WIDTH // 2 - len(shape[0]) // 2 self.y = 0 def rotated(self) -> List[List[int]]: """Return the shape rotated 90° clockwise.""" return [list(row) for row in zip(*self.shape[::-1])] class Tetris: def __init__(self): self.board: List[List[Optional[str]]] = [[None] * WIDTH for _ in range(HEIGHT)] self.current: Tetromino = self.new_piece() self.next: Tetromino = self.new_piece() self.game_over = False self.score = 0 self.level = 1 self.drop_time = 0.5 # seconds between drops def new_piece(self) -> Tetromino: shape = random.choice(SHAPES) color = random.choice(COLORS) return Tetromino(shape, color) def valid(self, shape: List[List[int]], x: int, y: int) -> bool: for dy, row in enumerate(shape): for dx, cell in enumerate(row): if cell: nx, ny = x + dx, y + dy if nx < 0 or nx >= WIDTH or ny >= HEIGHT or (ny >= 0 and self.board[ny][nx]): return False return True def lock_piece(self): shape = self.current.shape x, y = self.current.x, self.current.y for dy, row in enumerate(shape): for dx, cell in enumerate(row): if cell and y + dy >= 0: self.board[y + dy][x + dx] = self.current.color self.clear_lines() self.current = self.next self.next = self.new_piece() if not self.valid(self.current.shape, self.current.x, self.current.y): self.game_over = True def clear_lines(self): new_board = [row for row in self.board if any(cell is None for cell in row)] lines_cleared = HEIGHT - len(new_board) if lines_cleared: self.score += (lines_cleared ** 2) * 100 * self.level for _ in range(lines_cleared): new_board.insert(0, [None] * WIDTH) self.board = new_board self.level = self.score // 1000 + 1 self.drop_time = max(0.05, 0.5 - (self.level - 1) * 0.05) def move(self, dx: int, dy: int) -> bool: new_x = self.current.x + dx new_y = self.current.y + dy if self.valid(self.current.shape, new_x, new_y): self.current.x, self.current.y = new_x, new_y return True return False def rotate(self): rotated = self.current.rotated() if self.valid(rotated, self.current.x, self.current.y): self.current.shape = rotated else: # Try wall kicks (simple) for dx in (-1, 1, -2, 2): if self.valid(rotated, self.current.x + dx, self.current.y): self.current.x += dx self.current.shape = rotated break def hard_drop(self): while self.move(0, 1): pass self.lock_piece() def draw(self): # Clear screen + hide cursor print("\033[2J\033[?25l", end="") # Draw board with border output = [] output.append("Score: {} Level: {}".format(self.score, self.level)) output.append("┌" + "─" * WIDTH + "┐") for y in range(HEIGHT): line = [] for x in range(WIDTH): if (0 <= y - self.current.y < len(self.current.shape) and 0 <= x - self.current.x < len(self.current.shape[0]) and self.current.shape[y - self.current.y][x - self.current.x]): line.append(self.current.color + "■" + RESET) else: cell = self.board[y][x] line.append(cell + "■" + RESET if cell else " ") output.append("│" + "".join(line) + "│") output.append("└" + "─" * WIDTH + "┘") # Draw next output.append("Next:") for row in self.next.shape: line = [] for cell in row: line.append(self.next.color + "■" + RESET if cell else " ") output.append(" " + "".join(line)) output.append("\nControls: ← → ↓ rotate: ↑ drop: space quit: q") print("\n".join(output), end="", flush=True) def run(self): last_drop = time.time() while not self.game_over: self.draw() ch = self.getch() if ch == '\x1b': # ESC sequence ch += sys.stdin.read(2) if ch in ('q', '\x03'): # q or Ctrl-C break elif ch == '\x1b[A': # up self.rotate() elif ch == '\x1b[B': # down self.move(0, 1) elif ch == '\x1b[C': # right self.move(1, 0) elif ch == '\x1b[D': # left self.move(-1, 0) elif ch == ' ': self.hard_drop() last_drop = time.time() # Auto-drop if time.time() - last_drop > self.drop_time: if not self.move(0, 1): self.lock_piece() last_drop = time.time() print("\033[?25h\033[2JGame Over! Score: {}".format(self.score)) def getch(self) -> str: fd = sys.stdin.fileno() old = termios.tcgetattr(fd) try: tty.setraw(fd) ch = sys.stdin.read(1) return ch finally: termios.tcsetattr(fd, termios.TCSADRAIN, old) if __name__ == "__main__": try: Tetris().run() except KeyboardInterrupt: print("\033[?25h") # restore cursor