Python Pygame实战:从零开发双模式五子棋游戏与AI算法详解 1. 项目概述不只是写个游戏更是Python与Pygame的实战演练看到“用Pygame写一款五子棋游戏”这个标题很多朋友可能会觉得这又是一个简单的入门小项目。但如果你真的动手去实现一个包含PVP玩家对战和PVE人机对战双模式的完整五子棋你会发现它远比你想象的要“硬核”。这不仅仅是在棋盘上画线、落子那么简单它是对你Python逻辑思维、数据结构运用、事件驱动编程以及基础AI算法理解的一次综合性考验。我之所以选择这个项目作为分享是因为它完美地覆盖了从图形界面GUI搭建、游戏状态管理到简单AI策略实现的全链路非常适合有一定Python基础想通过一个完整项目来巩固技能、挑战自己的朋友。这个项目能做什么首先它为你提供了一个可玩性很高的成品你可以和朋友在电脑上对弈PVP也可以挑战一个由你亲手“调教”出来的电脑对手PVE。更重要的是在实现过程中你会深入理解如何用Pygame处理鼠标交互、绘制图形、管理游戏循环如何设计一个清晰的数据结构比如二维列表来代表棋盘和棋子状态以及如何为一个看似简单的游戏如五子棋设计一个不那么“笨”的AI逻辑。无论你是想学习Pygame还是想通过一个具体项目来提升自己的编程实战能力这个五子棋项目都是一个绝佳的练手材料。2. 项目整体设计与核心思路拆解2.1 为什么选择Pygame作为开发框架在Python的GUI或游戏开发领域可选方案不少比如Tkinter、PyQt、Kivy等。我选择Pygame来写这个五子棋主要基于以下几点实战考量第一轻量级与专注性。Pygame的核心就是为2D游戏开发设计的它封装了SDL库提供了对图像、声音、事件输入的直接控制。对于五子棋这种不需要复杂3D渲染、物理引擎但需要精确的像素级绘制和实时事件响应鼠标点击的项目来说Pygame非常合适。它没有PyQt那样庞大的控件体系学习曲线相对平缓能让我们把精力集中在游戏逻辑本身。第二事件驱动模型清晰。Pygame的主循环while running和事件队列pygame.event.get()机制是理解游戏编程的经典范式。通过处理MOUSEBUTTONDOWN事件我们可以轻松捕获玩家的落子操作。这种模型直观易懂对于初学者建立“游戏是如何跑起来的”这个概念非常有帮助。第三社区资源丰富。作为一款历史悠久的库Pygame拥有海量的教程、示例和问答。在开发过程中遇到任何关于绘图、碰撞检测虽然五子棋用不到复杂的碰撞、音效播放等问题几乎都能找到现成的解决方案或思路参考。注意有些朋友在安装Pygame时可能会遇到困难特别是通过pip install pygame命令时可能会报错提示缺少某些C库依赖如SDL开发库。这在Windows上通常问题不大官方提供的wheel包兼容性很好。但在某些Linux发行版或macOS上可能需要先安装系统级的开发工具链如gcc和SDL库本身。如果遇到“error: failed to build ‘pygame’ when getting requirements to build wheel”这类错误建议先确保你的Python环境比如通过Anaconda或官方安装包是完整的或者直接搜索对应操作系统的Pygame预编译安装方法。2.2 双模式PVP PVE的游戏架构设计一个支持双模式的游戏其核心在于状态机的设计。我们不能把代码写成面条式的点击一个按钮就从头跑到尾。我的设计思路是将游戏划分为几个明确的状态并用一个变量如game_mode和相应的处理函数来管理。核心状态定义菜单状态MENU游戏启动后的初始界面。在这里玩家可以选择“玩家对战PVP”或“人机对战PVE”。这个界面通常包含按钮和简单的文字提示。游戏进行状态PLAYING这是游戏的核心状态。在此状态下需要进一步区分当前是“玩家1回合”、“玩家2回合”还是“电脑回合”。棋盘绘制、落子逻辑、胜负判定都在这个状态下进行。游戏结束状态GAME_OVER当有一方获胜或棋盘下满平局时进入此状态。界面显示获胜方信息或平局提示并提供“重新开始”或“返回菜单”的选项。数据层与表现层分离这是保持代码清晰的关键。我将所有与游戏规则和状态相关的数据放在一个Board棋盘类中。这个类内部用一个15x15的二维列表board存储每个交叉点的状态0为空1为黑子2为白子。它提供方法如place_piece(x, y, player)落子、is_win(x, y, player)判断落子后是否获胜、is_full()判断棋盘是否已满。而Pygame的代码主循环、事件处理、屏幕绘制则负责调用Board类的方法并将数据状态可视化出来。这样AI算法的实现属于PVE模式也只需要与Board类交互读取棋盘状态并计算最佳落子点大大降低了模块间的耦合度。3. 核心模块实现与关键技术点解析3.1 棋盘与棋子的数据建模五子棋的棋盘是15x15的网格这是游戏规则的基础。在代码中我使用一个二维列表来建模这是最直观和高效的方式。class Board: def __init__(self, size15): self.size size # 初始化一个size x size的棋盘0代表空位 self.board [[0 for _ in range(size)] for _ in range(size)] self.current_player 1 # 1代表黑棋先手2代表白棋 self.game_over False self.winner None这里有几个设计细节值得注意坐标映射鼠标点击屏幕获取的是像素坐标如(300, 200)我们需要将其转换为棋盘上的行列索引如(7, 4)。这涉及到棋盘左上角起点坐标、每个格子的像素间距grid_size的计算。公式通常是row (y - board_top) // grid_sizecol (x - board_left) // grid_size。转换后必须检查索引是否在0到size-1的范围内。落子有效性校验在place_piece方法中除了检查坐标是否越界还必须检查self.board[row][col]是否为0。只有为空位才能落子。这是一个简单的逻辑但却是防止游戏状态被破坏的重要关卡。当前玩家切换每次成功落子后需要切换current_player。在PVP模式下就是在1和2之间切换。在PVE模式下如果是玩家回合落子后则切换到“电脑回合”由AI计算后落子再切换回玩家。3.2 胜负判定算法的实现与优化这是五子棋逻辑中最核心的部分。判断是否获胜本质上是检查最新落子点(row, col)的横、竖、左斜\、右斜/四个方向上是否存在连续五个同色棋子。朴素算法暴力搜索从落子点向一个方向如向左数同色棋子再向反方向向右数如果总数包括自身达到5则获胜。对四个方向各做一次。这个方法逻辑简单但每次落子都要对四个方向进行最多9格一边4格加自身的检查对于15x15的棋盘完全够用性能不是问题。def is_win(self, row, col, player): directions [(1, 0), (0, 1), (1, 1), (1, -1)] # 横竖左斜右斜 for dr, dc in directions: count 1 # 包括当前棋子 # 向正方向检查 r, c row dr, col dc while 0 r self.size and 0 c self.size and self.board[r][c] player: count 1 r dr c dc # 向反方向检查 r, c row - dr, col - dc while 0 r self.size and 0 c self.size and self.board[r][c] player: count 1 r - dr c - dc if count 5: return True return False优化思考上述算法在每次落子后执行是O(1)的复杂度因为检查范围固定完全满足需求。不需要更复杂的优化。但如果你要做一个超大规模棋盘或者需要AI进行大量模拟对局可以考虑使用“Zobrist哈希”或“增量更新”的方式来记录和判断胜负不过对于这个项目来说属于“杀鸡用牛刀”了。3.3 PVE模式一个“有点脑子”的AI如何实现让电脑随机落子是最简单的但毫无游戏体验。我们的目标是实现一个具有一定防守和进攻能力的AI。这里介绍一种广泛使用且效果不错的算法基于评分的贪心算法。核心思想为棋盘上每一个空位计算一个“分数”。分数越高表示在这个位置落子对当前玩家这里是电脑越有利。AI每次选择分数最高的空位落子。如何计算分数我们需要定义一些“棋型”并为它们赋予分数。例如连五100000分直接获胜活四10000分下一步就能形成连五冲四5000分只有一个点能形成连五活三1000分能形成活四眠三200分能形成冲四活二100分眠二10分实现步骤遍历所有空位对于棋盘上的每一个空位(i, j)。模拟落子假设电脑白棋在此处落子。评估该点对电脑的价值以(i, j)为中心检查四个方向匹配上述棋型累加得到一个“进攻分”。评估该点对玩家的威胁再假设玩家黑棋在此处落子同样计算一个“防守分”因为阻止对手获胜同样重要。综合评分将进攻分和防守分以一定权重相加例如进攻分权重1.0防守分权重1.2因为防守往往更重要得到该空位的最终分数。选择最高分遍历完所有空位后选择分数最高的位置作为AI的落子点。def evaluate_point(self, board, row, col, player): 评估在(row, col)位置对player方的价值 score 0 # 这里需要实现具体的棋型匹配逻辑比较繁琐 # 通常会预定义一些棋型模式然后在四个方向上进行匹配 # 例如检查是否形成了“01111*”这样的模式*代表空位或边界 # 这是一个简化示例实际代码会更长 directions [(1,0),(0,1),(1,1),(1,-1)] for dr, dc in directions: line [] for k in range(-4, 5): # 取该方向上前后共9个点 r, c row dr*k, col dc*k if 0 r self.size and 0 c self.size: line.append(board[r][c]) else: line.append(-1) # 边界用-1表示 # 分析line列表匹配棋型累加score score self.pattern_score(line, player) return score def find_best_move(self, board): best_score -1 best_move None for i in range(self.size): for j in range(self.size): if board[i][j] 0: # 空位 # 计算如果我方电脑下这里的价值 attack_score self.evaluate_point(board, i, j, self.ai_player) # 计算如果对方玩家下这里的威胁 defend_score self.evaluate_point(board, i, j, self.human_player) total_score attack_score * 1.0 defend_score * 1.2 if total_score best_score: best_score total_score best_move (i, j) return best_move实操心得棋型定义和分数赋值是调整AI强度的关键。你可以通过大量对弈来微调这些分数。例如如果你发现AI进攻性太弱可以适当提高“活三”、“冲四”的进攻分权重。此外这个贪心算法只看一步更强大的AI如基于蒙特卡洛树搜索MCTS或Minimax搜索会考虑多步之后的局面但实现复杂度也呈指数级增长。对于入门到中级项目这个评分贪心算法在速度和智力上已经取得了很好的平衡。4. Pygame界面与交互的完整实现流程4.1 游戏窗口、棋盘与棋子的绘制首先我们需要初始化Pygame创建游戏窗口并设置一些常量。import pygame import sys # 初始化 pygame.init() SCREEN_WIDTH, SCREEN_HEIGHT 800, 600 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(五子棋 (PVP PVE)) clock pygame.time.Clock() # 颜色定义 BACKGROUND (220, 179, 92) # 棋盘背景色仿木色 LINE_COLOR (0, 0, 0) BLACK (0, 0, 0) WHITE (255, 255, 255) RED (255, 0, 0) BLUE (0, 120, 255) # 棋盘参数 BOARD_SIZE 15 GRID_SIZE 35 # 每个格子的像素大小 MARGIN 50 # 棋盘边距 BOARD_WIDTH (BOARD_SIZE - 1) * GRID_SIZE绘制棋盘在游戏循环的绘制阶段我们先填充背景色然后画横线和竖线。def draw_board(screen): # 绘制棋盘背景矩形 board_rect pygame.Rect(MARGIN, MARGIN, BOARD_WIDTH, BOARD_WIDTH) pygame.draw.rect(screen, BACKGROUND, board_rect) # 绘制网格线 for i in range(BOARD_SIZE): # 横线 start_pos (MARGIN, MARGIN i * GRID_SIZE) end_pos (MARGIN BOARD_WIDTH, MARGIN i * GRID_SIZE) pygame.draw.line(screen, LINE_COLOR, start_pos, end_pos, 2) # 竖线 start_pos (MARGIN i * GRID_SIZE, MARGIN) end_pos (MARGIN i * GRID_SIZE, MARGIN BOARD_WIDTH) pygame.draw.line(screen, LINE_COLOR, start_pos, end_pos, 2) # 绘制五个小黑点天元和星增加美观度 dots [(3,3), (3,11), (7,7), (11,3), (11,11)] for d in dots: center_x MARGIN d[0] * GRID_SIZE center_y MARGIN d[1] * GRID_SIZE pygame.draw.circle(screen, BLACK, (center_x, center_y), 5)绘制棋子根据Board类中board二维列表的数据来绘制。def draw_pieces(screen, board_obj): for row in range(board_obj.size): for col in range(board_obj.size): player board_obj.board[row][col] if player ! 0: center_x MARGIN col * GRID_SIZE center_y MARGIN row * GRID_SIZE color BLACK if player 1 else WHITE pygame.draw.circle(screen, color, (center_x, center_y), GRID_SIZE//2 - 2) # 棋子半径略小于格子一半 # 可以为白棋添加一个细黑边使其在浅色背景下更清晰 if player 2: pygame.draw.circle(screen, BLACK, (center_x, center_y), GRID_SIZE//2 - 2, 1)4.2 事件处理鼠标点击与游戏状态流转游戏主循环负责处理事件、更新逻辑和重绘画面。def main(): board Board(BOARD_SIZE) game_mode None # 初始为None在菜单选择 current_state MENU ai GomokuAI() # 假设有一个GomokuAI类 running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN: mouse_x, mouse_y pygame.mouse.get_pos() if current_state MENU: # 检查鼠标是否点击了PVP或PVE按钮需要事先定义按钮矩形 if pvp_button.collidepoint(mouse_x, mouse_y): game_mode PVP current_state PLAYING board.reset() # 重置棋盘 elif pve_button.collidepoint(mouse_x, mouse_y): game_mode PVE current_state PLAYING board.reset() # PVE模式默认玩家先手黑棋 ai.set_players(human1, ai2) elif current_state PLAYING: # 将鼠标坐标转换为棋盘坐标 row, col get_board_position(mouse_x, mouse_y) if row is not None and col is not None: if game_mode PVP: # PVP模式处理玩家落子 if board.place_piece(row, col, board.current_player): if board.is_win(row, col, board.current_player): board.winner board.current_player current_state GAME_OVER elif board.is_full(): current_state GAME_OVER else: board.switch_player() elif game_mode PVE: # PVE模式只有当前是玩家回合时才处理点击 if board.current_player ai.human_player: if board.place_piece(row, col, ai.human_player): if board.is_win(row, col, ai.human_player): board.winner ai.human_player current_state GAME_OVER elif board.is_full(): current_state GAME_OVER else: board.switch_player() # 玩家下完后轮到AI ai_move ai.find_best_move(board.board) if ai_move: r, c ai_move board.place_piece(r, c, ai.ai_player) if board.is_win(r, c, ai.ai_player): board.winner ai.ai_player current_state GAME_OVER elif board.is_full(): current_state GAME_OVER else: board.switch_player() elif current_state GAME_OVER: # 检查是否点击了“重新开始”或“返回菜单”按钮 if restart_button.collidepoint(mouse_x, mouse_y): board.reset() current_state PLAYING elif menu_button.collidepoint(mouse_x, mouse_y): current_state MENU game_mode None # 绘制部分 screen.fill((240, 240, 240)) # 填充屏幕背景色 if current_state MENU: draw_menu(screen) elif current_state PLAYING: draw_board(screen) draw_pieces(screen, board) draw_game_info(screen, board, game_mode) # 绘制当前玩家等信息 elif current_state GAME_OVER: draw_board(screen) draw_pieces(screen, board) draw_game_over(screen, board.winner) pygame.display.flip() clock.tick(60) # 控制帧率 pygame.quit() sys.exit()4.3 游戏状态界面绘制菜单与结束界面为了让游戏完整我们需要绘制简单的菜单和结束界面。这里使用Pygame的字体渲染功能。def draw_menu(screen): font_title pygame.font.SysFont(None, 64) font_button pygame.font.SysFont(None, 48) title font_title.render(五子棋, True, BLUE) screen.blit(title, (SCREEN_WIDTH//2 - title.get_width()//2, 100)) # 定义按钮矩形 global pvp_button, pve_button # 声明为全局以便事件处理中访问 pvp_button pygame.Rect(SCREEN_WIDTH//2 - 100, 250, 200, 60) pve_button pygame.Rect(SCREEN_WIDTH//2 - 100, 350, 200, 60) pygame.draw.rect(screen, (70, 130, 180), pvp_button, border_radius10) pygame.draw.rect(screen, (70, 130, 180), pve_button, border_radius10) pvp_text font_button.render(玩家对战 (PVP), True, WHITE) pve_text font_button.render(人机对战 (PVE), True, WHITE) screen.blit(pvp_text, (pvp_button.centerx - pvp_text.get_width()//2, pvp_button.centery - pvp_text.get_height()//2)) screen.blit(pve_text, (pve_button.centerx - pve_text.get_width()//2, pve_button.centery - pve_text.get_height()//2)) def draw_game_over(screen, winner): overlay pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 128)) # 半透明黑色遮罩 screen.blit(overlay, (0,0)) font pygame.font.SysFont(None, 72) if winner 0: text font.render(平局, True, WHITE) else: winner_name 黑方 if winner 1 else 白方 text font.render(f{winner_name} 获胜, True, RED if winner1 else WHITE) screen.blit(text, (SCREEN_WIDTH//2 - text.get_width()//2, SCREEN_HEIGHT//2 - 50)) font_small pygame.font.SysFont(None, 36) restart_text font_small.render(点击任意处重新开始, True, (200, 200, 200)) screen.blit(restart_text, (SCREEN_WIDTH//2 - restart_text.get_width()//2, SCREEN_HEIGHT//2 30))5. 项目打包与进阶优化思路5.1 使用PyInstaller将项目打包为独立EXE文件当你完成开发并测试无误后可能希望分享给没有Python环境的朋友。使用PyInstaller可以轻松将Python脚本打包成单个可执行文件.exe。操作步骤安装PyInstaller在命令行中运行pip install pyinstaller。准备资源文件如果你的游戏使用了图片、字体等资源文件确保它们位于一个固定的相对路径下例如./assets/并在代码中使用os.path.join来构建资源路径以兼容打包后的环境。执行打包命令在项目根目录下打开命令行执行pyinstaller -F -w -i your_icon.ico main.py-F打包成单个文件。-w运行时不显示控制台窗口对于GUI程序很重要。-i your_icon.ico指定生成exe文件的图标。main.py你的主程序入口文件。处理打包后的问题打包后程序的工作目录可能发生变化。如果遇到找不到资源文件如图片的错误需要在代码开头添加以下逻辑来修正路径import sys import os if getattr(sys, ‘frozen‘, False): # 如果是打包后的exe工作目录是exe所在目录 base_path sys._MEIPASS else: # 如果是直接运行的脚本工作目录是脚本所在目录 base_path os.path.dirname(os.path.abspath(__file__)) # 加载资源时使用 os.path.join(base_path, ‘assets‘, ‘image.png‘)踩坑记录打包Pygame项目时最常见的错误是缺少依赖的动态链接库.dll或资源文件。PyInstaller通常能自动找到Pygame的依赖但如果你使用了自定义字体.ttf或图片务必确保它们被包含进去。可以使用--add-data参数手动指定资源文件夹。例如--add-data “assets;assets”Windows或--add-data “assets:assets”macOS/Linux。这告诉PyInstaller将本地的assets文件夹复制到打包后的程序中。5.2 性能优化与代码结构改进建议AI算法优化启发式搜索当前的贪心算法只评估一步。可以引入一个深度有限的搜索如2-3步使用Minimax算法或Alpha-Beta剪枝让AI具备一定的“前瞻性”。虽然实现复杂度增加但AI强度会显著提升。预计算与缓存棋型评分函数evaluate_point会被频繁调用且很多计算是重复的。可以考虑为整个棋盘维护一个“评分表”每次落子后只更新受影响位置落子点周围一定范围的分数而不是全盘重新计算。开局库与定式为AI添加一个简单的开局库。例如如果AI执黑前几步可以直接下在常见的优势点如天元、星位。这能快速提升AI在开局阶段的表现。代码结构优化使用状态模式将MENU、PLAYING、GAME_OVER等状态抽象成独立的类每个类负责自己的事件处理、逻辑更新和绘制。主循环只负责调用当前状态对象的方法。这会使代码更加模块化易于扩展例如增加“设置”状态。资源管理器创建一个ResourceManager类统一加载和管理图片、字体、音效等资源避免在代码中散落着多个pygame.image.load()。配置文件将棋盘大小、颜色、AI难度参数等可配置项写入一个单独的配置文件如config.ini或settings.py方便调整而不需要修改核心代码。用户体验增强落子提示在鼠标移动到可落子位置时显示一个半透明的棋子预览。音效添加落子音效、获胜音效使用pygame.mixer模块。动画效果为棋子落下添加一个简单的缩放动画让交互更生动。悔棋功能维护一个落子历史栈history每次落子时将(row, col, player)压栈。实现悔棋时从栈中弹出上一步并清空棋盘对应位置同时切换玩家。注意在PVE模式下悔棋需要连续弹出两步玩家一步和AI一步。6. 常见问题排查与调试技巧实录在开发过程中你几乎一定会遇到下面这些问题。这里我把它们和解决方法记录下来希望能帮你节省时间。6.1 Pygame安装与导入失败问题ModuleNotFoundError: No module named ‘pygame‘或安装时出现编译错误。排查确认Python环境在命令行输入python --version和pip --version确保你安装Pygame的pip和运行代码的python是同一个环境。特别是使用了虚拟环境venv, conda或IDE如PyCharm, VSCode内置终端的情况。使用国内镜像源加速安装pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simpleWindows/Mac用户优先尝试安装预编译的wheel包。如果pip install pygame失败可以到 Pygame官网 查找对应Python版本和系统的预编译包进行安装。Linux用户可能需要先安装SDL等开发库。例如在Ubuntu上sudo apt-get install python3-dev libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev6.2 鼠标点击位置与棋盘坐标映射错误现象点击一个交叉点附近棋子却下在了别处或者点击无效。调试打印坐标在鼠标点击事件中打印出原始的鼠标像素坐标(mouse_x, mouse_y)和转换后的棋盘坐标(row, col)。检查转换公式确认board_top棋盘上边距、board_left棋盘左边距和grid_size格子大小计算正确。可以用pygame.draw.rect在棋盘区域画一个边框直观检查坐标范围。验证索引范围确保转换后的row和col在0到BOARD_SIZE-1之间。在get_board_position函数中做好边界检查和取整通常使用//整除。6.3 AI运行速度慢或“卡顿”现象在PVE模式AI思考时间过长导致游戏界面“冻结”。优化缩小搜索范围AI不需要评估棋盘上所有空位。五子棋有“邻域”概念通常只需要考虑已有棋子周围2-3格范围内的空位。这能极大减少评估点数。优化评分函数evaluate_point函数是性能热点。确保内部没有低效的循环或重复计算。可以考虑将棋型模式预定义为常量列表或字符串使用快速的字符串匹配或位运算来判断。分帧计算对于深度搜索的AI一次搜索可能耗时几百毫秒。为了避免界面卡死可以将AI的思考过程放入一个单独的线程或者将搜索拆分成多步在游戏循环中分帧执行每帧计算一部分期间仍能响应界面事件如关闭窗口。这需要更复杂的异步编程。6.4 胜负判定出现误判或漏判现象明明连成五子却未判胜或者未连成五子却判胜了。调试单元测试为is_win函数编写专门的测试用例。模拟各种获胜场景横、竖、斜、边界情况和非获胜场景确保函数在所有情况下都返回正确结果。可视化调试在判断获胜时临时将连成五子的棋子用特殊颜色如红色高亮显示出来便于肉眼验证。检查方向向量确保directions列表中的四个方向向量(1,0), (0,1), (1,1), (1,-1)是正确的它们分别代表了横、竖、左斜\、右斜/四个方向。6.5 游戏状态切换逻辑混乱现象比如游戏结束后还能落子或者模式切换后棋盘状态没重置。解决状态变量检查在事件处理的关键分支如鼠标点击落子逻辑开始处严格检查current_state和game_mode变量是否符合预期。重置函数实现一个清晰的board.reset()函数确保在开始新游戏或切换模式时不仅清空棋盘数组还要重置current_player、game_over、winner等所有状态变量。绘制隔离确保每个游戏状态MENU,PLAYING,GAME_OVER的绘制函数是独立的不会相互干扰。例如在PLAYING状态下只绘制棋盘和棋子在GAME_OVER状态下再叠加绘制结束提示。这个项目从零到一实现下来代码量大约在500-800行左右但它涵盖的知识点非常密集。当你看到自己编写的程序能够流畅运行并且能和一个由你赋予“智慧”的AI对弈时那种成就感是无可比拟的。更重要的是通过这个项目你获得了一套解决同类问题棋盘类游戏、状态机管理、简单AI的可复用方法论。如果学有余力不妨尝试我上面提到的进阶优化比如给AI加上搜索深度或者为游戏添加音效和动画这会让你的作品更加出彩。编程的乐趣正是在于这种从构建到打磨最终创造出有价值、有互动的事物的过程。