Initial commit

This commit is contained in:
2025-11-05 14:14:10 +08:00
commit 8332a821b3
10 changed files with 568 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

541
main.py Normal file
View File

@@ -0,0 +1,541 @@
# main.py (增强版复古俄罗斯方块)
# 版本: 2.5 (稳定版)
# 开发工程师: aistudio Gemini
# 日期: 2025-06-23
# 更新日志:
# - [移除] 为保证游戏稳定性,彻底移除了导致按键锁死的“暂存(Hold)”功能。
# - [修复] 将所有可配置参数整合到顶部的“配置中心”,方便用户自定义。
# - [调整] 增加了游戏结束画面按钮的垂直间距,优化布局。
# - [修复] 再次重构事件处理逻辑,彻底解决主菜单和游戏内侧边栏按钮无法点击的问题。
# - [修复] 将“影子”按钮的文字改为英文,以解决因字体不支持中文而导致的乱码问题。
# - [新增] 增加“影子方块”开关功能。
# - [调整] 将“旋转”功能改回空格键,“硬降落”移至“上箭头(↑)”键。
import pygame
import random
import sys
import os
from collections import deque
# =================================================================================
# ====================== ⭐️ 游戏核心配置中心 (可在此处调整) ⭐️ =====================
# =================================================================================
CONFIG = {
# --- 界面文本与字体 (UI Text & Fonts) ---
"title_text": "Retro Tetris",
"font_sizes": {
"large": 26, # 大号字体,用于标题
"medium": 24, # 中号字体 (未使用,备用)
"small": 16, # 小号字体用于UI文本
},
# --- 布局与尺寸 (Layout & Sizing) ---
"scale": 1.2, # 全局缩放比例
"base_block_size": 30, # 方块的基础像素尺寸
"sidebar_width": 220, # 侧边栏宽度
"padding": 30, # 窗口内边距
"gap_between_areas": 30, # 游戏区与侧边栏的间距
"game_over_buttons_y_offset": 80, # 游戏结束画面按钮与上方文字的垂直距离
# --- 游戏难度与手感 (Gameplay & Feel) ---
"initial_fall_speed": 800, # 初始下落速度(毫秒),数字越大越慢
"speed_increase_per_level": 70, # 每升一级,速度加快多少毫秒
"lock_delay": 500, # 方块触底后的锁定延迟(毫秒)
"move_sideways_delay": 180, # 长按左右键的初始延迟(毫秒)
"move_sideways_interval": 40, # 长按左右键的移动间隔(毫秒)
}
# =================================================================================
# --- 从配置中心计算全局常量 ---
SCALE = CONFIG['scale']
GRID_WIDTH, GRID_HEIGHT = 10, 20
BASE_BLOCK_SIZE = CONFIG['base_block_size']
BLOCK_SIZE = int(BASE_BLOCK_SIZE * SCALE)
SIDEBAR_WIDTH = int(CONFIG['sidebar_width'] * SCALE)
PADDING = int(CONFIG['padding'] * SCALE)
GAP_BETWEEN_AREAS = int(CONFIG['gap_between_areas'] * SCALE)
GAME_AREA_WIDTH = GRID_WIDTH * BLOCK_SIZE
GAME_AREA_HEIGHT = GRID_HEIGHT * BLOCK_SIZE
SCREEN_WIDTH = GAME_AREA_WIDTH + SIDEBAR_WIDTH + PADDING * 2 + GAP_BETWEEN_AREAS
SCREEN_HEIGHT = GAME_AREA_HEIGHT + PADDING * 2
GAME_AREA_X, GAME_AREA_Y = PADDING, PADDING
# --- 颜色定义 ---
BLACK = (20, 20, 30)
WHITE = (224, 224, 224)
GRID_COLOR = (40, 40, 50)
GHOST_COLOR_ALPHA = 80
BUTTON_COLOR = (45, 45, 60)
BUTTON_HOVER_COLOR = (80, 80, 100)
OVERLAY_COLOR = (0, 0, 0, 180)
# --- 方块与计分定义 ---
TETROMINO_DATA = {
'I': {'shape': [[1, 1, 1, 1]], 'color': (30, 180, 210)},
'O': {'shape': [[1, 1], [1, 1]], 'color': (230, 200, 40)},
'T': {'shape': [[0, 1, 0], [1, 1, 1]], 'color': (180, 60, 220)},
'J': {'shape': [[1, 0, 0], [1, 1, 1]], 'color': (50, 100, 230)},
'L': {'shape': [[0, 0, 1], [1, 1, 1]], 'color': (220, 130, 30)},
'S': {'shape': [[0, 1, 1], [1, 1, 0]], 'color': (80, 200, 90)},
'Z': {'shape': [[1, 1, 0], [0, 1, 1]], 'color': (220, 50, 80)}
}
SCORE_VALUES = {0: 0, 1: 100, 2: 300, 3: 500, 4: 800}
LINES_PER_LEVEL = 10
# --- 资源路径处理 ---
def resource_path(relative_path):
try: base_path = sys._MEIPASS
except Exception: base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# =================================================================================
# ======================== ⭐️ 资源加载模块 ⭐️ ===========================
# =================================================================================
class Assets:
def __init__(self):
self.large_font = self._load_font(CONFIG['font_sizes']['large'])
self.medium_font = self._load_font(CONFIG['font_sizes']['medium'])
self.small_font = self._load_font(CONFIG['font_sizes']['small'])
self.sounds = self._load_sounds()
self.icon = self._load_icon()
self.set_sound_volumes()
def _load_font(self, size):
font_path = resource_path(os.path.join('assets', 'fonts', 'press-start-2p.ttf'))
try:
return pygame.font.Font(font_path, int(size * SCALE))
except pygame.error:
print(f"Warning: Font file not found at {font_path}. Using default font.")
return pygame.font.Font(None, int(size * SCALE * 1.2))
def _load_sounds(self):
sounds = {}
sound_files = {
'drop': 'block-1-328874.mp3', 'clear': 'clear-bell-notification-sound-351709.mp3',
'level_up': 'game-level-complete-143022.mp3', 'game_over': 'game-over-arcade-6435.mp3'
}
try:
pygame.mixer.music.load(resource_path(os.path.join('assets', 'sounds', 'game-music-loop-6-144641.mp3')))
for name, filename in sound_files.items():
sounds[name] = pygame.mixer.Sound(resource_path(os.path.join('assets', 'sounds', filename)))
return sounds
except pygame.error as e:
print(f"Warning: Could not load sounds. {e}. Game will be silent.")
class DummySound:
def play(self): pass
def set_volume(self, v): pass
return {name: DummySound() for name in sound_files.keys()}
def _load_icon(self):
try: return pygame.image.load(resource_path('favicon.ico'))
except pygame.error: print("Warning: 'favicon.ico' not found."); return None
def set_sound_volumes(self):
self.volumes = {'music': 0.3, 'drop': 0.5, 'clear': 0.7, 'level_up': 0.8, 'game_over': 1.0}
if pygame.mixer.get_init():
pygame.mixer.music.set_volume(self.volumes['music'])
for name, sound in self.sounds.items(): sound.set_volume(self.volumes[name])
# =================================================================================
# ======================== ⭐️ 游戏核心逻辑类 ⭐️ =========================
# =================================================================================
class Piece:
def __init__(self, shape_name):
self.shape_name = shape_name
data = TETROMINO_DATA[shape_name]
self.matrix = data['shape']
self.color = data['color']
self.x = GRID_WIDTH // 2 - len(self.matrix[0]) // 2
self.y = 0
def rotate(self): self.matrix = list(zip(*self.matrix[::-1]))
class PieceGenerator:
def __init__(self): self.bag = []; self.refill_bag()
def refill_bag(self): self.bag = list(TETROMINO_DATA.keys()); random.shuffle(self.bag)
def next(self):
if not self.bag: self.refill_bag()
return Piece(self.bag.pop())
class Grid:
def __init__(self): self.grid = [[BLACK for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
def is_valid_position(self, piece, ox=0, oy=0):
for y, row in enumerate(piece.matrix):
for x, cell in enumerate(row):
if cell:
new_x, new_y = piece.x + x + ox, piece.y + y + oy
if not (0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT and self.grid[new_y][new_x] == BLACK):
return False
return True
def lock_piece(self, piece):
for y, row in enumerate(piece.matrix):
for x, cell in enumerate(row):
if cell: self.grid[piece.y + y][piece.x + x] = piece.color
def clear_lines(self):
lines_to_clear = [i for i, row in enumerate(self.grid) if all(cell != BLACK for cell in row)]
if lines_to_clear:
for i in lines_to_clear:
del self.grid[i]; self.grid.insert(0, [BLACK for _ in range(GRID_WIDTH)])
return len(lines_to_clear)
def reset(self): self.grid = [[BLACK for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
class ScoreManager:
def __init__(self): self.reset()
def reset(self):
self.score = 0; self.level = 1; self.lines_cleared = 0
self.fall_speed = CONFIG['initial_fall_speed']
def update(self, cleared_lines):
leveled_up = False
self.score += SCORE_VALUES.get(cleared_lines, 0) * self.level
self.lines_cleared += cleared_lines
new_level = 1 + self.lines_cleared // LINES_PER_LEVEL
if new_level > self.level:
self.level = new_level
self.fall_speed = max(100, CONFIG['initial_fall_speed'] - (self.level - 1) * CONFIG['speed_increase_per_level'])
leveled_up = True
return leveled_up
# =================================================================================
# ======================== ⭐️ UI 与渲染类 ⭐️ ===========================
# =================================================================================
class Button:
def __init__(self, text, pos, size, font, callback):
self.rect = pygame.Rect(pos, size)
self.text = text; self.font = font; self.callback = callback
self.is_hovered = False
def draw(self, screen):
color = BUTTON_HOVER_COLOR if self.is_hovered else BUTTON_COLOR
pygame.draw.rect(screen, color, self.rect, border_radius=int(8 * SCALE))
surf = self.font.render(self.text, True, WHITE)
screen.blit(surf, surf.get_rect(center=self.rect.center))
def handle_event(self, event):
if event.type == pygame.MOUSEMOTION:
self.is_hovered = self.rect.collidepoint(event.pos)
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and self.is_hovered:
if self.callback: self.callback()
class UIManager:
def __init__(self, game, assets):
self.game = game; self.assets = assets
self.buttons = {}; self._setup_buttons()
def _setup_buttons(self):
btn_w, btn_h = int(180 * SCALE), int(45 * SCALE)
sidebar_x = GAME_AREA_X + GAME_AREA_WIDTH + GAP_BETWEEN_AREAS
btn_x = sidebar_x + (SIDEBAR_WIDTH - btn_w) // 2
btn_gap = int(55 * SCALE)
# --- [移除] 暂存功能后,按钮数量减少,重新布局 ---
btn_y_start = SCREEN_HEIGHT - PADDING - (btn_h + int(10*SCALE)) * 4
self.buttons['in_game'] = [
Button("Pause", (btn_x, btn_y_start), (btn_w, btn_h), self.assets.small_font, self.game.toggle_pause),
Button("Restart", (btn_x, btn_y_start + btn_gap), (btn_w, btn_h), self.assets.small_font, self.game.reset),
Button("Mute", (btn_x, btn_y_start + btn_gap * 2), (btn_w, btn_h), self.assets.small_font, self.game.toggle_mute),
Button("Ghost:Off", (btn_x, btn_y_start + btn_gap * 3), (btn_w, btn_h), self.assets.small_font, self.game.toggle_ghost)
]
menu_btn_x = SCREEN_WIDTH / 2 - btn_w / 2
self.buttons['menu'] = [
Button("Start Game", (menu_btn_x, SCREEN_HEIGHT / 2), (btn_w, btn_h), self.assets.small_font, self.game.start_game),
Button("Quit", (menu_btn_x, SCREEN_HEIGHT / 2 + btn_gap), (btn_w, btn_h), self.assets.small_font, sys.exit)
]
self.buttons['paused'] = [
Button("Resume", (menu_btn_x, SCREEN_HEIGHT / 2 - btn_gap), (btn_w, btn_h), self.assets.small_font, self.game.toggle_pause),
Button("Restart", (menu_btn_x, SCREEN_HEIGHT / 2), (btn_w, btn_h), self.assets.small_font, self.game.reset),
Button("Quit", (menu_btn_x, SCREEN_HEIGHT / 2 + btn_gap), (btn_w, btn_h), self.assets.small_font, sys.exit)
]
game_over_btn_y = SCREEN_HEIGHT/2 + int(CONFIG['game_over_buttons_y_offset'] * SCALE)
self.buttons['game_over'] = [
Button("Restart", (menu_btn_x, game_over_btn_y), (btn_w, btn_h), self.assets.small_font, self.game.reset),
Button("Main Menu", (menu_btn_x, game_over_btn_y + btn_gap), (btn_w, btn_h), self.assets.small_font, self.game.go_to_menu)
]
def get_buttons(self, state): return self.buttons.get(state, [])
class Renderer:
def __init__(self, screen, assets):
self.screen = screen; self.assets = assets
self.crt_scanline_surface = self._create_crt_scanline_surface()
def _create_crt_scanline_surface(self):
w, h = self.screen.get_size()
scanline_surface = pygame.Surface((w, h), pygame.SRCALPHA)
for y in range(0, h, int(4 * SCALE)):
pygame.draw.line(scanline_surface, (0, 0, 0, 40), (0, y), (w, y), int(2 * SCALE))
return scanline_surface
def draw(self, game):
self.screen.fill(BLACK)
if game.state == "menu": self.draw_main_menu(game)
else:
self.draw_game_area(game.grid)
self.draw_piece(game.current_piece)
if game.show_ghost: self.draw_ghost_piece(game)
self.draw_sidebar(game)
if game.state == "paused": self.draw_overlay("PAUSED", game.ui_manager.get_buttons('paused'))
elif game.state == "game_over": self.draw_game_over_overlay(game)
self.screen.blit(self.crt_scanline_surface, (0, 0))
pygame.display.flip()
def draw_piece(self, piece, ghost=False):
if not piece: return
for y, row in enumerate(piece.matrix):
for x, cell in enumerate(row):
if cell:
rect = (GAME_AREA_X + (piece.x + x) * BLOCK_SIZE, GAME_AREA_Y + (piece.y + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
if ghost:
surf = pygame.Surface((BLOCK_SIZE, BLOCK_SIZE), pygame.SRCALPHA)
surf.fill((*piece.color, GHOST_COLOR_ALPHA))
self.screen.blit(surf, rect[:2])
else:
pygame.draw.rect(self.screen, piece.color, rect)
pygame.draw.rect(self.screen, WHITE, rect, 1)
def draw_ghost_piece(self, game):
if not game.current_piece: return
ghost = Piece(game.current_piece.shape_name)
ghost.matrix = game.current_piece.matrix
ghost.x = game.current_piece.x; ghost.y = game.current_piece.y
ghost.color = game.current_piece.color
while game.grid.is_valid_position(ghost, oy=1): ghost.y += 1
self.draw_piece(ghost, ghost=True)
def draw_game_area(self, grid_obj):
for y, row in enumerate(grid_obj.grid):
for x, color in enumerate(row):
if color != BLACK:
rect = (GAME_AREA_X + x * BLOCK_SIZE, GAME_AREA_Y + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
pygame.draw.rect(self.screen, color, rect); pygame.draw.rect(self.screen, WHITE, rect, 1)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
rect = (GAME_AREA_X + x * BLOCK_SIZE, GAME_AREA_Y + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
pygame.draw.rect(self.screen, GRID_COLOR, rect, 1)
def draw_sidebar(self, game):
sidebar_x = GAME_AREA_X + GAME_AREA_WIDTH + GAP_BETWEEN_AREAS
sidebar_center_x = sidebar_x + SIDEBAR_WIDTH // 2
def draw_info(label, value, y_pos):
label_surf = self.assets.small_font.render(label, True, WHITE)
self.screen.blit(label_surf, label_surf.get_rect(centerx=sidebar_center_x, top=y_pos))
value_surf = self.assets.small_font.render(str(value), True, WHITE)
self.screen.blit(value_surf, value_surf.get_rect(centerx=sidebar_center_x, top=y_pos + int(25 * SCALE)))
draw_info("SCORE", game.score_manager.score, GAME_AREA_Y + int(40*SCALE))
draw_info("LEVEL", game.score_manager.level, GAME_AREA_Y + int(110*SCALE))
draw_info("LINES", game.score_manager.lines_cleared, GAME_AREA_Y + int(180*SCALE))
next_title_y = GAME_AREA_Y + int(260*SCALE)
next_label_surf = self.assets.small_font.render("NEXT", True, WHITE)
self.screen.blit(next_label_surf, next_label_surf.get_rect(centerx=sidebar_center_x, top=next_title_y))
if game.next_piece: self.draw_sidebar_piece(game.next_piece, sidebar_center_x, next_title_y + int(25*SCALE))
# --- [移除] 移除暂存方块的绘制逻辑 ---
for btn in game.ui_manager.get_buttons('in_game'): btn.draw(self.screen)
def draw_sidebar_piece(self, piece, center_x, top_y):
matrix = piece.matrix
piece_w_pixels = len(matrix[0]) * BLOCK_SIZE
start_x = center_x - piece_w_pixels // 2
for y, row in enumerate(matrix):
for x, cell in enumerate(row):
if cell:
rect = (start_x + x * BLOCK_SIZE, top_y + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
pygame.draw.rect(self.screen, piece.color, rect); pygame.draw.rect(self.screen, WHITE, rect, 1)
def draw_overlay(self, text, buttons):
overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA); overlay.fill(OVERLAY_COLOR)
self.screen.blit(overlay, (0, 0))
main_surf = self.assets.large_font.render(text, True, WHITE)
self.screen.blit(main_surf, main_surf.get_rect(center=(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 3)))
for btn in buttons: btn.draw(self.screen)
def draw_game_over_overlay(self, game):
overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA); overlay.fill(OVERLAY_COLOR)
self.screen.blit(overlay, (0, 0))
title_surf = self.assets.large_font.render("GAME OVER", True, WHITE)
self.screen.blit(title_surf, title_surf.get_rect(center=(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 4)))
y_start = SCREEN_HEIGHT / 4 + int(100*SCALE); line_height = int(40*SCALE)
def draw_stat(label, value, y):
text = f"{label}: {value}"
surf = self.assets.small_font.render(text, True, WHITE)
self.screen.blit(surf, surf.get_rect(centerx=SCREEN_WIDTH / 2, top=y))
draw_stat("Final Score", game.score_manager.score, y_start)
draw_stat("Level Reached", game.score_manager.level, y_start + line_height)
draw_stat("Lines Cleared", game.score_manager.lines_cleared, y_start + line_height * 2)
for btn in game.ui_manager.get_buttons('game_over'): btn.draw(self.screen)
def draw_main_menu(self, game):
surf = self.assets.large_font.render(CONFIG['title_text'], True, WHITE)
self.screen.blit(surf, surf.get_rect(center=(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 3)))
for btn in game.ui_manager.get_buttons('menu'): btn.draw(self.screen)
# =================================================================================
# ======================== ⭐️ 游戏主控类 ⭐️ ===========================
# =================================================================================
class TetrisGame:
def __init__(self):
pygame.init(); pygame.mixer.init()
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption(f"{CONFIG['title_text']} - 复古方块 - 像素风俄罗斯方块挑战")
self.assets = Assets()
if self.assets.icon: pygame.display.set_icon(self.assets.icon)
self.clock = pygame.time.Clock(); self.state = "menu"
self.grid = Grid(); self.score_manager = ScoreManager()
self.piece_generator = PieceGenerator(); self.renderer = Renderer(self.screen, self.assets)
self.ui_manager = UIManager(self, self.assets)
self.fall_time = 0; self.lock_timer = None
self.current_piece = None; self.next_piece = None
# --- [移除] 移除暂存功能相关的状态变量 ---
self.is_muted = False; self.show_ghost = False
self.move_sideways_time = 0; self.key_down_time = {'left': 0, 'right': 0}
def reset(self):
self.grid.reset(); self.score_manager.reset(); self.piece_generator.refill_bag()
self.current_piece = self.piece_generator.next(); self.next_piece = self.piece_generator.next()
# --- [移除] 移除暂存功能相关的重置逻辑 ---
self.fall_time = pygame.time.get_ticks(); self.lock_timer = None
self.state = "playing"
if not self.is_muted and pygame.mixer.get_init(): pygame.mixer.music.play(-1)
def start_game(self): self.reset()
def go_to_menu(self): self.state = "menu"; pygame.mixer.music.stop() if pygame.mixer.get_init() else None
def toggle_pause(self):
if self.state == "playing": self.state = "paused"; pygame.mixer.music.pause() if pygame.mixer.get_init() else None
elif self.state == "paused": self.state = "playing"; pygame.mixer.music.unpause() if not self.is_muted and pygame.mixer.get_init() else None
def toggle_mute(self):
self.is_muted = not self.is_muted
self.ui_manager.buttons['in_game'][2].text = "Unmute" if self.is_muted else "Mute"
if pygame.mixer.get_init():
music_vol = self.assets.volumes['music'] if not self.is_muted else 0
pygame.mixer.music.set_volume(music_vol)
for name, sound in self.assets.sounds.items(): sound.set_volume(self.assets.volumes[name] if not self.is_muted else 0)
def toggle_ghost(self):
self.show_ghost = not self.show_ghost
self.ui_manager.buttons['in_game'][3].text = "Ghost: On" if self.show_ghost else "Ghost: Off"
def move(self, dx, dy):
if not self.current_piece: return False
if self.grid.is_valid_position(self.current_piece, dx, dy):
self.current_piece.x += dx; self.current_piece.y += dy
self.reset_lock_delay(); return True
return False
def rotate(self):
if not self.current_piece: return
original_matrix = self.current_piece.matrix
self.current_piece.rotate()
if not self.grid.is_valid_position(self.current_piece):
if self.grid.is_valid_position(self.current_piece, ox=1): self.current_piece.x += 1
elif self.grid.is_valid_position(self.current_piece, ox=-1): self.current_piece.x -= 1
else: self.current_piece.matrix = original_matrix; return
self.reset_lock_delay()
def hard_drop(self):
if not self.current_piece: return
while self.grid.is_valid_position(self.current_piece, oy=1): self.current_piece.y += 1
self.lock_piece()
# --- [移除] 彻底移除 hold 方法 ---
def lock_piece(self):
self.grid.lock_piece(self.current_piece)
if self.assets.sounds.get('drop'): self.assets.sounds['drop'].play()
cleared_lines = self.grid.clear_lines()
if cleared_lines > 0:
if self.assets.sounds.get('clear'): self.assets.sounds['clear'].play()
if self.score_manager.update(cleared_lines):
if self.assets.sounds.get('level_up'): self.assets.sounds['level_up'].play()
self.current_piece = self.next_piece
self.next_piece = self.piece_generator.next()
self.lock_timer = None
if not self.grid.is_valid_position(self.current_piece):
self.state = "game_over"
if pygame.mixer.get_init():
pygame.mixer.music.stop()
if self.assets.sounds.get('game_over'): self.assets.sounds['game_over'].play()
def reset_lock_delay(self):
if self.current_piece and not self.grid.is_valid_position(self.current_piece, oy=1):
self.lock_timer = pygame.time.get_ticks()
def run(self):
while True:
now = pygame.time.get_ticks()
self.handle_events(now)
if self.state == "playing": self.update(now)
self.renderer.draw(self)
self.clock.tick(60)
def handle_events(self, now):
if self.state == "playing":
keys = pygame.key.get_pressed()
if keys[pygame.K_DOWN] or keys[pygame.K_s]: self.move(0, 1)
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
if now - self.key_down_time.get('left', 0) > CONFIG['move_sideways_delay'] and now - self.move_sideways_time > CONFIG['move_sideways_interval']:
self.move(-1, 0); self.move_sideways_time = now
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
if now - self.key_down_time.get('right', 0) > CONFIG['move_sideways_delay'] and now - self.move_sideways_time > CONFIG['move_sideways_interval']:
self.move(1, 0); self.move_sideways_time = now
for event in pygame.event.get():
if event.type == pygame.QUIT: pygame.quit(); sys.exit()
if self.state == 'menu': self.handle_menu_events(event)
elif self.state == 'playing': self.handle_playing_events(event, now)
elif self.state == 'paused': self.handle_paused_events(event)
elif self.state == 'game_over': self.handle_game_over_events(event)
def handle_menu_events(self, event):
for btn in self.ui_manager.get_buttons('menu'): btn.handle_event(event)
def handle_playing_events(self, event, now):
for btn in self.ui_manager.get_buttons('in_game'): btn.handle_event(event)
if event.type == pygame.KEYDOWN:
if event.key in [pygame.K_LEFT, pygame.K_a]: self.move(-1, 0); self.key_down_time['left'] = now
elif event.key in [pygame.K_RIGHT, pygame.K_d]: self.move(1, 0); self.key_down_time['right'] = now
elif event.key in [pygame.K_SPACE, pygame.K_x, pygame.K_w]: self.rotate()
elif event.key == pygame.K_UP: self.hard_drop()
# --- [移除] 移除C键暂存的事件绑定 ---
elif event.key == pygame.K_ESCAPE: self.toggle_pause()
if event.type == pygame.KEYUP:
if event.key in [pygame.K_LEFT, pygame.K_a]: self.key_down_time['left'] = 0
if event.key in [pygame.K_RIGHT, pygame.K_d]: self.key_down_time['right'] = 0
def handle_paused_events(self, event):
for btn in self.ui_manager.get_buttons('paused'): btn.handle_event(event)
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: self.toggle_pause()
def handle_game_over_events(self, event):
for btn in self.ui_manager.get_buttons('game_over'): btn.handle_event(event)
if event.type == pygame.KEYDOWN and event.key in [pygame.K_r, pygame.K_RETURN]: self.reset()
def update(self, now):
if self.current_piece is None: return
if now - self.fall_time > self.score_manager.fall_speed:
self.fall_time = now
if not self.move(0, 1):
if self.lock_timer is None: self.lock_timer = now
if self.lock_timer and now - self.lock_timer > CONFIG['lock_delay']:
if not self.grid.is_valid_position(self.current_piece, oy=1): self.lock_piece()
if __name__ == '__main__':
game = TetrisGame()
game.run()

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
pyinstaller
pygame

25
run.bat Normal file
View File

@@ -0,0 +1,25 @@
@echo off
chcp 65001 >nul
echo --------------------------------------------------
echo <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E2BBB7><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD><EFBFBD><EFBFBD>...
if exist venv (
echo <20><><EFBFBD><EFBFBD><E2BBB7><EFBFBD>Ѵ<EFBFBD><D1B4>ڡ<EFBFBD>
) else (
echo <20><><EFBFBD><EFBFBD><E2BBB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><DAA3><EFBFBD><EFBFBD>ڴ<EFBFBD><DAB4><EFBFBD>...
python -m venv venv
echo <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E2BBB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ<EFBFBD><D7B0><EFBFBD><EFBFBD>...
call venv\Scripts\activate.bat
pip install -r requirements.txt
goto run_script
)
echo <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E2BBB7>...
call venv\Scripts\activate.bat
:run_script
echo <20><><EFBFBD><EFBFBD>ת<EFBFBD><D7AA><EFBFBD>ű<EFBFBD>...
python main.py
echo --------------------------------------------------
echo <20><><EFBFBD>ɣ<EFBFBD>
pause