|
|
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") |