| import random |
| import sys |
| import time |
| import termios |
| import tty |
| from typing import List, Tuple, Optional |
|
|
| |
| WIDTH, HEIGHT = 10, 20 |
|
|
| |
| SHAPES = [ |
| [[1, 1, 1, 1]], |
| [[1, 1], [1, 1]], |
| [[0, 1, 0], [1, 1, 1]], |
| [[0, 1, 1], [1, 1, 0]], |
| [[1, 1, 0], [0, 1, 1]], |
| [[1, 0, 0], [1, 1, 1]], |
| [[0, 0, 1], [1, 1, 1]], |
| ] |
|
|
| |
| 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 |
|
|
| 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: |
| |
| 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): |
| |
| print("\033[2J\033[?25l", end="") |
| |
| 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 + "β") |
| |
| 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': |
| ch += sys.stdin.read(2) |
| if ch in ('q', '\x03'): |
| break |
| elif ch == '\x1b[A': |
| self.rotate() |
| elif ch == '\x1b[B': |
| self.move(0, 1) |
| elif ch == '\x1b[C': |
| self.move(1, 0) |
| elif ch == '\x1b[D': |
| self.move(-1, 0) |
| elif ch == ' ': |
| self.hard_drop() |
| last_drop = time.time() |
| |
| 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") |