Browse Source

Add Tetris implementation using Textual TUI

master
Matteo Benedetto 3 days ago
parent
commit
5184df290c
  1. 224
      tetris_textual.py

224
tetris_textual.py

@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""Implementazione minimale di Tetris con Textual (TUI moderna)."""
import random
from dataclasses import dataclass
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Static, Footer, Header
from textual.binding import Binding
from textual.timer import Timer
# Tetromini classici come liste di (x, y) relative al centro
SHAPES = {
"I": [(0, -1), (0, 0), (0, 1), (0, 2)],
"O": [(0, 0), (0, 1), (1, 0), (1, 1)],
"T": [(-1, 0), (0, 0), (1, 0), (0, 1)],
"S": [(-1, 0), (0, 0), (0, 1), (1, 1)],
"Z": [(-1, 1), (0, 1), (0, 0), (1, 0)],
"J": [(-1, -1), (-1, 0), (0, 0), (1, 0)],
"L": [(-1, 0), (0, 0), (1, 0), (1, -1)],
}
COLORS = {
"I": "cyan",
"O": "yellow",
"T": "magenta",
"S": "green",
"Z": "red",
"J": "blue",
"L": "orange3",
}
BOARD_WIDTH = 10
BOARD_HEIGHT = 20
@dataclass
class Piece:
cells: list[tuple[int, int]]
x: int
y: int
kind: str
@property
def color(self) -> str:
return COLORS[self.kind]
def translated(self, dx: int, dy: int) -> "Piece":
return Piece(self.cells, self.x + dx, self.y + dy, self.kind)
def rotated(self) -> "Piece":
# Rotazione 90 gradi orario: (x, y) -> (-y, x)
rotated_cells = [(-y, x) for x, y in self.cells]
return Piece(rotated_cells, self.x, self.y, self.kind)
def absolute_positions(self) -> list[tuple[int, int]]:
return [(self.x + cx, self.y + cy) for cx, cy in self.cells]
class TetrisApp(App):
CSS = """
Screen { align: center middle; }
#board {
width: 22;
height: 22;
border: solid green;
padding: 0;
}
#side {
width: 24;
height: 22;
padding: 1 2;
}
"""
BINDINGS = [
Binding("left", "move(-1)", "Left"),
Binding("right", "move(1)", "Right"),
Binding("down", "drop", "Down"),
Binding("up", "rotate", "Rotate"),
Binding("space", "rotate", "Rotate"),
Binding("q", "quit", "Quit"),
Binding("p", "pause", "Pause"),
]
def __init__(self) -> None:
super().__init__()
self.grid: list[list[str | None]] = [[None] * BOARD_WIDTH for _ in range(BOARD_HEIGHT)]
self.current_piece: Piece = self._spawn_piece()
self.score = 0
self.game_over = False
self.paused = False
self.tick_timer: Timer | None = None
def compose(self) -> ComposeResult:
yield Header()
with Horizontal():
yield Static(id="board")
with Vertical(id="side"):
yield Static("Score: 0\n\nControls:\n← → move\n↓ soft drop\n↑/space rotate\np pause\nq quit", id="info")
yield Footer()
def on_mount(self) -> None:
self.tick_timer = self.set_interval(0.6, self._tick)
self._render()
def _spawn_piece(self) -> Piece:
kind = random.choice(list(SHAPES.keys()))
return Piece(SHAPES[kind][:], BOARD_WIDTH // 2, 0, kind)
def _is_valid(self, piece: Piece) -> bool:
for x, y in piece.absolute_positions():
if x < 0 or x >= BOARD_WIDTH or y >= BOARD_HEIGHT:
return False
if y >= 0 and self.grid[y][x] is not None:
return False
return True
def _lock_piece(self) -> None:
for x, y in self.current_piece.absolute_positions():
if 0 <= y < BOARD_HEIGHT and 0 <= x < BOARD_WIDTH:
self.grid[y][x] = self.current_piece.color
self._clear_lines()
self.current_piece = self._spawn_piece()
if not self._is_valid(self.current_piece):
self.game_over = True
if self.tick_timer:
self.tick_timer.stop()
self.query_one("#info", Static).update("GAME OVER\n\nScore: " + str(self.score))
def _clear_lines(self) -> None:
new_grid = [row for row in self.grid if any(cell is None for cell in row)]
cleared = BOARD_HEIGHT - len(new_grid)
for _ in range(cleared):
new_grid.insert(0, [None] * BOARD_WIDTH)
self.grid = new_grid
self.score += cleared * 100
def _tick(self) -> None:
if self.game_over or self.paused:
return
moved = self.current_piece.translated(0, 1)
if self._is_valid(moved):
self.current_piece = moved
else:
self._lock_piece()
self._render()
def action_move(self, delta: int) -> None:
if self.game_over or self.paused:
return
moved = self.current_piece.translated(delta, 0)
if self._is_valid(moved):
self.current_piece = moved
self._render()
def action_drop(self) -> None:
if self.game_over or self.paused:
return
moved = self.current_piece.translated(0, 1)
if self._is_valid(moved):
self.current_piece = moved
self.score += 1
else:
self._lock_piece()
self._render()
def action_rotate(self) -> None:
if self.game_over or self.paused:
return
rotated = self.current_piece.rotated()
# Wall kick minimale
for dx in (0, 1, -1, 2, -2):
candidate = rotated.translated(dx, 0)
if self._is_valid(candidate):
self.current_piece = candidate
self._render()
return
def action_pause(self) -> None:
self.paused = not self.paused
self._render()
def _render(self) -> None:
board_widget = self.query_one("#board", Static)
lines = []
# Bordo superiore
lines.append("+" + "-" * (BOARD_WIDTH * 2) + "+")
for y in range(BOARD_HEIGHT):
row_str = "|"
for x in range(BOARD_WIDTH):
# Pezzo attivo
occupied = False
for px, py in self.current_piece.absolute_positions():
if px == x and py == y:
row_str += f"[{self.current_piece.color}]#[/]"
occupied = True
break
if not occupied:
cell = self.grid[y][x]
if cell:
row_str += f"[{cell}]#[/]"
else:
row_str += " ."
row_str += "|"
lines.append(row_str)
# Bordo inferiore
lines.append("+" + "-" * (BOARD_WIDTH * 2) + "+")
board_widget.update("\n".join(lines))
info_widget = self.query_one("#info", Static)
status = "PAUSED" if self.paused else ""
info_widget.update(
f"Score: {self.score}\n{status}\n\n"
"Controls:\n← → move\n↓ soft drop\n↑/space rotate\np pause\nq quit"
)
if __name__ == "__main__":
app = TetrisApp()
app.run()
Loading…
Cancel
Save