generated from mwc/project_game
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.
This commit is contained in:
299
cursor/play_game.py
Normal file
299
cursor/play_game.py
Normal file
@@ -0,0 +1,299 @@
|
||||
# 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.')
|
||||
Reference in New Issue
Block a user