1. 项目概述
1.1 核心需求解析
五子棋这个项目,看起来不过是棋盘上黑白子的博弈,但真正动手去写,你会发现它几乎涵盖了游戏开发的全部基础知识点:数据结构设计、图形渲染、交互事件、AI策略落子、胜负判定、状态管理。我从第一次在终端里用星号和井号打印棋盘,到最终做出带悔棋、AI对战、计分板的完整客户端,中间踩了不少坑,也沉淀了不少值得写下来的心得。
这篇文章写的是我用Python + Pygame做一个可玩的五子棋游戏的完整过程,适合刚入门游戏开发或者想用项目练手Python的朋友。看这篇文章你会收获三样东西:一是整套可复制的代码结构与实现思路,二是算法层面的优化方案(尤其是AI落子和胜负判定),三是我在实际运行中踩过的坑和解决办法,这些在教程里通常不会写。
做这个项目前我给自己定了一个目标:不能只是“能落子”的demo,必须做到有图形界面、能人机对战还能双人对战、有悔棋和重开功能。很多新手练五子棋都在控制台搞定,但一旦牵扯到图形界面,事件循环、帧率控制、界面刷新这些问题就会一个接一个冒出来,这正是我想要的练习效果。
1.2 方案选型与技术范围
选择Python + Pygame不是因为它最强大,而是因为它让你在“看到成果”和“搞清楚原理”之间找到一个平衡点。Py game的事件驱动模型相当于你手机里的通知推送系统——鼠标点击是通知,键盘按键是通知,窗口关闭也是通知。游戏主循环不断询问“有没有新通知?有就处理”,这种模型理解透了,以后学任何图形界面的游戏开发都顺畅。
开发环境的准备很简单,安装Python 3.8以上版本后执行:
pip install pygame我在Windows 11和macOS上都跑过,没遇到什么环境兼容问题。建议用虚拟环境隔离依赖,尤其是同时在做多个Python项目的时候。Pygame的版本我用的是2.5.x,不同小版本的API变化不大,基本不影响本文代码。
技术方案最终确定如下:
- 语言:Python 3.10
- 图形库:Pygame 2.5
- 棋盘:15x15标准棋盘,格子边长40像素
- 交互:鼠标点击落子,按键快捷键操作
- AI:基础评分函数 + 极大极小搜索,用α-β剪枝优化,难度分三档
- 架构:棋盘逻辑与界面渲染分离,为以后扩展网络对战留余地
2. 五子棋实现的核心细节
2.1 棋盘数据结构的建模
棋盘是游戏的心脏。数据结构设计得好,后面AI、胜负判断、悔棋功能的实现都会变得很顺畅。我见过不少人开局就写二维数组存棋子,这没错,但到了实现悔棋或者复盘时就痛苦了。
我用了经典方案:15x15的二维列表,0表示空,1表示黑棋,2表示白棋。
self.board = [[0 for _ in range(15)] for _ in range(15)]这行代码只是起点。游戏真正需要的不只是“当前棋盘长什么样”,还有“棋局是怎么一步步走到这里的”。悔棋的实现方式是:每次落子后把坐标push进一个move_stack,悔棋时直接从栈顶弹出最后一步,回滚棋盘状态。这个思路和浏览器返回上一页一个道理,把历史记录堆起来,才能回退。如果没有这个历史栈,悔棋就只能用深拷贝保存每个历史棋局,内存开销大而且代码丑得多。
如果你还想做“撤销对手的落子”或“全部重开”,历史栈就更重要了。我最初的版本没有考虑这些,后来加功能时不得不重构,这是我希望你从一开始就避免的。教训是:设计数据结构时,多想想未来可能要加什么功能,而不是只管当前需求。
2.2 界面渲染的坐标换算
Pygame里所有绘图都基于像素坐标,而游戏逻辑关心的是棋盘行列号。鼠标点击位置到行列号的换算,是每个图形界面棋盘游戏都会遇到的基础问题。棋盘左上角留了40像素的边距,行列索引i对应像素坐标(30 + i * 40, 30 + j * 40)。
反向换算鼠标点击位置:
def get_board_pos(mouse_pos): x, y = mouse_pos row = round((y - 30) / 40) col = round((x - 30) / 40) if 0 <= row < 15 and 0 <= col < 15: return row, col return None这里有个关键细节:用round而不是int转换。round是四舍五入,点击位置靠近格子边缘时也能正确落到最近的格点上;int直接截断会导致点击格子上半部分落到上一行,手感会有明显偏差。我第一版就用的int,结果测试时总觉得落子位置偏了半格,排查了很久才发现是这个原因。
棋盘绘制我用了Pygame的draw.line和draw.circle,线条颜色选了深棕色,接近木质棋盘质感。坐标计算要特别小心边界,不要因为少画一条线导致棋盘不对称。这一步没有什么高技术含量,纯粹是耐心活,但棋盘画得整洁,整个游戏的第一印象就好了很多。
2.3 胜负判定算法的优化
五子棋胜负判定的常规做法是:每次落子后,从落子位置向四个方向(横、竖、两个对角线)延伸数连续同色棋子,任意方向连成5颗或以上即判胜。
def check_winner(board, row, col): player = board[row][col] directions = [(0, 1), (1, 0), (1, 1), (1, -1)] for dr, dc in directions: count = 1 for sign in (1, -1): r, c = row + sign * dr, col + sign * dc while 0 <= r < 15 and 0 <= c < 15 and board[r][c] == player: count += 1 r += sign * dr c += sign * dc if count >= 5: return True return False只判当前落子位置而不是全盘扫描,是性能的关键。15x15棋盘不算大,全盘扫描也不是不行,但全盘扫描意味着每步都要遍历225个位置,而局部判定只需要检查落子点附近最多4条线,时间差在可以忽略和明显卡顿之间。
需要注意“连成5颗或以上”和“恰好5颗”的区别。标准五子棋规则中,超过5颗的长连并不算赢(部分规则甚至算输),但很多休闲向五子棋只要≥5就判胜。我在代码里写成count >= 5,因为本文讨论的是休闲玩法,如果你写的是严格比赛规则,需要把条件改成count == 5并且确认两端不被阻断。
2.4 AI落子算法的设计思路
AI是五子棋项目的重头戏。最简单的是随机落子,纯属凑数;往上一步是打分制——也就是遍历棋盘的每个空位,计算该位置的“价值分”,选最高分落子。打分需要同时评估进攻(自己连子)和防守(对手连子)。
我实现的评分函数如下:
- 五连:100000分
- 活四:50000分
- 冲四:10000分
- 活三:5000分
- 眠三:1000分
- 活二:500分
- 眠二:100分
对于每个空位,分别在四个方向扫描,计算如果放上自己的棋子能形成多少连子、是否被堵死。AI对每个位置给出的进攻分和防守分取最高的作为决策依据。防守分就是把该位置当成白棋来算,AI虽是黑棋,也要评估对手如果占据这里会有多危险。
def evaluate_position(board, row, col, ai_color): attack_score = evaluate_line_score(board, row, col, ai_color) human_color = 3 - ai_color defend_score = evaluate_line_score(board, row, col, human_color) return max(attack_score, defend_score)这个思路对应的是AI策略中“最小防守损失、最大进攻收益”的核心逻辑:一方面尽量让自己形成有力棋形,另一方面要堵住对手的活三和冲四。
基础打分AI在休闲五子棋中已经能胜任大多数情况,它不仅能赢新手,也有能力暴打随手乱下的玩家。但它的弱点也明显:缺乏“多步计算”能力,看不到两步以后的棋,不会设计陷阱。想进一步提升就用极大极小搜索加上α-β剪枝,搜索深度设到4层,配合评估函数。代价是计算耗时明显上升,需要设定一个落子时间上限,比如2秒,超时就退回深度浅一层的决策。
实际测试经验:打分AI应对90%的休闲局没有任何问题,除非你自己本身就是五子棋高手,否则不要一上来就做深度搜索。先把基础打分打磨好,效果远超预期。
3. 完整实现与核心代码解析
3.1 项目代码结构
我建议按模块组织代码,而不是把所有逻辑塞进一个main.py。下面是我用的一套简单结构,清晰易扩展,后续加AI难度或者网络对战时不用大改:
gobang/ ├── main.py # 程序入口,Pygame初始化与主循环 ├── board.py # 棋盘逻辑类:落子、悔棋、胜负判断 ├── ai.py # AI模块:评分函数、决策 ├── ui.py # 界面绘制相关:棋盘、棋子、按钮 ├── config.py # 配置:颜色、大小、尺寸常量 └── requirements.txt这套结构好在分工明确。board.py只负责数据逻辑,完全不涉及绘图,也就是说就算有一天你想做Web版本的同一套逻辑,把board.py和ai.py直接搬过去还能用。测试也更方便,不必打开图形窗口就能单独验证胜负逻辑。
3.2 初始化与主循环框架
main.py的核心代码逻辑:
import pygame from board import Board from ai import AI from ui import draw_board, draw_buttons def main(): pygame.init() screen = pygame.display.set_mode((760, 680)) pygame.display.set_caption("五子棋") clock = pygame.time.Clock() board = Board() ai = AI(level=2) game_mode = "PVE" # PVE人机对战 / PVP双人对战 current_player = 1 # 1黑棋先行(玩家),2白棋(AI) game_over = False running = True while running: clock.tick(60) # 控制帧率,1秒最多循环60次 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.MOUSEBUTTONDOWN: handle_click(event.pos) if game_mode == "PVE" and current_player == 2 and not game_over: row, col = ai.get_move(board) board.place(row, col, 2) current_player = 1 if board.check_winner(row, col, 2): game_over = True print("AI获胜") draw_board(screen, board) draw_buttons(screen) pygame.display.flip() pygame.quit() if __name__ == "__main__": main()主循环本质是一个永不停止的while循环,每轮做三件事:处理事件、更新游戏状态、重新绘制画面。Pygame不像网页前端有浏览器帮你管理渲染周期,开发者的工作就是让循环转起来并且保持稳定。clock.tick(60)的意义在于控制循环速率,不写的话游戏会以每秒数千帧的速度狂转,CPU占用直接拉满,落子判定也会因事件处理过快而出现各种奇怪问题。
3.3 棋盘核心逻辑的实现细节
board.py的完整逻辑如下,包含初始化、落子、悔棋、胜负判定:
class Board: def __init__(self, size=15): self.size = size self.board = [[0 for _ in range(size)] for _ in range(size)] self.move_stack = [] def place(self, row, col, player): if self.board[row][col] != 0: return False self.board[row][col] = player self.move_stack.append((row, col, player)) return True def undo(self): if not self.move_stack: return None row, col, _ = self.move_stack.pop() self.board[row][col] = 0 return row, col def reset(self): self.board = [[0 for _ in range(self.size)] for _ in range(self.size)] self.move_stack.clear() def check_winner(self, row, col, player): if self.board[row][col] != player: return False directions = [(0, 1), (1, 0), (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 Falseplace方法必须有“该位置是否被占用”的判断,这是一个非常重要的输入校验。否则多个人点击同一个位置,棋子会被重复覆盖,棋局数据就全乱了。很多“bug”看起来神出鬼没,追根溯源都在这种基础校验的缺失上。
悔棋的undo处理直接返回了(row, col),被弹出的位置信息实际上就是棋盘上最后一步棋的位置,绘制时不需要主动“擦除”,因为每一帧都从零重画整个画面,这得益于Pygame的绘制方式——直接把整个棋盘重绘一遍,而不是在旧画面上涂改。
这里跟你分享一个核心技巧:游戏画面用全量重绘而不是局部更新。Painter模型是“每一帧都从白纸开始画”,你只要保证状态数据正确,渲染一定是对的。很多新手想用局部擦除来“优化性能”,反而制造了大量难以排查的脏痕问题。
3.4 AI评分机制的工程实现
AI模块的实现是评分函数的策略体现,落子优先选择能“活三变活四”或“双活三”的棋形。我在实际调试中总结了一套简单高效的评分方式。
class AI: def __init__(self, level=2): self.level = level # 难度:1简单 2中等 3困难 def get_move(self, board): best_score = -1 best_moves = [] ai_color = 2 # AI执白 for row in range(board.size): for col in range(board.size): if board.board[row][col] != 0: continue # 只评估与已有棋子距离不超过2的位置,极大提升效率 if not self._near_existing(board, row, col): continue score = self._evaluate(board, row, col, ai_color) if score > best_score: best_score = score best_moves = [(row, col)] elif score == best_score: best_moves.append((row, col)) if best_moves: import random return random.choice(best_moves) return board.size // 2, board.size // 2_near_existing这个逻辑很重要,是我用来优化性能用的。15x15棋盘有225个位置,如果AI每次都要遍历全部空位做评分,速度会很慢,而且大量远离棋局的空位评分毫无意义。我加了一个限制条件:只评估与已有棋子距离2格以内的空位。这相当于围棋里的“只有战斗区域才需要计算”,全局安全性几乎不受影响,但耗时会缩减到原来的五分之一。
早期版本没有这个优化,AI每次走棋在棋盘空旷时要遍历近200个位置,每个位置还要做复杂的方向扫描,能明显感觉到一步要等几秒。加了这个限制后,中前期的思考时间缩短到1秒以内,体验完全不一样了。
_evaluate方法实现为:
def _evaluate(self, board, row, col, player): attack = self._evaluate_point(board, row, col, player) defend = self._evaluate_point(board, row, col, 3 - player) return max(attack * 1.1, defend)把进攻分稍微乘了一个1.1的权重,这是无数对局测试出来的结果。纯防守型AI很容易陷入被动,对手下一步活三它就堵,堵完对手继续展开,自己始终没机会布局。给进攻一点优先权可以让AI在防守的同时主动找机会形成四连和五连。实际对局测试下来,1.1这个比例棋风比较均衡。
3.5 PVE与PVP模式切换及悔棋的实现
游戏要可玩,模式切换必须流畅。我设置了两个按钮,点击“人机对战”进入PVE模式,点击“双人对战”进入PVP模式。切换时要重置棋盘和所有历史状态,不然棋局数据会串。
悔棋逻辑要分模式处理。PVE模式里,如果当前是玩家回合,点悔棋应该回滚两步(一步AI的上一步,一步玩家的上一步)。PVP模式则回滚一步。
def handle_undo(): if game_over: return if game_mode == "PVE": board.undo() # 撤销AI的一步 board.undo() # 撤销玩家的一步 current_player = 1 else: board.undo() current_player = 3 - current_player game_over = False这里要特别注意game_over状态的恢复。如果不重置game_over为False,就会出现赢了之后点悔棋画面一直停在“获胜”提示的尴尬局面。这是我的真实踩坑经历,调试时发现提示不消失,想了整整十分钟才意识到还要把获胜标志位复位。
对于多人对战的计分,我在界面顶部放了黑方白方各自的胜场计数。计分是独立于棋盘的另一个状态,不能在棋盘重置时被清零。用单独的变量保存,每次判胜后更新,只有在点“重新开始”时才重置分数。
3.6 界面绘制与交互细节
界面设计没有太多技术深度,但直接影响体验。我用了几个细节提升视觉效果:棋子画了阴影和渐变高光,比纯色圆形更有立体感;当前轮到哪一方时,棋盘右侧用彩色圆点提示;最后落子位置用红色小方块标记,方便快速找到最近一步,这在复盘时特别有用。
棋子绘制的核心代码:
def draw_piece(screen, row, col, color): center_x = MARGIN + col * GRID_SIZE center_y = MARGIN + row * GRID_SIZE # 阴影 if color == 1: pygame.draw.circle(screen, (30, 30, 30), (center_x + 3, center_y + 3), RADIUS) pygame.draw.circle(screen, (10, 10, 10), (center_x, center_y), RADIUS) pygame.draw.circle(screen, (80, 80, 80), (center_x - 2, center_y - 2), RADIUS - 4) else: pygame.draw.circle(screen, (200, 200, 200), (center_x + 3, center_y + 3), RADIUS) pygame.draw.circle(screen, (240, 240, 240), (center_x, center_y), RADIUS) pygame.draw.circle(screen, (255, 255, 255), (center_x - 2, center_y - 2), RADIUS - 4)阴影和三层圆的绘制让棋子有一种轻微浮雕感。这个效果其实就是用偏移 + 颜色深浅欺骗人眼,成本几乎为零,视觉效果提升很明显。黑色棋子先画一圈深灰作为阴影,再画黑色主体,最后在偏左上画小一圈的亮灰模拟光线——类似你给头像加了层高光,立刻从平面图标变成了有质感的圆钮。
3.7 交互响应与游戏状态管理
主循环的事件处理里,鼠标点击要区分落在哪个区域——棋盘点还是按钮区。每颗棋子只需要正确识别一次点击,事件队列不能积压重复的点击。这里有一个细节值得注意:Pygame有一个事件队列机制,如果一帧里处理了多个MOUSEBUTTONDOWN,可能会造成一次点击触发两次落子的问题。
解决方案是给落子操作加一个冷却判断:
if pygame.mouse.get_pressed()[0]: if pygame.time.get_ticks() - last_click_time > CLICK_COOLDOWN: handle_click() last_click_time = pygame.time.get_ticks()300毫秒的冷却阈值是我试过比较舒服的参数。它不会影响连续快速点击的响应,但又有效避免了同一时间点上重复事件造成的双落子问题。这也解释了为什么事件循环里处理简单判断可能会翻车——点击操作不是瞬时脉冲,物理上的单击可能在事件队列中产生多个事件记录。
状态管理方面用一个简单的枚举把游戏状态分清:
class GameState: PLAYING = 1 # 对局中 WINNER = 2 # 有人获胜 DRAW = 3 # 和棋(全盘满子)PVE对战时,AI的计算是在主循环中同步执行的。当AI计算需要1秒以上时,画面会短暂卡住。工业级做法是开一个线程做AI计算,主循环保持流畅渲染,但这会引入线程间的状态同步问题,复杂度提升不少。我选择了折中:AI计算超过3秒时才考虑异步处理,否则保持同步,简单可靠。
4. 运行调试中的常见问题与优化思路实践
4.1 棋盘显示不完整或坐标偏移
如果你发现鼠标点到某个位置棋子却出现在相邻格子上,问题绝大多数出在坐标换算逻辑。我处理过从round误写成int导致偏移半格的情况,还有一次把边距算错,导致整个棋盘整体偏移了十几像素,看起来棋子都悬空了。
排查技巧是:在画棋盘时给每个格子角落打上小点,或者临时输出鼠标坐标转换后的row和col到控制台,点击几个不同位置验证,很快就能定位问题所在。
建议把MARGIN和GRID_SIZE定义在config文件里,只改一处就能控制整块棋盘,而不是散落在代码各处手动填数字。
4.2 AI思考时间长导致界面卡顿
AI思考时间太长会让玩家产生“程序死了”的错觉。玩家体验其实有个隐性门槛:3秒以上思考就会让人开始感到不安。我在实际优化过程中做了三件事:
第一,只评估棋子2格范围内的空位,这一步效果最明显。第二,把中间空棋盘时的AI走法固定为第一次落子策略,不必计算——这是基于开局执黑方优先下天元(棋盘正中心)的策略,省掉不少无意义搜索。第三,给难度档位设置最大计算深度,中等难度上浅层搜索+评分函数混合,困难难度才用完整搜索,每档控制在300毫秒到2秒的安全区间。
4.3 胜负判定偶发失效
多人反馈过一种情况:明明棋盘上已经连成五颗了,却没有触发胜利。排查发现,问题在于我的胜利判定只检查了最后一步落子位置的方向值。如果连成五子里的最后一手不是关键胜负手时,判定完全没有问题;但如果构造特殊,比如棋子是被对方在另一处落子后突然形成的,就存在逻辑漏洞。正常对局里连五的发生一定是在最后落子那个位置延伸出来的,理论上不存在五连却判定不出的情况,除非棋形状态被破坏或读写不一致。
把board状态全量打个日志回放一遍是最后的排查手段。每次落子后记录整个棋盘状态到文本文件,然后从最后一次能正常判定的棋局逐步重放,定位出现异常的落子那一手,基本就能找到原因。这类不定时出现的bug,靠肉眼盯代码很难发现,日志回放才高效。
4.4 悔棋后AI重复走同一位置
一个有意思的问题:玩家在AI走完后点悔棋,把AI和玩家的两步都撤了,然后玩家如果还走原位置,AI可能就会重复上一轮的步骤。这不是bug,AI本来就根据当前棋盘状态按最优解走,重来一次自然容易选择同一个位置。如果希望AI每次有点变化,可以在得分完全相同的位置中随机选择,这就是我的get_move里用了random.choice的原因。从玩家角度,这一步让对局过程有了变动,不会每次复盘都走一模一样的变化线。
4.5 棋盘状态重置不干净
所有需要重置的功能——新开局、模式切换、返回主菜单——都要做“深度重置”,而不只是改了某几个变量。我最初的reset方法只清空了二维数组,忘了清move_stack,导致新开局点悔棋把上一局的棋给撤销出来,场面一度很滑稽。
复用代码时总结出一个规矩:凡是做重置逻辑,必须把对局相关的所有状态变量列成清单,逐个确认是否处理。
| 状态 | PVE模式下是否重置 | PVP模式下是否重置 | 模式切换时是否重置 |
|---|---|---|---|
| board数组 | 是 | 是 | 是 |
| move_stack | 是 | 是 | 是 |
| current_player | 是 | 是 | 是 |
| game_over | 是 | 是 | 是 |
| 双方比分 | 否 | 否 | 是 |
| 最后一步标记 | 是 | 是 | 是 |
这个表格帮我避免很多次脑内状态混乱。
5. 体验优化与玩法扩展
5.1 音效与动画反馈
纯粹的功能能玩,加反馈才好玩。我为落子加了一个轻脆的“嗒”声,获胜时有一段短旋律。Pygame的mixer模块可以加载wav或ogg文件,不要用mp3,Pygame对mp3解码支持不稳定,有时候加载失败有时候播放有杂音。
pygame.mixer.init() place_sound = pygame.mixer.Sound("sounds/place.wav") win_sound = pygame.mixer.Sound("sounds/win.wav") place_sound.play()音效不需要复杂,关键是动作发生后100毫秒内反馈到耳朵里,延迟太久就会让人感觉声音和动作脱节。落子动画我用的简单缩放效果:棋子从外圈以30毫秒间隔逐帧扩大绘制到标准大小,实测手感比较跟手,代码量也不大,相当于为每次落子额外画了3帧中间状态。让我对这个效果产生执念的原因是玩过太多棋类游戏后发现,没有动画反馈的落子经常让人怀疑自己到底点上没有。
5.2 多档AI难度的灰度控制
难度设计不是直白给AI调“更聪明”的参数,而是为不同水平的玩家提供不同挑战。我定义了三档:
- 简单:AI只在进攻分里做决策,忽略防守,偶尔故意随机扣掉20%概率不走最优,给人留活路
- 中等:攻防兼顾、只在空位2格范围内做评估,能挡住活三,有时应对双活三不够从容
- 困难:攻防兼修,引入双威胁感知,能识别出能同时制造两个活三的位置并优先抢占,对大多数休闲玩家来说需要认真应对
难度的调节本质上是在“做出最优决策”和“刻意制造破绽”之间画一条线。简单AI如果真正做到每一步棋都最优,新手玩家很难赢,游戏就没有乐趣了。给简单AI加随机失误概率是迎合用户水平的做法。
5.3 后续扩展方向的思考
当核心玩法稳定后,值得考虑的扩展方向有很多。网络对战是最自然的选择——这个我要提醒你,涉及网络通信就要考虑连接管理和退出异常,复杂度上一个台阶。我做过的简单实现是用Socket传坐标,服务端做端口监听,用的是基本库,但掉线重连和状态同步问题花了我大量时间,建议先确认自己是否真的需要网络功能,再动手。
另外一个成本较低但收益明显的扩展方向是棋谱记录与导出。每次对局结束后把move_stack保存为JSON文件,相当于拿到了完整棋谱,可以做复盘、做统计、甚至可以做一个“分析你最近开局偏好”的功能。这个功能做起来难度不高,价值长期存在。
6. 实操心得与经验总结
做个能玩的五子棋项目不难,几天就能让你从零到一把基础功能都跑通。但从“能玩”到“好玩、流畅、稳定”,还要花不少精力打磨。这种打磨不是某个炫酷技术,而是数据完整性、事件响应准确性、状态重置干净度这些听起来平凡无奇的细节。
我在实际开发中总结了两条核心经验想分享给你们:
第一,逻辑与渲染分离带来的收益远超预期。我写第一版时把棋盘状态检查和画图混在一起,每次改功能都提心吊胆,生怕动一处影响了另一处。后来花了半天时间把两者彻底分开,后续加任何功能——AI难度、音效、动画、悔棋——都顺畅了很多。别急着说“这个小项目不需要设计模式”,哪怕只是把代码按职责拆分成不同文件,对于学习成长已经是个很好的开始。
第二,善用日志打印来排查交互类bug。图形界面的问题经常不是“逻辑错了”,而是“状态在某个顺序下变得不对”。每次落子时打印当前行列、轮到谁、胜败状态,看起来繁琐,一旦bug浮现,那些打印就是最好的破案线索。
五子棋项目做一个版本后如果还有余力,强烈建议尝试写一个AI难度更高的版本,不是从网上下载现成引擎,而是自己动手实现限制搜索深度、启发式搜索、膨胀剪枝这些经典棋类AI技术——这套知识对后续做任何类型的博弈类项目都是通用的,比起不断开新项目练手,把一个项目做深更划算。