mirror of
https://github.com/OpenBMB/ChatDev.git
synced 2026-04-26 19:58:25 +00:00
680 lines
26 KiB
Python
680 lines
26 KiB
Python
import pygame
|
|
import random
|
|
import numpy as np
|
|
from typing import List, Tuple, Optional
|
|
import os
|
|
import math
|
|
import time
|
|
|
|
# Initialize Pygame
|
|
pygame.init()
|
|
pygame.mixer.init()
|
|
|
|
# Constants
|
|
BLOCK_SIZE = 30
|
|
GRID_WIDTH = 10
|
|
GRID_HEIGHT = 20
|
|
PREVIEW_SIZE = 4
|
|
|
|
# Calculate window size
|
|
SIDE_PANEL_WIDTH = 200
|
|
WINDOW_WIDTH = BLOCK_SIZE * GRID_WIDTH + SIDE_PANEL_WIDTH
|
|
WINDOW_HEIGHT = BLOCK_SIZE * GRID_HEIGHT
|
|
|
|
# Colors
|
|
BLACK = (0, 0, 0)
|
|
WHITE = (255, 255, 255)
|
|
GRAY = (128, 128, 128)
|
|
COLORS = {
|
|
'I': (0, 255, 255), # Cyan
|
|
'O': (255, 255, 0), # Yellow
|
|
'T': (128, 0, 128), # Purple
|
|
'S': (0, 255, 0), # Green
|
|
'Z': (255, 0, 0), # Red
|
|
'J': (0, 0, 255), # Blue
|
|
'L': (255, 165, 0), # Orange
|
|
}
|
|
|
|
# Game settings
|
|
INITIAL_FALL_SPEED = 0.8 # Initial time between falls in seconds
|
|
SOFT_DROP_SPEED = 0.05 # Time between falls when soft dropping
|
|
SPEED_UP_FACTOR = 0.08 # How much to speed up per level
|
|
MIN_FALL_SPEED = 0.1 # Minimum fall speed
|
|
LOCK_DELAY = 0.5 # Time in seconds before piece locks in place
|
|
MAX_LOCK_RESETS = 15 # Maximum number of lock delay resets
|
|
|
|
# Animation settings
|
|
MOVE_ANIMATION_SPEED = 0.05 # seconds (faster horizontal movement)
|
|
ROTATION_ANIMATION_SPEED = 0.08 # seconds
|
|
LINE_CLEAR_ANIMATION_TIME = 0.3 # seconds
|
|
FLASH_SPEED = 0.05 # seconds
|
|
|
|
# Tetromino shapes
|
|
SHAPES = {
|
|
'I': [[1, 1, 1, 1]],
|
|
'O': [[1, 1], [1, 1]],
|
|
'T': [[0, 1, 0], [1, 1, 1]],
|
|
'S': [[0, 1, 1], [1, 1, 0]],
|
|
'Z': [[1, 1, 0], [0, 1, 1]],
|
|
'J': [[1, 0, 0], [1, 1, 1]],
|
|
'L': [[0, 0, 1], [1, 1, 1]]
|
|
}
|
|
|
|
class AnimationState:
|
|
def __init__(self):
|
|
self.move_progress = 0
|
|
self.rotation_progress = 0
|
|
self.line_clear_progress = 0
|
|
self.flash_progress = 0
|
|
self.last_pos = None
|
|
self.last_shape = None
|
|
self.target_pos = None
|
|
self.target_shape = None
|
|
self.lines_being_cleared = []
|
|
self.flash_active = False
|
|
|
|
class Particle:
|
|
def __init__(self, x: int, y: int, color: Tuple[int, int, int]):
|
|
self.x = x
|
|
self.y = y
|
|
self.color = color
|
|
self.velocity = [random.uniform(-3, 3), random.uniform(-8, -4)]
|
|
self.life = 255
|
|
self.size = random.randint(2, 6)
|
|
self.rotation = random.uniform(0, 360)
|
|
self.rotation_speed = random.uniform(-5, 5)
|
|
|
|
def update(self):
|
|
self.x += self.velocity[0]
|
|
self.y += self.velocity[1]
|
|
self.velocity[1] += 0.2 # Gravity
|
|
self.velocity[0] *= 0.99 # Air resistance
|
|
self.life -= 3
|
|
self.rotation += self.rotation_speed
|
|
return self.life > 0
|
|
|
|
def draw(self, screen):
|
|
if self.life <= 0:
|
|
return
|
|
|
|
alpha = max(0, min(255, self.life))
|
|
color = (*self.color, alpha)
|
|
|
|
# Create rotated particle
|
|
surface = pygame.Surface((self.size * 2, self.size * 2), pygame.SRCALPHA)
|
|
points = [
|
|
(self.size + math.cos(math.radians(self.rotation)) * self.size,
|
|
self.size + math.sin(math.radians(self.rotation)) * self.size),
|
|
(self.size + math.cos(math.radians(self.rotation + 120)) * self.size,
|
|
self.size + math.sin(math.radians(self.rotation + 120)) * self.size),
|
|
(self.size + math.cos(math.radians(self.rotation + 240)) * self.size,
|
|
self.size + math.sin(math.radians(self.rotation + 240)) * self.size)
|
|
]
|
|
pygame.draw.polygon(surface, color, points)
|
|
screen.blit(surface, (self.x - self.size, self.y - self.size))
|
|
|
|
class Tetris:
|
|
def __init__(self):
|
|
self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
|
|
pygame.display.set_caption('俄罗斯方块')
|
|
|
|
self.clock = pygame.time.Clock()
|
|
self.grid = [[None for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
|
self.current_piece = None
|
|
self.current_shape = None
|
|
self.current_pos = None
|
|
self.held_piece = None
|
|
self.can_hold = True
|
|
self.next_piece = self._get_random_piece()
|
|
self.game_over = False
|
|
self.score = 0
|
|
self.level = 1
|
|
self.lines_cleared = 0
|
|
self.particles = []
|
|
self.fall_speed = INITIAL_FALL_SPEED
|
|
self.current_fall_speed = INITIAL_FALL_SPEED
|
|
self.last_fall_time = time.time()
|
|
self.lock_delay_time = 0
|
|
self.lock_delay_active = False
|
|
self.lock_reset_count = 0
|
|
self.last_move_time = time.time()
|
|
self.paused = False
|
|
self.combo = 0
|
|
self.force_down = False # New flag for forcing piece down
|
|
|
|
# Animation state
|
|
self.animation = AnimationState()
|
|
|
|
# Load high score
|
|
self.high_score = self._load_high_score()
|
|
|
|
# Initialize fonts
|
|
self.font_big = pygame.font.Font(None, 48)
|
|
self.font_small = pygame.font.Font(None, 36)
|
|
|
|
# Load sounds
|
|
self._load_sounds()
|
|
|
|
# Background gradient
|
|
self.background = self._create_background()
|
|
|
|
def _load_sounds(self):
|
|
# Create sounds directory if it doesn't exist
|
|
if not os.path.exists('sounds'):
|
|
os.makedirs('sounds')
|
|
|
|
# Initialize empty/silent sounds
|
|
empty_sound = pygame.mixer.Sound(buffer=bytes([0]*44)) # Minimal silent sound
|
|
self.sounds = {
|
|
'move': empty_sound,
|
|
'rotate': empty_sound,
|
|
'drop': empty_sound,
|
|
'clear': empty_sound,
|
|
'game_over': empty_sound
|
|
}
|
|
|
|
def _create_background(self):
|
|
surface = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT))
|
|
for y in range(WINDOW_HEIGHT):
|
|
progress = y / WINDOW_HEIGHT
|
|
color = (
|
|
int(20 + 20 * math.sin(progress * math.pi)),
|
|
int(10 + 10 * math.sin(progress * math.pi * 2)),
|
|
int(40 + 20 * math.sin(progress * math.pi * 0.5))
|
|
)
|
|
pygame.draw.line(surface, color, (0, y), (WINDOW_WIDTH, y))
|
|
return surface
|
|
|
|
def _load_high_score(self) -> int:
|
|
try:
|
|
if os.path.exists('highscore.txt'):
|
|
with open('highscore.txt', 'r') as f:
|
|
return int(f.read())
|
|
except:
|
|
pass
|
|
return 0
|
|
|
|
def _save_high_score(self):
|
|
with open('highscore.txt', 'w') as f:
|
|
f.write(str(self.high_score))
|
|
|
|
def _get_random_piece(self) -> str:
|
|
return random.choice(list(SHAPES.keys()))
|
|
|
|
def new_piece(self):
|
|
self.current_piece = self.next_piece
|
|
self.next_piece = self._get_random_piece()
|
|
self.current_shape = SHAPES[self.current_piece]
|
|
self.current_pos = [0, GRID_WIDTH//2 - len(self.current_shape[0])//2]
|
|
self.can_hold = True
|
|
|
|
# Reset animation state
|
|
self.animation.move_progress = 0
|
|
self.animation.rotation_progress = 0
|
|
self.animation.last_pos = self.current_pos.copy()
|
|
self.animation.last_shape = [row[:] for row in self.current_shape]
|
|
self.animation.target_pos = self.current_pos.copy()
|
|
self.animation.target_shape = [row[:] for row in self.current_shape]
|
|
|
|
# Check if game over
|
|
if self._check_collision():
|
|
self.game_over = True
|
|
self.sounds['game_over'].play()
|
|
|
|
def hold_piece(self):
|
|
if not self.can_hold:
|
|
return
|
|
|
|
self.sounds['rotate'].play()
|
|
|
|
if self.held_piece is None:
|
|
self.held_piece = self.current_piece
|
|
self.new_piece()
|
|
else:
|
|
self.held_piece, self.current_piece = self.current_piece, self.held_piece
|
|
self.current_shape = SHAPES[self.current_piece]
|
|
self.current_pos = [0, GRID_WIDTH//2 - len(self.current_shape[0])//2]
|
|
|
|
self.can_hold = False
|
|
|
|
def rotate_piece(self, clockwise: bool = True):
|
|
if self.current_piece == 'O':
|
|
return
|
|
|
|
self.sounds['rotate'].play()
|
|
|
|
old_shape = self.current_shape
|
|
self.current_shape = np.rot90(self.current_shape, 1 if not clockwise else -1).tolist()
|
|
|
|
# Update animation state
|
|
self.animation.last_shape = old_shape
|
|
self.animation.target_shape = self.current_shape
|
|
self.animation.rotation_progress = 0
|
|
|
|
if self._check_collision():
|
|
self.current_shape = old_shape
|
|
self.animation.target_shape = old_shape
|
|
|
|
def _check_collision(self) -> bool:
|
|
for y, row in enumerate(self.current_shape):
|
|
for x, cell in enumerate(row):
|
|
if cell:
|
|
grid_y = self.current_pos[0] + y
|
|
grid_x = self.current_pos[1] + x
|
|
|
|
if (grid_x < 0 or grid_x >= GRID_WIDTH or
|
|
grid_y >= GRID_HEIGHT or
|
|
(grid_y >= 0 and self.grid[grid_y][grid_x] is not None)):
|
|
return True
|
|
return False
|
|
|
|
def _get_ghost_position(self) -> List[int]:
|
|
ghost_pos = self.current_pos.copy()
|
|
temp = self.current_pos.copy()
|
|
|
|
while True:
|
|
ghost_pos[0] += 1
|
|
self.current_pos = ghost_pos.copy()
|
|
if self._check_collision():
|
|
ghost_pos[0] -= 1
|
|
self.current_pos = temp
|
|
break
|
|
return ghost_pos
|
|
|
|
def move(self, dx: int, dy: int):
|
|
old_pos = self.current_pos.copy()
|
|
self.current_pos[1] += dx
|
|
self.current_pos[0] += dy
|
|
|
|
if dx != 0:
|
|
self.sounds['move'].play()
|
|
|
|
collision = self._check_collision()
|
|
if collision:
|
|
self.current_pos[1] -= dx
|
|
self.current_pos[0] -= dy
|
|
|
|
if dy > 0: # If moving down caused collision
|
|
if not self.lock_delay_active:
|
|
# Start lock delay when piece first touches ground
|
|
self.lock_delay_active = True
|
|
self.lock_delay_time = time.time()
|
|
self.lock_reset_count = 0
|
|
elif time.time() - self.lock_delay_time > LOCK_DELAY or self.force_down:
|
|
# Lock piece if lock delay expired or forced down
|
|
self._place_piece()
|
|
self._clear_lines()
|
|
self.new_piece()
|
|
self.lock_delay_active = False
|
|
self.force_down = False
|
|
return True
|
|
else:
|
|
# Reset lock delay if piece moved successfully and still touching ground
|
|
if self.lock_delay_active and self.lock_reset_count < MAX_LOCK_RESETS:
|
|
# Check if still touching ground after move
|
|
self.current_pos[0] += 1
|
|
if self._check_collision():
|
|
self.lock_delay_time = time.time()
|
|
self.lock_reset_count += 1
|
|
self.current_pos[0] -= 1
|
|
else:
|
|
# If piece is not touching ground, deactivate lock delay
|
|
self.current_pos[0] += 1
|
|
if not self._check_collision():
|
|
self.lock_delay_active = False
|
|
self.current_pos[0] -= 1
|
|
|
|
# Update animation state for horizontal movement only
|
|
if dx != 0:
|
|
self.animation.last_pos = old_pos
|
|
self.animation.target_pos = self.current_pos.copy()
|
|
self.animation.move_progress = 0
|
|
return False
|
|
|
|
def hard_drop(self):
|
|
self.sounds['drop'].play()
|
|
ghost_pos = self._get_ghost_position()
|
|
self.current_pos = ghost_pos
|
|
self.force_down = True # Force the piece to lock immediately
|
|
self.move(0, 1) # This will trigger the locking process
|
|
|
|
def _place_piece(self):
|
|
for y, row in enumerate(self.current_shape):
|
|
for x, cell in enumerate(row):
|
|
if cell:
|
|
grid_y = self.current_pos[0] + y
|
|
grid_x = self.current_pos[1] + x
|
|
if 0 <= grid_y < GRID_HEIGHT:
|
|
self.grid[grid_y][grid_x] = self.current_piece
|
|
|
|
# Create landing particles
|
|
for x in range(len(self.current_shape[0])):
|
|
color = COLORS[self.current_piece]
|
|
px = (self.current_pos[1] + x) * BLOCK_SIZE
|
|
py = (self.current_pos[0] + len(self.current_shape) - 1) * BLOCK_SIZE
|
|
for _ in range(5):
|
|
self.particles.append(Particle(px, py, color))
|
|
|
|
def _clear_lines(self):
|
|
lines_to_clear = []
|
|
for y in range(GRID_HEIGHT):
|
|
if all(cell is not None for cell in self.grid[y]):
|
|
lines_to_clear.append(y)
|
|
|
|
if not lines_to_clear:
|
|
self.combo = 0
|
|
return
|
|
|
|
self.sounds['clear'].play()
|
|
self.animation.lines_being_cleared = lines_to_clear
|
|
self.animation.line_clear_progress = 0
|
|
self.animation.flash_active = True
|
|
self.animation.flash_progress = 0
|
|
|
|
# Create particles for cleared lines
|
|
for y in lines_to_clear:
|
|
for x in range(GRID_WIDTH):
|
|
color = COLORS[self.grid[y][x]]
|
|
px = x * BLOCK_SIZE
|
|
py = y * BLOCK_SIZE
|
|
for _ in range(5): # 5 particles per block
|
|
self.particles.append(Particle(px, py, color))
|
|
|
|
# Clear lines and update score
|
|
for y in lines_to_clear:
|
|
self.grid.pop(y)
|
|
self.grid.insert(0, [None] * GRID_WIDTH)
|
|
|
|
lines_count = len(lines_to_clear)
|
|
self.lines_cleared += lines_count
|
|
|
|
# Calculate score with combo bonus
|
|
self.combo += 1
|
|
combo_multiplier = min(self.combo, 10) # Cap combo at 10x
|
|
base_score = [100, 300, 500, 800][lines_count - 1]
|
|
self.score += base_score * self.level * combo_multiplier
|
|
|
|
self.level = self.lines_cleared // 10 + 1
|
|
self.fall_speed = max(MIN_FALL_SPEED,
|
|
INITIAL_FALL_SPEED - (self.level - 1) * SPEED_UP_FACTOR)
|
|
|
|
if self.score > self.high_score:
|
|
self.high_score = self.score
|
|
self._save_high_score()
|
|
|
|
def _interpolate_position(self, progress: float) -> List[int]:
|
|
if self.animation.last_pos is None or self.animation.target_pos is None:
|
|
return self.current_pos
|
|
|
|
# Only interpolate horizontal movement
|
|
return [
|
|
self.current_pos[0], # Vertical position is always current
|
|
self.animation.last_pos[1] + (self.animation.target_pos[1] - self.animation.last_pos[1]) * progress
|
|
]
|
|
|
|
def draw(self):
|
|
# Draw background
|
|
self.screen.blit(self.background, (0, 0))
|
|
|
|
# Draw grid
|
|
for y in range(GRID_HEIGHT):
|
|
for x in range(GRID_WIDTH):
|
|
pygame.draw.rect(self.screen, GRAY,
|
|
(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 1)
|
|
|
|
# Draw placed pieces
|
|
for y in range(GRID_HEIGHT):
|
|
for x in range(GRID_WIDTH):
|
|
if self.grid[y][x]:
|
|
if y in self.animation.lines_being_cleared:
|
|
# Skip drawing blocks in lines being cleared during animation
|
|
if self.animation.line_clear_progress < LINE_CLEAR_ANIMATION_TIME:
|
|
continue
|
|
color = COLORS[self.grid[y][x]]
|
|
pygame.draw.rect(self.screen, color,
|
|
(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE))
|
|
pygame.draw.rect(self.screen, WHITE,
|
|
(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 1)
|
|
|
|
# Draw ghost piece
|
|
if self.current_piece:
|
|
ghost_pos = self._get_ghost_position()
|
|
for y, row in enumerate(self.current_shape):
|
|
for x, cell in enumerate(row):
|
|
if cell:
|
|
color = (*COLORS[self.current_piece], 128)
|
|
ghost_x = (ghost_pos[1] + x) * BLOCK_SIZE
|
|
ghost_y = (ghost_pos[0] + y) * BLOCK_SIZE
|
|
surface = pygame.Surface((BLOCK_SIZE, BLOCK_SIZE), pygame.SRCALPHA)
|
|
pygame.draw.rect(surface, color, (0, 0, BLOCK_SIZE, BLOCK_SIZE))
|
|
self.screen.blit(surface, (ghost_x, ghost_y))
|
|
|
|
# Draw current piece with animation
|
|
if self.current_piece:
|
|
pos = self._interpolate_position(min(1, self.animation.move_progress / MOVE_ANIMATION_SPEED))
|
|
|
|
for y, row in enumerate(self.current_shape):
|
|
for x, cell in enumerate(row):
|
|
if cell:
|
|
color = COLORS[self.current_piece]
|
|
block_x = (pos[1] + x) * BLOCK_SIZE
|
|
block_y = (pos[0] + y) * BLOCK_SIZE
|
|
|
|
# Apply rotation animation
|
|
if self.animation.rotation_progress < ROTATION_ANIMATION_SPEED:
|
|
progress = self.animation.rotation_progress / ROTATION_ANIMATION_SPEED
|
|
scale = 1 - math.sin(progress * math.pi) * 0.2
|
|
|
|
# Calculate center of rotation
|
|
center_x = pos[1] * BLOCK_SIZE + len(row) * BLOCK_SIZE / 2
|
|
center_y = pos[0] * BLOCK_SIZE + len(self.current_shape) * BLOCK_SIZE / 2
|
|
|
|
# Adjust block position for rotation
|
|
block_x = center_x + (block_x - center_x) * scale
|
|
block_y = center_y + (block_y - center_y) * scale
|
|
|
|
pygame.draw.rect(self.screen, color,
|
|
(block_x, block_y, BLOCK_SIZE, BLOCK_SIZE))
|
|
pygame.draw.rect(self.screen, WHITE,
|
|
(block_x, block_y, BLOCK_SIZE, BLOCK_SIZE), 1)
|
|
|
|
# Draw side panel
|
|
panel_x = GRID_WIDTH * BLOCK_SIZE + 10
|
|
|
|
# Draw next piece preview
|
|
next_text = self.font_small.render('Next:', True, WHITE)
|
|
self.screen.blit(next_text, (panel_x, 20))
|
|
next_shape = SHAPES[self.next_piece]
|
|
for y, row in enumerate(next_shape):
|
|
for x, cell in enumerate(row):
|
|
if cell:
|
|
color = COLORS[self.next_piece]
|
|
pygame.draw.rect(self.screen, color,
|
|
(panel_x + x * BLOCK_SIZE,
|
|
60 + y * BLOCK_SIZE,
|
|
BLOCK_SIZE, BLOCK_SIZE))
|
|
pygame.draw.rect(self.screen, WHITE,
|
|
(panel_x + x * BLOCK_SIZE,
|
|
60 + y * BLOCK_SIZE,
|
|
BLOCK_SIZE, BLOCK_SIZE), 1)
|
|
|
|
# Draw held piece
|
|
held_text = self.font_small.render('Hold:', True, WHITE)
|
|
self.screen.blit(held_text, (panel_x, 160))
|
|
if self.held_piece:
|
|
held_shape = SHAPES[self.held_piece]
|
|
for y, row in enumerate(held_shape):
|
|
for x, cell in enumerate(row):
|
|
if cell:
|
|
color = COLORS[self.held_piece]
|
|
if not self.can_hold:
|
|
color = tuple(c//2 for c in color) # Darken color
|
|
pygame.draw.rect(self.screen, color,
|
|
(panel_x + x * BLOCK_SIZE,
|
|
200 + y * BLOCK_SIZE,
|
|
BLOCK_SIZE, BLOCK_SIZE))
|
|
pygame.draw.rect(self.screen, WHITE,
|
|
(panel_x + x * BLOCK_SIZE,
|
|
200 + y * BLOCK_SIZE,
|
|
BLOCK_SIZE, BLOCK_SIZE), 1)
|
|
|
|
# Draw score and level
|
|
score_text = self.font_small.render(f'Score: {self.score}', True, WHITE)
|
|
self.screen.blit(score_text, (panel_x, 300))
|
|
|
|
high_score_text = self.font_small.render(f'High: {self.high_score}', True, WHITE)
|
|
self.screen.blit(high_score_text, (panel_x, 340))
|
|
|
|
level_text = self.font_small.render(f'Level: {self.level}', True, WHITE)
|
|
self.screen.blit(level_text, (panel_x, 380))
|
|
|
|
lines_text = self.font_small.render(f'Lines: {self.lines_cleared}', True, WHITE)
|
|
self.screen.blit(lines_text, (panel_x, 420))
|
|
|
|
if self.combo > 1:
|
|
combo_text = self.font_small.render(f'Combo: x{self.combo}', True, WHITE)
|
|
self.screen.blit(combo_text, (panel_x, 460))
|
|
|
|
# Draw particles
|
|
self.particles = [p for p in self.particles if p.update()]
|
|
for particle in self.particles:
|
|
particle.draw(self.screen)
|
|
|
|
# Draw line clear flash effect
|
|
if self.animation.flash_active and self.animation.lines_being_cleared:
|
|
flash_alpha = int(255 * (1 - self.animation.flash_progress / FLASH_SPEED))
|
|
if flash_alpha > 0:
|
|
flash_surface = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA)
|
|
for y in self.animation.lines_being_cleared:
|
|
pygame.draw.rect(flash_surface, (255, 255, 255, flash_alpha),
|
|
(0, y * BLOCK_SIZE, GRID_WIDTH * BLOCK_SIZE, BLOCK_SIZE))
|
|
self.screen.blit(flash_surface, (0, 0))
|
|
|
|
# Draw game over or pause screen
|
|
if self.game_over:
|
|
self._draw_overlay("Game Over! Press R to restart")
|
|
elif self.paused:
|
|
self._draw_overlay("Paused")
|
|
|
|
pygame.display.flip()
|
|
|
|
def _draw_overlay(self, text: str):
|
|
overlay = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT))
|
|
overlay.set_alpha(128)
|
|
overlay.fill(BLACK)
|
|
self.screen.blit(overlay, (0, 0))
|
|
|
|
text_surface = self.font_big.render(text, True, WHITE)
|
|
text_rect = text_surface.get_rect(center=(WINDOW_WIDTH//2, WINDOW_HEIGHT//2))
|
|
self.screen.blit(text_surface, text_rect)
|
|
|
|
def reset(self):
|
|
self.grid = [[None for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
|
self.current_piece = None
|
|
self.current_shape = None
|
|
self.current_pos = None
|
|
self.held_piece = None
|
|
self.can_hold = True
|
|
self.next_piece = self._get_random_piece()
|
|
self.game_over = False
|
|
self.score = 0
|
|
self.level = 1
|
|
self.lines_cleared = 0
|
|
self.particles = []
|
|
self.fall_speed = INITIAL_FALL_SPEED
|
|
self.current_fall_speed = INITIAL_FALL_SPEED
|
|
self.last_fall_time = time.time()
|
|
self.lock_delay_time = 0
|
|
self.lock_delay_active = False
|
|
self.lock_reset_count = 0
|
|
self.combo = 0
|
|
self.animation = AnimationState()
|
|
self.paused = False
|
|
self.new_piece()
|
|
|
|
def update_animations(self, dt: float):
|
|
# Update move animation
|
|
if self.animation.move_progress < MOVE_ANIMATION_SPEED:
|
|
self.animation.move_progress += dt
|
|
|
|
# Update rotation animation
|
|
if self.animation.rotation_progress < ROTATION_ANIMATION_SPEED:
|
|
self.animation.rotation_progress += dt
|
|
|
|
# Update line clear animation
|
|
if self.animation.line_clear_progress < LINE_CLEAR_ANIMATION_TIME:
|
|
self.animation.line_clear_progress += dt
|
|
|
|
# Update flash animation
|
|
if self.animation.flash_active:
|
|
self.animation.flash_progress += dt
|
|
if self.animation.flash_progress >= FLASH_SPEED:
|
|
self.animation.flash_active = False
|
|
self.animation.flash_progress = 0
|
|
|
|
def run(self):
|
|
self.new_piece()
|
|
last_time = time.time()
|
|
|
|
while True:
|
|
current_time = time.time()
|
|
dt = current_time - last_time
|
|
last_time = current_time
|
|
|
|
self.clock.tick(60)
|
|
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
pygame.quit()
|
|
return
|
|
|
|
if event.type == pygame.KEYDOWN:
|
|
if event.key == pygame.K_ESCAPE:
|
|
pygame.quit()
|
|
return
|
|
|
|
if self.game_over:
|
|
if event.key == pygame.K_r:
|
|
self.reset()
|
|
continue
|
|
|
|
if event.key == pygame.K_p:
|
|
self.paused = not self.paused
|
|
continue
|
|
|
|
if self.paused:
|
|
continue
|
|
|
|
if event.key == pygame.K_LEFT:
|
|
self.move(-1, 0)
|
|
elif event.key == pygame.K_RIGHT:
|
|
self.move(1, 0)
|
|
elif event.key == pygame.K_DOWN:
|
|
self.current_fall_speed = SOFT_DROP_SPEED
|
|
elif event.key == pygame.K_UP:
|
|
self.rotate_piece()
|
|
elif event.key == pygame.K_z:
|
|
self.rotate_piece(False)
|
|
elif event.key == pygame.K_SPACE:
|
|
self.hard_drop()
|
|
elif event.key == pygame.K_c:
|
|
self.hold_piece()
|
|
|
|
elif event.type == pygame.KEYUP:
|
|
if event.key == pygame.K_DOWN:
|
|
self.current_fall_speed = self.fall_speed
|
|
|
|
if not self.game_over and not self.paused:
|
|
# Update animations
|
|
self.update_animations(dt)
|
|
|
|
# Handle automatic falling
|
|
if current_time - self.last_fall_time > self.current_fall_speed:
|
|
self.move(0, 1) # Move down one grid
|
|
self.last_fall_time = current_time
|
|
|
|
self.draw()
|
|
|
|
if __name__ == '__main__':
|
|
game = Tetris()
|
|
game.run() |