generated from mwc/project_game
I had a game idea working with cursor, but I am trying
to convert it to work in retro. I'm stuck.
This commit is contained in:
BIN
__pycache__/tetris.cpython-311.pyc
Normal file
BIN
__pycache__/tetris.cpython-311.pyc
Normal file
Binary file not shown.
299
play_game.py
Normal file
299
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.')
|
||||||
452
play_game2.py
Normal file
452
play_game2.py
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
# Import libraries for random piece selection, terminal graphics, and timing
|
||||||
|
import random
|
||||||
|
import curses
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Board dimensions: 10 columns wide, 20 rows tall (standard Tetris size)
|
||||||
|
W, H = 10, 20
|
||||||
|
|
||||||
|
# Terminal layout: Scale up cells for bigger blocks
|
||||||
|
# CELL_W: number of characters wide per block (4 = wider blocks)
|
||||||
|
# CELL_H: number of rows tall per block (2 = taller blocks)
|
||||||
|
CELL_W = 4
|
||||||
|
CELL_H = 2
|
||||||
|
|
||||||
|
# Function to calculate centered positioning in the terminal
|
||||||
|
# This reads the terminal size and returns padding values to center the game board
|
||||||
|
def get_centered_offsets(stdscr):
|
||||||
|
"""Calculate PAD_X and PAD_Y to center the game board in the terminal."""
|
||||||
|
# Get terminal dimensions (max_y = height, max_x = width)
|
||||||
|
max_y, max_x = stdscr.getmaxyx()
|
||||||
|
# Calculate board dimensions in characters: width and height with borders
|
||||||
|
board_width = W * CELL_W + 2
|
||||||
|
board_height = H * CELL_H + 2
|
||||||
|
# Total width includes board + side panel (score, next piece preview)
|
||||||
|
total_width = board_width + 6 + 20
|
||||||
|
# Center horizontally and vertically: divide remaining space by 2
|
||||||
|
pad_x = max(1, (max_x - total_width) // 2)
|
||||||
|
pad_y = max(1, (max_y - board_height) // 2)
|
||||||
|
return pad_x, pad_y
|
||||||
|
|
||||||
|
# Default padding values (will be overridden by get_centered_offsets in main_curses)
|
||||||
|
# PAD_X: left padding (columns from terminal edge to board edge)
|
||||||
|
# PAD_Y: top padding (rows from terminal top to board edge)
|
||||||
|
# SIDE_X: column position of the side panel (score, next block)
|
||||||
|
PAD_X = 2
|
||||||
|
PAD_Y = 1
|
||||||
|
SIDE_X = PAD_X + W * CELL_W + 6
|
||||||
|
|
||||||
|
# 2D game board: 20 rows (height) × 10 columns (width)
|
||||||
|
# Each cell is either None (empty) or a color string (filled with a block)
|
||||||
|
board = [[None] * W for _ in range(H)]
|
||||||
|
|
||||||
|
# Function to draw the game board on screen
|
||||||
|
def draw_board_curses(win):
|
||||||
|
# Draw the left and right borders (| characters)
|
||||||
|
for y in range(H + 2):
|
||||||
|
win.addstr(PAD_Y + y, PAD_X - 1, '|')
|
||||||
|
win.addstr(PAD_Y + y, PAD_X + W * 2, '|')
|
||||||
|
# Draw border that spans exactly from the top corner to the bottom corner
|
||||||
|
top_row = PAD_Y - 1
|
||||||
|
bottom_row = PAD_Y + H * CELL_H
|
||||||
|
left_col = PAD_X - 1
|
||||||
|
right_col = PAD_X + W * CELL_W
|
||||||
|
# Draw vertical borders from top_row to bottom_row (inclusive)
|
||||||
|
for row in range(top_row, bottom_row + 1):
|
||||||
|
win.addstr(row, left_col, '|')
|
||||||
|
win.addstr(row, right_col, '|')
|
||||||
|
# Draw top and bottom border lines
|
||||||
|
top = '+' + '-' * (W * CELL_W) + '+'
|
||||||
|
win.addstr(top_row, left_col, top)
|
||||||
|
win.addstr(bottom_row, left_col, top)
|
||||||
|
# Draw each cell on the board (filled cells are shown as ██, empty as spaces)
|
||||||
|
for i, row in enumerate(board[::-1]): # board[::-1] reverses rows so row 0 (bottom) is at bottom
|
||||||
|
for h in range(CELL_H): # CELL_H is the height in rows each block takes
|
||||||
|
screen_row = PAD_Y + 1 + i * CELL_H + h
|
||||||
|
for j, color in enumerate(row):
|
||||||
|
# Empty cell: spaces; filled cell: solid blocks (██)
|
||||||
|
ch = ' ' * CELL_W
|
||||||
|
attr = curses.color_pair(0)
|
||||||
|
if color:
|
||||||
|
ch = '█' * CELL_W
|
||||||
|
attr = curses.color_pair(2)
|
||||||
|
win.addstr(screen_row, PAD_X + j * CELL_W, ch, attr)
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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 draw a tetromino (falling block) on screen
|
||||||
|
# Can draw on the main board or in the side panel (preview area)
|
||||||
|
def draw_block_curses(win, block, x0, y0, use_board=True, draw_x=None, draw_y=None):
|
||||||
|
"""Draw a block either on the main board (use_board=True) or in a side area.
|
||||||
|
|
||||||
|
For board drawing, x0,y0 are board coordinates. For side drawing, set
|
||||||
|
use_board=False and provide draw_x, draw_y as screen coordinates (columns, rows).
|
||||||
|
"""
|
||||||
|
# Iterate through each block cell in the tetromino
|
||||||
|
for dx, dy in block:
|
||||||
|
# Calculate actual position by adding offsets to base position
|
||||||
|
x, y = x0 + dx, y0 + dy
|
||||||
|
if use_board:
|
||||||
|
# Draw on main game board
|
||||||
|
if 0 <= x < W and 0 <= y < H:
|
||||||
|
# Convert board coordinates to screen coordinates
|
||||||
|
# (accounting for CELL_W and CELL_H scaling)
|
||||||
|
screen_row_base = PAD_Y + 1 + (H - 1 - y) * CELL_H
|
||||||
|
screen_col = PAD_X + x * CELL_W
|
||||||
|
# Draw the block with height CELL_H (fill multiple rows)
|
||||||
|
for hh in range(CELL_H):
|
||||||
|
win.addstr(screen_row_base + hh, screen_col, '█' * CELL_W, curses.color_pair(2))
|
||||||
|
else:
|
||||||
|
# Draw in side panel (for next-piece preview)
|
||||||
|
if draw_x is None or draw_y is None:
|
||||||
|
continue
|
||||||
|
# Calculate side panel position using scaled cell dimensions
|
||||||
|
sx = draw_x + x * CELL_W
|
||||||
|
sy = draw_y + (y * CELL_H)
|
||||||
|
try:
|
||||||
|
# Draw block with CELL_H rows
|
||||||
|
for hh in range(CELL_H):
|
||||||
|
win.addstr(sy + hh, sx, '█' * CELL_W, curses.color_pair(2))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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 between red and blue)
|
||||||
|
for t in range(duration):
|
||||||
|
if t in (0, duration // 2):
|
||||||
|
# Change color of completed rows (red or blue)
|
||||||
|
color = ('red', 'blue')[t // (duration//2)]
|
||||||
|
for row in find_completed_rows():
|
||||||
|
row[:] = [color] * W
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# Function to render (draw) the current game state on the terminal screen
|
||||||
|
def render_stage_curses(win):
|
||||||
|
# Clear the screen
|
||||||
|
win.erase()
|
||||||
|
# Draw the main game board with borders and filled cells
|
||||||
|
draw_board_curses(win)
|
||||||
|
# Display score in the side panel
|
||||||
|
win.addstr(PAD_Y, SIDE_X, f'SCORE: {score}')
|
||||||
|
# Display "NEXT:" label
|
||||||
|
win.addstr(PAD_Y + 2, SIDE_X, 'NEXT:')
|
||||||
|
# Draw the next piece preview in the side panel
|
||||||
|
if next_block:
|
||||||
|
# Normalize block coordinates so preview fits in a compact box
|
||||||
|
minx = min(dx for dx, dy in next_block)
|
||||||
|
miny = min(dy for dx, dy in next_block)
|
||||||
|
# Position preview below the NEXT label
|
||||||
|
preview_x = SIDE_X
|
||||||
|
preview_y = PAD_Y + 6
|
||||||
|
draw_block_curses(win, next_block, -minx, -miny, use_board=False, draw_x=preview_x, draw_y=preview_y)
|
||||||
|
# Draw the currently falling piece on the main board
|
||||||
|
if block:
|
||||||
|
draw_block_curses(win, block, x, y)
|
||||||
|
# Display GAME OVER message if the game has ended
|
||||||
|
if state == 'game_over':
|
||||||
|
win.addstr(PAD_Y + H//2, PAD_X + W - 4, 'GAME OVER', curses.color_pair(1))
|
||||||
|
# Update the screen display
|
||||||
|
win.refresh()
|
||||||
|
|
||||||
|
|
||||||
|
# Input handler: move piece left
|
||||||
|
# Called when 'a' or left arrow is pressed
|
||||||
|
def on_key_left():
|
||||||
|
if block:
|
||||||
|
move_block(-1, 0)
|
||||||
|
|
||||||
|
# Input handler: move piece right
|
||||||
|
# Called when 'd' or right arrow is pressed
|
||||||
|
def on_key_right():
|
||||||
|
if block:
|
||||||
|
move_block(1, 0)
|
||||||
|
|
||||||
|
# Input handler: soft drop (or lock piece if already at bottom)
|
||||||
|
# Called when 's' or down arrow is pressed
|
||||||
|
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)
|
||||||
|
# Called when spacebar is pressed
|
||||||
|
def on_key_space():
|
||||||
|
if block:
|
||||||
|
begin_falling(period=0) # period=0 means fall instantly
|
||||||
|
|
||||||
|
# Input handler: rotate piece 90 degrees
|
||||||
|
# Called when 'w' or up arrow is pressed
|
||||||
|
def on_key_up():
|
||||||
|
if block:
|
||||||
|
rotate_block()
|
||||||
|
|
||||||
|
|
||||||
|
# Main game function - runs the curses terminal game loop
|
||||||
|
def main_curses(stdscr):
|
||||||
|
# Declare globals so we can modify them in this function
|
||||||
|
global PAD_X, PAD_Y, SIDE_X
|
||||||
|
|
||||||
|
# Curses initialization
|
||||||
|
curses.curs_set(0) # Hide the cursor
|
||||||
|
stdscr.nodelay(True) # Non-blocking input (don't wait for keypresses)
|
||||||
|
stdscr.keypad(True) # Enable special keys (arrow keys, etc.)
|
||||||
|
curses.start_color() # Enable color support
|
||||||
|
curses.use_default_colors() # Use terminal's default colors
|
||||||
|
# Define color pairs: (ID, foreground color, background color)
|
||||||
|
curses.init_pair(1, curses.COLOR_RED, -1) # Red for GAME OVER text
|
||||||
|
curses.init_pair(2, curses.COLOR_BLUE, -1) # Blue for game blocks
|
||||||
|
|
||||||
|
# Calculate centered board position based on terminal size
|
||||||
|
PAD_X, PAD_Y = get_centered_offsets(stdscr)
|
||||||
|
SIDE_X = PAD_X + W * CELL_W + 6
|
||||||
|
|
||||||
|
# Initialize the game: spawn first piece
|
||||||
|
reset_block()
|
||||||
|
# Game tick rate: 100ms (0.1 seconds) between game updates
|
||||||
|
tick = 0.1
|
||||||
|
# Track time for tick timing
|
||||||
|
last = time.time()
|
||||||
|
|
||||||
|
# Main game loop
|
||||||
|
while True:
|
||||||
|
# Get current time
|
||||||
|
now = time.time()
|
||||||
|
# If enough time has passed, do a game update
|
||||||
|
if now - last >= tick:
|
||||||
|
# Advance game state (gravity, animations, etc.)
|
||||||
|
update_stage()
|
||||||
|
# Redraw the screen with updated state
|
||||||
|
render_stage_curses(stdscr)
|
||||||
|
# Reset timer for next tick
|
||||||
|
last = now
|
||||||
|
|
||||||
|
# Check for keyboard input (non-blocking)
|
||||||
|
try:
|
||||||
|
ch = stdscr.getch()
|
||||||
|
except Exception:
|
||||||
|
ch = -1
|
||||||
|
|
||||||
|
# Process keyboard input
|
||||||
|
if ch != -1:
|
||||||
|
# Quit game
|
||||||
|
if ch in (ord('q'), ord('Q')):
|
||||||
|
break
|
||||||
|
# Move left
|
||||||
|
elif ch in (curses.KEY_LEFT, ord('a')):
|
||||||
|
on_key_left()
|
||||||
|
# Move right
|
||||||
|
elif ch in (curses.KEY_RIGHT, ord('d')):
|
||||||
|
on_key_right()
|
||||||
|
# Soft drop
|
||||||
|
elif ch in (curses.KEY_DOWN, ord('s')):
|
||||||
|
on_key_down()
|
||||||
|
# Rotate
|
||||||
|
elif ch in (curses.KEY_UP, ord('w')):
|
||||||
|
on_key_up()
|
||||||
|
# Hard drop
|
||||||
|
elif ch == ord(' '):
|
||||||
|
on_key_space()
|
||||||
|
|
||||||
|
# Small sleep to avoid consuming 100% CPU
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
# Entry point: run the game using curses wrapper (handles cleanup automatically)
|
||||||
|
if __name__ == '__main__':
|
||||||
27
proposal.md
Normal file
27
proposal.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## Game name
|
||||||
|
|
||||||
|
Tessera
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
|
||||||
|
Tessera takes classic Tetris and adds a light puzzle layer: arranged clears that match colors grant small bonuses, rewarding planning without changing the tight, fast arcade feel. It's worth making because it blends a familiar, addictive core with a gentle twist that increases depth for skilled players while staying approachable.
|
||||||
|
|
||||||
|
## What the game will look like
|
||||||
|
|
||||||
|
A clean, modern pixel/flat aesthetic with bright, readable tetromino colors on a dark background; the HUD shows next pieces, hold slot, score, level, and subtle particle effects on clears.
|
||||||
|
|
||||||
|
## Player interaction
|
||||||
|
|
||||||
|
Players use keyboard controls to move, rotate, soft/hard drop, and hold pieces; menus use simple mouse/keyboard navigation. The game emphasizes quick decision-making and responsive controls with a visible ghost piece to aid placement.
|
||||||
|
|
||||||
|
## Core mechanics
|
||||||
|
|
||||||
|
1. Falling tetromino placement and rotation — move and rotate pieces to fit them into the well.
|
||||||
|
2. Line clearing and gravity — completed rows clear and the above rows shift down, possibly chaining combos.
|
||||||
|
3. Hold and next-queue — store one piece and preview upcoming pieces to plan ahead.
|
||||||
|
4. Color-match bonus (optional) — clearing lines where most blocks share a color grants small score/energy bonuses to encourage pattern play.
|
||||||
|
|
||||||
|
## Milestone (first target)
|
||||||
|
|
||||||
|
Implement the core game engine: represent the board and pieces, support spawning, movement, rotation, collision detection, locking, and single-line clearing so you can run a minimal playable loop and unit tests for the engine.
|
||||||
|
|
||||||
142
test_game.py
Normal file
142
test_game.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import retro
|
||||||
|
import tetris
|
||||||
|
from retro.agent import ArrowKeyAgent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Expose Game name expected by create_game helper
|
||||||
|
game = tetris.Tetris()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Factory helpers and small harness ---
|
||||||
|
|
||||||
|
def create_game():
|
||||||
|
"""Create a Game instance with minimal sensible defaults."""
|
||||||
|
try:
|
||||||
|
return Game()
|
||||||
|
except TypeError:
|
||||||
|
try:
|
||||||
|
sig = inspect.signature(Game)
|
||||||
|
kwargs = {}
|
||||||
|
if 'agents' in sig.parameters:
|
||||||
|
kwargs['agents'] = []
|
||||||
|
if 'state' in sig.parameters:
|
||||||
|
kwargs['state'] = {}
|
||||||
|
return Game(**kwargs)
|
||||||
|
except Exception:
|
||||||
|
for args in (([], {}), ([], None), ([],), (None,)):
|
||||||
|
try:
|
||||||
|
return Game(*args)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def create_agent(game=None):
|
||||||
|
try:
|
||||||
|
return ArrowKeyAgent(game)
|
||||||
|
except TypeError:
|
||||||
|
return ArrowKeyAgent()
|
||||||
|
|
||||||
|
|
||||||
|
def draw_screen(game: Game):
|
||||||
|
# simple text render
|
||||||
|
game.clear()
|
||||||
|
s = game.draw_board()
|
||||||
|
game.refresh()
|
||||||
|
# store/return the string for tests
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argv = sys.argv[1:]
|
||||||
|
if len(argv) >= 1 and argv[0] == 'play':
|
||||||
|
# Try to run a terminal player. Use curses if available.
|
||||||
|
try:
|
||||||
|
play_in_terminal(create_game())
|
||||||
|
except Exception as e:
|
||||||
|
print('Terminal play failed:', e)
|
||||||
|
print('Run without args to run smoke tests instead.')
|
||||||
|
return
|
||||||
|
|
||||||
|
game = create_game()
|
||||||
|
agent = create_agent(game)
|
||||||
|
|
||||||
|
while not game.is_over():
|
||||||
|
action = agent.get_action()
|
||||||
|
game.step(action)
|
||||||
|
draw_screen(game)
|
||||||
|
|
||||||
|
print('Game Over! Score:', game.get_score())
|
||||||
|
|
||||||
|
def play_in_terminal(game):
|
||||||
|
"""Simple curses-based player loop.
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
- Left/Right arrows or 'a'/'d' to move
|
||||||
|
- Up arrow or 'w' to rotate
|
||||||
|
- Down arrow or 's' to soft drop
|
||||||
|
- Space for hard drop
|
||||||
|
- 'q' to quit
|
||||||
|
"""
|
||||||
|
|
||||||
|
keymap = {
|
||||||
|
# map keys to the Testris.action integer codes:
|
||||||
|
# 0 noop, 1 left, 2 right, 3 rotate, 4 soft drop
|
||||||
|
curses.KEY_LEFT: 1,
|
||||||
|
curses.KEY_RIGHT: 2,
|
||||||
|
curses.KEY_UP: 3,
|
||||||
|
curses.KEY_DOWN: 4,
|
||||||
|
ord('a'): 1,
|
||||||
|
ord('d'): 2,
|
||||||
|
ord('w'): 3,
|
||||||
|
ord('s'): 4,
|
||||||
|
# space will perform a hard drop (special handling)
|
||||||
|
ord(' '): 'hard_drop',
|
||||||
|
ord('q'): 'quit',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- Simple smoke tests ---
|
||||||
|
|
||||||
|
def test_game_initialization():
|
||||||
|
g = create_game()
|
||||||
|
assert g is not None
|
||||||
|
assert g.get_score() == 0
|
||||||
|
assert not g.is_over()
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_step():
|
||||||
|
g = create_game()
|
||||||
|
score0 = g.get_score()
|
||||||
|
g.step(0)
|
||||||
|
assert g.get_score() >= score0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_board_returns_str():
|
||||||
|
g = create_game()
|
||||||
|
s = g.draw_board()
|
||||||
|
assert isinstance(s, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_over_sequence():
|
||||||
|
g = create_game()
|
||||||
|
# run a few steps to ensure no immediate crash
|
||||||
|
for _ in range(50):
|
||||||
|
if g.is_over():
|
||||||
|
break
|
||||||
|
g.step(0)
|
||||||
|
assert g.get_score() >= 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
123
tetris.py
Normal file
123
tetris.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Tetris:
|
||||||
|
W = 10
|
||||||
|
H = 20
|
||||||
|
|
||||||
|
PIECES = {
|
||||||
|
'I': [[(0,1),(1,1),(2,1),(3,1)], [(2,0),(2,1),(2,2),(2,3)]],
|
||||||
|
'O': [[(1,0),(2,0),(1,1),(2,1)]],
|
||||||
|
'T': [[(1,0),(0,1),(1,1),(2,1)], [(1,0),(1,1),(2,1),(1,2)], [(0,1),(1,1),(2,1),(1,2)], [(1,0),(0,1),(1,1),(1,2)]],
|
||||||
|
'L': [[(2,0),(0,1),(1,1),(2,1)], [(1,0),(1,1),(1,2),(2,2)], [(0,1),(1,1),(2,1),(0,2)], [(0,0),(1,0),(1,1),(1,2)]],
|
||||||
|
'J': [[(0,0),(0,1),(1,1),(2,1)], [(1,0),(2,0),(1,1),(1,2)], [(0,1),(1,1),(2,1),(2,2)], [(1,0),(1,1),(1,2),(0,2)]],
|
||||||
|
'S': [[(1,0),(2,0),(0,1),(1,1)], [(1,0),(1,1),(2,1),(2,2)]],
|
||||||
|
'Z': [[(0,0),(1,0),(1,1),(2,1)], [(2,0),(1,1),(2,1),(1,2)]],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, agents=None, state=None):
|
||||||
|
self.score = 0
|
||||||
|
self.over = False
|
||||||
|
self.grid = [[0] * self.W for _ in range(self.H)]
|
||||||
|
self.rng = random.Random(0)
|
||||||
|
self.next_piece = self._rand_piece()
|
||||||
|
self._spawn()
|
||||||
|
self.last_render = ''
|
||||||
|
|
||||||
|
def _rand_piece(self):
|
||||||
|
return self.rng.choice(list(self.PIECES.keys()))
|
||||||
|
|
||||||
|
def _spawn(self):
|
||||||
|
self.piece = self.next_piece
|
||||||
|
self.next_piece = self._rand_piece()
|
||||||
|
self.rot = 0
|
||||||
|
self.px = (self.W // 2) - 2
|
||||||
|
self.py = 0
|
||||||
|
if not self._fits(self.px, self.py, self.rot):
|
||||||
|
self.over = True
|
||||||
|
|
||||||
|
def _cells(self, px, py, rot):
|
||||||
|
rstates = self.PIECES[self.piece]
|
||||||
|
r = rot % len(rstates)
|
||||||
|
return [(px + x, py + y) for (x, y) in rstates[r]]
|
||||||
|
|
||||||
|
def _fits(self, px, py, rot):
|
||||||
|
for x, y in self._cells(px, py, rot):
|
||||||
|
if x < 0 or x >= self.W or y < 0 or y >= self.H:
|
||||||
|
return False
|
||||||
|
if self.grid[y][x]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _lock(self):
|
||||||
|
for x, y in self._cells(self.px, self.py, self.rot):
|
||||||
|
if 0 <= y < self.H and 0 <= x < self.W:
|
||||||
|
self.grid[y][x] = 1
|
||||||
|
# clear lines
|
||||||
|
newg = [row for row in self.grid if not all(row)]
|
||||||
|
cleared = self.H - len(newg)
|
||||||
|
if cleared:
|
||||||
|
for _ in range(cleared):
|
||||||
|
newg.insert(0, [0] * self.W)
|
||||||
|
self.grid = newg
|
||||||
|
self.score += 100 * cleared
|
||||||
|
self._spawn()
|
||||||
|
|
||||||
|
# API methods used by tests / agent
|
||||||
|
def step(self, action=0):
|
||||||
|
# actions: 0 noop, 1 left, 2 right, 3 rotate, 4 soft drop
|
||||||
|
if self.over:
|
||||||
|
return
|
||||||
|
if action == 1:
|
||||||
|
if self._fits(self.px - 1, self.py, self.rot):
|
||||||
|
self.px -= 1
|
||||||
|
elif action == 2:
|
||||||
|
if self._fits(self.px + 1, self.py, self.rot):
|
||||||
|
self.px += 1
|
||||||
|
elif action == 3:
|
||||||
|
if self._fits(self.px, self.py, self.rot + 1):
|
||||||
|
self.rot += 1
|
||||||
|
elif action == 4:
|
||||||
|
if self._fits(self.px, self.py + 1, self.rot):
|
||||||
|
self.py += 1
|
||||||
|
else:
|
||||||
|
self._lock()
|
||||||
|
return
|
||||||
|
# gravity
|
||||||
|
if self._fits(self.px, self.py + 1, self.rot):
|
||||||
|
self.py += 1
|
||||||
|
else:
|
||||||
|
self._lock()
|
||||||
|
|
||||||
|
def is_over(self):
|
||||||
|
return bool(self.over)
|
||||||
|
|
||||||
|
def get_score(self):
|
||||||
|
return int(self.score)
|
||||||
|
|
||||||
|
# rendering stubs (retro compatibility)
|
||||||
|
def clear(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def draw_board(self):
|
||||||
|
lines = []
|
||||||
|
occupied = set(self._cells(self.px, self.py, self.rot)) if not self.over else set()
|
||||||
|
for y in range(self.H):
|
||||||
|
row = ''
|
||||||
|
for x in range(self.W):
|
||||||
|
if (x, y) in occupied:
|
||||||
|
row += '[]'
|
||||||
|
else:
|
||||||
|
row += '##' if self.grid[y][x] else '..'
|
||||||
|
lines.append(row)
|
||||||
|
self.last_render = '\n'.join(lines)
|
||||||
|
return self.last_render
|
||||||
|
|
||||||
|
def draw_piece(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.__init__()
|
||||||
Reference in New Issue
Block a user