# 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.')