用Python和PyQt5构建俄罗斯方块AI:穷举搜索算法实战指南

俄罗斯方块作为经典游戏,其AI实现一直是编程爱好者探索的热门领域。本文将带您从零开始,使用Python和PyQt5构建一个基于穷举搜索算法的俄罗斯方块AI系统。不同于简单的代码讲解,我们将重点关注工程化实现过程中的关键决策点和实战技巧。

1. 环境配置与项目架构

在开始编码前,需要确保开发环境正确配置。推荐使用Python 3.8+版本,并创建独立的虚拟环境:

python -m venv tetris_ai_env
source tetris_ai_env/bin/activate  # Linux/Mac
# 或
tetris_ai_env\Scripts\activate  # Windows

安装必要的依赖库:

pip install pyqt5 numpy

项目采用模块化设计,分为三个核心文件:

  • tetris_model.py :游戏状态管理与逻辑处理
  • tetris_ai.py :穷举搜索算法实现
  • tetris_game.py :PyQt5界面与主程序

这种分离使得算法、业务逻辑和界面展示各司其职,便于后期维护和扩展。

2. 游戏模型构建

tetris_model.py 中,我们首先定义游戏的核心数据结构。俄罗斯方块的标准游戏区域为10列×20行,使用二维数组表示:

import numpy as np

class BoardData:
    def __init__(self):
        self.width = 10
        self.height = 20
        self.reset()
        
    def reset(self):
        self.board = np.full((self.height, self.width), 
                           fill_value=Shape.shapeNone,
                           dtype=np.int8)

方块形状采用经典的7种俄罗斯方块表示法(I、O、T、L、J、S、Z)。每种形状有1-4种旋转状态,通过坐标偏移量定义:

class Shape:
    shapeNone = 0
    shapeI = 1
    shapeO = 2
    # ...其他形状定义
    
    @staticmethod
    def get_coords(shape_type, rotation):
        """返回指定形状在给定旋转状态下的坐标"""
        coords_table = {
            Shape.shapeI: [
                [(0, -1), (0, 0), (0, 1), (0, 2)],
                [(-1, 0), (0, 0), (1, 0), (2, 0)]
            ],
            # 其他形状坐标定义...
        }
        return coords_table.get(shape_type, [])[rotation % len(coords_table.get(shape_type, []))]

注意:形状坐标采用相对中心点的偏移表示,便于旋转计算

3. 穷举搜索算法实现

穷举搜索算法的核心思想是评估当前方块和下一个方块所有可能的放置组合,选择最优解。在 tetris_ai.py 中实现:

class TetrisAI:
    def next_move(self):
        if not self.current_shape:
            return None
            
        best_score = -float('inf')
        best_move = None
        
        # 遍历当前方块所有可能方向
        for rotation in self.get_valid_rotations(self.current_shape):
            # 遍历所有可放置的x位置
            for x in self.get_valid_x_positions(self.current_shape, rotation):
                # 模拟放置当前方块
                temp_board = self.simulate_drop(self.current_shape, rotation, x)
                
                # 遍历下一个方块所有可能方向
                for next_rot in self.get_valid_rotations(self.next_shape):
                    # 遍历下一个方块所有可放置的x位置
                    for next_x in self.get_valid_x_positions(self.next_shape, next_rot):
                        # 计算综合评分
                        score = self.evaluate_position(temp_board, self.next_shape, next_rot, next_x)
                        
                        if score > best_score:
                            best_score = score
                            best_move = (rotation, x)
        
        return best_move

评分函数的设计直接影响AI的表现。我们综合考虑以下因素:

评分因素 权重 说明
消行数 1.8 消除的行数越多越好
空洞数 -1.0 避免出现无法填补的空洞
堆叠高度 -0.02 防止堆叠过高
表面平滑度 -0.2 保持表面相对平整
def evaluate_position(self, board, shape, rotation, x):
    # 模拟放置方块
    temp_board = self.simulate_drop(shape, rotation, x, board.copy())
    
    # 计算各项指标
    lines_cleared = self.count_lines_cleared(temp_board)
    holes = self.count_holes(temp_board)
    height = self.get_max_height(temp_board)
    roughness = self.get_surface_roughness(temp_board)
    
    # 综合评分
    score = (lines_cleared * 1.8 
             - holes * 1.0 
             - (height ** 1.5) * 0.02 
             - roughness * 0.2)
    
    return score

4. PyQt5界面集成

tetris_game.py 负责将算法与界面连接。我们创建一个继承自QMainWindow的主窗口:

class TetrisGame(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.model = BoardData()
        self.ai = TetrisAI(self.model)
        
    def initUI(self):
        self.setWindowTitle('俄罗斯方块AI')
        self.setFixedSize(800, 600)
        
        # 游戏区域
        self.game_canvas = QLabel(self)
        self.game_canvas.setGeometry(50, 50, 300, 600)
        
        # 控制按钮
        self.ai_toggle = QPushButton('AI开关', self)
        self.ai_toggle.setGeometry(400, 50, 100, 30)
        self.ai_toggle.clicked.connect(self.toggle_ai)
        
        # 定时器控制游戏节奏
        self.timer = QTimer()
        self.timer.timeout.connect(self.game_loop)
        self.timer.start(500)  # 每500ms更新一次

游戏主循环处理用户输入和AI决策:

def game_loop(self):
    if self.ai_enabled:
        move = self.ai.next_move()
        if move:
            rotation, x = move
            self.model.rotate_current(rotation)
            self.model.move_current(x)
    
    self.model.drop_down()
    self.update_display()
    
    if self.model.check_game_over():
        self.game_over()

5. 性能优化与调试技巧

穷举搜索算法在原始实现中可能存在性能问题,特别是当考虑多层预判时。以下是几个优化方向:

  1. 搜索空间剪枝

    • 限制旋转方向的尝试次数
    • 跳过明显不合理的x位置
    def get_valid_x_positions(self, shape, rotation):
        min_x, max_x = self.get_x_bounds(shape, rotation)
        return range(min_x, max_x + 1, 2)  # 每隔2个位置尝试一次
    
  2. 并行计算 : 使用多进程加速评分计算:

    from concurrent.futures import ProcessPoolExecutor
    
    def evaluate_all_positions(self, positions):
        with ProcessPoolExecutor() as executor:
            results = list(executor.map(self.evaluate_single_position, positions))
        return max(results, key=lambda x: x[1])
    
  3. 缓存计算结果 : 对重复出现的板面状态进行缓存:

    from functools import lru_cache
    
    @lru_cache(maxsize=1000)
    def evaluate_position(self, board_hash, shape_type, rotation, x):
        board = self.unhash_board(board_hash)
        # ...原有计算逻辑
    

常见问题及解决方案:

  • PyQt5界面卡顿 : 将耗时计算移到子线程,通过信号槽更新UI:

    class AIWorker(QObject):
        finished = pyqtSignal(tuple)
        
        def run_ai(self, model):
            move = model.ai.next_move()
            self.finished.emit(move)
    
  • AI决策不合理 : 调整评分函数权重,例如增加对"井"结构的惩罚:

    def evaluate_position(self, board, shape, rotation, x):
        # ...原有计算
        wells = self.count_wells(temp_board)
        score -= wells * 0.5  # 惩罚井结构
        return score
    

6. 进阶扩展方向

基础实现完成后,可以考虑以下扩展:

  1. 机器学习调参 : 使用遗传算法自动优化评分函数权重:

    def evolve_weights(population_size=50, generations=100):
        # 初始化种群
        population = [random_weights() for _ in range(population_size)]
        
        for _ in range(generations):
            # 评估适应度
            fitness = [evaluate_ai(weights) for weights in population]
            # 选择、交叉、变异
            population = evolve(population, fitness)
        
        return best_weights(population)
    
  2. 预测更多方块 : 修改搜索算法考虑后续多个方块:

    def next_move(self, lookahead=2):
        if lookahead == 0:
            return self.evaluate_current()
            
        best_score = -float('inf')
        for move in self.get_all_moves(self.current_shape):
            temp_board = self.simulate_move(move)
            next_score = self.next_move(temp_board, 
                                      self.next_shapes[:lookahead-1],
                                      lookahead-1)
            total_score = move.score + next_score
            if total_score > best_score:
                best_score = total_score
                best_move = move
        return best_move
    
  3. 可视化调试工具 : 添加AI决策过程可视化:

    def draw_ai_decision(self, painter):
        for move in self.ai.possible_moves:
            color = QColor(255, 0, 0, 50) if move == self.ai.best_move else QColor(0, 0, 255, 30)
            painter.setBrush(color)
            self.draw_shape(painter, move.shape, move.rotation, move.x)
    

在实际项目中,我发现AI的表现很大程度上取决于评分函数的细调。通过记录游戏数据并分析失败案例,可以持续改进算法。例如,当AI经常因为"井"结构而失败时,就需要在评分函数中增加对垂直凹陷的惩罚项。

更多推荐