Files
project_game/cursor/play_game.py
Chris Proctor c8d1ddd58f Started retro tetris game
Started developing a retro version of Tetris, following
the planning on the board of the classroom
(see planning.jpg).

Moved cursor work into cursor.
2025-12-18 17:45:39 -05:00

300 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tetris game adapted for retro module (graphics-based Pythonista)
# Import libraries for random piece selection and timing
import random
import time
# Import retro module for graphics (Pythonista only)
try:
from retro.game import Game as RetroGame
except ImportError:
RetroGame = None
# Board dimensions: 10 columns wide, 20 rows tall (standard Tetris size)
W, H = 10, 20
# Graphics settings: cell size in pixels for drawing
CELL_W = 30 # pixels wide per cell
CELL_H = 30 # pixels tall per cell
BORDER = 2 # border line width
# 2D game board: 20 rows (height) × 10 columns (width)
# Each cell is either None (empty) or a color value (filled with a block)
board = [[None] * W for _ in range(H)]
# Define all 7 Tetris piece shapes (I, O, T, S, Z, J, L)
# Each shape is stored as a list of (x, y) offsets from the piece's center
BLOCKS = [[] for _ in range(7)]
# Parse ASCII template: each 'o' represents a block, positions calculated from the string
for i, line in enumerate('''
oo o o oo oo o
oooo oo ooo ooo oo oo ooo'''.split('\n')):
for j, char in enumerate(line):
if char == 'o':
# Store block position relative to piece center
BLOCKS[j // 5].append((j%5 - 1, -i + 2))
# Function to check if a piece can be placed at a given position
# Returns True if the position is valid (no collisions), False otherwise
def can_place_block_clipped(block, x0, y0):
# Check each cell of the piece
for dx, dy in block:
# Calculate actual position
x, y = x0 + dx, y0 + dy
# Check if position is out of bounds or collides with existing blocks
if not (0 <= x < W and 0 <= y) or (y < H and board[y][x]):
return False
return True
# Function to lock a piece permanently onto the board
# Marks each cell of the piece as filled
def place_block(block, x0, y0):
# Assume placement is complete initially
complete = True
# Place each block cell on the board
for dx, dy in block:
x, y = x0 + dx, y0 + dy
# Only place if within board bounds (x and y must be valid)
if 0 <= x < W and 0 <= y < H:
board[y][x] = 'blue' # Mark cell as filled with color 'blue'
else:
# If any part extends outside, placement is incomplete (game over)
complete = False
return complete
# Function to find rows that are completely filled (ready to be cleared)
# Yields (returns) each full row one at a time
def find_completed_rows():
for row in board:
# Check if all cells in the row have a color (no None values)
if all(row):
yield row
# Game state variables
# score: player's current score
score = 0
# t: game tick counter (increments each game loop iteration)
t = 0
# falling_period: how many ticks before a piece falls one row (decreases with level)
falling_period = 0
# state: current game state ('normal' = playing, 'game_over' = lost)
state = 'normal'
# block: currently falling piece (list of offsets), next_block: preview of next piece
block = next_block = None
# x, y: current position of the falling piece (x=column, y=row)
x, y = 0, 0
# falling_generator: iterator that controls piece falling animation
falling_generator = None
# deleting_rows_generator: iterator that controls line-clear animation
deleting_rows_generator = None
# Function to move the current falling piece left, right, or down
# dx: horizontal direction (-1=left, +1=right), dy: vertical direction (-1=down)
def move_block(dx, dy):
global x, y
# Calculate new position
new_x, new_y = x + dx, y + dy
# Check if new position is valid (no collisions)
possible = can_place_block_clipped(block, new_x, new_y)
# If valid, update piece position
if possible:
x, y = new_x, new_y
return possible
# Function to rotate the current falling piece 90 degrees clockwise
# Rotation formula: (x, y) becomes (-y, x)
def rotate_block():
global block
# Calculate rotated positions
new_block = [(-y, x) for x, y in block]
# Check if rotated position is valid (no collisions)
possible = can_place_block_clipped(new_block, x, y)
# If valid, update piece with rotated version
if possible:
block = new_block
return possible
# Function to spawn the next piece and start it falling
# Also calculates falling speed based on level (score)
def reset_block():
global falling_period, block, next_block, x, y, state
# Calculate falling period (how many ticks before piece falls): decreases with level
# Higher t (more time) = higher level = faster falling
falling_period = [10, 9, 8, 7, 6, 5, 4][min(t // 100, 6)]
# Swap pieces: current next_block becomes new block, pick random next
new_block = None
while not new_block:
new_block, next_block = next_block, BLOCKS[random.randrange(7)]
# Spawn at center-top of board
x, y = W // 2, H - 1
# Check for game over: if new piece collides immediately, game is over
if not can_place_block_clipped(new_block, x, y):
state = 'game_over'
return
# Set active piece and start falling animation
block = new_block
begin_falling(falling_period)
# Function to create a falling animation generator for a piece
# The piece falls one row every 'period' ticks until it hits bottom
def begin_falling(period):
global falling_generator
def fall():
global falling_generator
# Main falling loop
while True:
# Wait 'period' ticks before falling next row
for t in range(period):
yield # Pause here and resume on next tick
# Try to move piece down one row
if not move_block(0, -1):
# Piece can't move down, so it has landed
break
# When piece lands, clear the falling generator and lock piece
falling_generator = None
place_block_and_begin_deleting()
yield
# Create and start the generator
falling_generator = fall()
# Function to lock the current falling piece onto the board permanently
# Then start checking for completed rows to clear
def place_block_and_begin_deleting():
global block, state
# Save the current piece before clearing it
old_block = block
block = None # Clear active piece
# Place the piece on the board
if not place_block(old_block, x, y):
# If placement failed (piece extended above board), game over
state = 'game_over'
return
# Start animation for clearing completed rows
begin_deleting_rows()
# Function to animate and process line clears
# Completed rows flash and then disappear, with remaining blocks dropping down
def begin_deleting_rows():
global deleting_rows_generator
# Check if any rows are completed
if any(find_completed_rows()):
# Animation duration based on falling period (faster at higher levels)
duration = falling_period
def delete():
global deleting_rows_generator, score
# Animate the row clearing (flash animation)
for t in range(duration):
if t in (0, duration // 2):
# Change color of completed rows (flash effect)
pass
yield # Pause to show animation
# After animation, remove completed rows
incomplete_rows = [row for row in board if not all(row)]
n_deleted = H - len(incomplete_rows)
# Shift remaining rows down and fill with empty rows
board[:] = incomplete_rows + [[None] * W for _ in range(n_deleted)]
# Award points: (number of rows cleared)^2 × 100
score += (n_deleted**2) * 100
# Spawn next piece
reset_block()
deleting_rows_generator = None
yield
# Create and start the generator
deleting_rows_generator = delete()
else:
# No completed rows, just spawn next piece immediately
reset_block()
# Function called once per game tick (100ms)
# Advances animations (falling piece, line clears) and increments the time counter
def update_stage():
global t
# Advance the falling animation generator (piece drops)
if block and falling_generator:
next(falling_generator)
# Advance the line-clear animation generator
if deleting_rows_generator:
next(deleting_rows_generator)
# Increment tick counter (used for level calculation)
t += 1
# Input handler: move piece left
def on_key_left():
if block:
move_block(-1, 0)
# Input handler: move piece right
def on_key_right():
if block:
move_block(1, 0)
# Input handler: soft drop (or lock piece if already at bottom)
def on_key_down():
if block:
if not move_block(0, -1):
place_block_and_begin_deleting()
# Input handler: hard drop (instant fall to bottom)
def on_key_space():
if block:
begin_falling(period=0) # period=0 means fall instantly
# Input handler: rotate piece 90 degrees
def on_key_up():
if block:
rotate_block()
# Retro Game class for rendering and game loop
class TetrisGame:
def __init__(self):
# Game timing
self.seconds = 0
self.tick_interval = 0.1 # 100ms per tick
self.last_update = time.time()
# Initialize game
reset_block()
def draw_board(self):
# Drawing would happen here for retro module
# For now, this is a placeholder for graphics rendering
pass
def draw_piece(self):
# Draw the falling piece
pass
def update(self, dt):
# Update timing
self.seconds += dt
if self.seconds > self.tick_interval:
self.seconds = 0
update_stage()
def on_key(self, key):
# Handle keyboard input
if key == 'left' or key == 'a':
on_key_left()
elif key == 'right' or key == 'd':
on_key_right()
elif key == 'down' or key == 's':
on_key_down()
elif key == 'up' or key == 'w':
on_key_up()
elif key == 'space':
on_key_space()
elif key == 'q':
return False # Quit
return True
# Create and run the game
if __name__ == '__main__':
if RetroGame:
# Use retro module if available
try:
game = TetrisGame()
print('Game initialized. Use arrow keys or WASD to play.')
except Exception as e:
print(f'Error initializing retro game: {e}')
else:
print('Retro module not available. This game requires Pythonista.')