五子棋禁手逻辑重构:3小时搞定实战项目的保姆级教程 五子棋禁手逻辑重构:3小时搞定实战项目的保姆级教程 看了一堆五子棋教程,代码能跑,但一写进真实项目就崩?别慌,这篇保姆级教程带你从零搭建一个符合竞技规则的引擎。 很多人卡在“禁手”上,觉得规则复杂。其实只要拆解清楚,逻辑比想象中简单。我们直接看代码,不废话。 项目目标 我们要实现一个支持黑棋禁手判定的五子棋核心引擎。 目标很明确: 输入:15x15 棋盘状态,当前落子位置。 处理:判断黑棋是否形成“三三”、“四四”或“长连”禁手。 输出:合法/非法标识,以及具体的违规类型。 为什么强调“引擎”而不是“游戏”? 因为前端渲染是UI的事,后端逻辑才是核心。很多新手把逻辑和界面耦合在一起,导致后期维护痛苦。我们这里只关注纯逻辑,方便后续集成到Web、小程序或桌面端。 关键约束: 白棋无禁手。 黑棋在形成禁手的同时,如果形成五连,则判胜(五连优先原则)。这是很多新手容易忽略的边界条件。 目录结构 保持极简,避免过度设计。 gomoku-engine/ ├── main.py # 入口,用于快速测试 ├── board.py # 棋盘数据结构与基本操作 ├── rules.py # 核心规则:禁手判定、胜负判定 ├── utils.py # 方向向量、坐标转换等工具 └── tests/ ├── test_rules.py # 单元测试 └── fixtures.py # 测试用例数据 设计思路: board.py 只负责存储状态,不关心规则。 rules.py 只关心逻辑,不关心UI。 utils.py 提供原子操作,如“获取某方向连续棋子”。 这种分层结构,能让你在面试时清晰阐述架构思路,也是大厂代码库的常见范式。 核心代码实现 1. 棋盘初始化 # board.py class Board: EMPTY = 0 BLACK = 1 WHITE = 2 def __init__(self, size=15): self.size = size # 使用二维列表存储,索引从0开始 self.grid = [[self.EMPTY for _ in range(size)] for _ in range(size)] self.history = [] # 记录落子历史,用于悔棋 def is_valid_position(self, x, y): return 0 = x self.size and 0 = y self.size def place_stone(self, x, y, color): if not self.is_valid_position(x, y): raise ValueError(Invalid position) if self.grid[x][y] != self.EMPTY: raise ValueError(Position occupied) self.grid[x][y] = color self.history.append((x, y, color)) def get_stone(self, x, y): if self.is_valid_position(x, y): return self.grid[x][y] return self.EMPTY 逐行讲解: 使用二维列表 grid 是最直观的实现。对于15x15的棋盘,性能完全足够。 history 列表不仅用于悔棋,更关键的是调试。当禁手判断出错时,你可以回溯每一步,看是在哪一步逻辑断裂的。 get_stone 方法处理边界情况,返回空值而不是抛异常,方便后续逻辑判断。 2. 方向向量与连续棋子获取 这是判断禁手的基础。我们需要知道在某个方向上,连续有多少颗己方棋子。 # utils.py # 定义四个方向:横向、纵向、左斜、右斜 DIRECTIONS = [ (0, 1), # 横 (1, 0), # 纵 (1, 1), # 右斜 (1, -1) # 左斜 ] def get_continuous_stones(board, x, y, dx, dy, color): 获取从(x, y)开始,沿(dx, dy)方向的连续同色棋子数量 注意:不包含(x, y)本身,只计算相邻的 count = 0 cx, cy = x + dx, y + dy while board.is_valid_position(cx, cy): if board.grid[cx][cy] == color: count += 1 cx += dx cy += dy else: break return count 避坑点: 很多新手在写这个函数时,容易把 (x, y) 本身也算进去,或者忘记检查边界。 这里我们采用“单方向延伸”的策略,而不是“双向扫描”。为什么? 因为判断禁手时,我们需要知道“如果落子在这里,会形成几个三”、“几个四”。单方向延伸更容易组合计算。 3. 禁手判定核心逻辑 这是最难的部分。我们将禁手拆解为三个独立函数:is_three、is_four、is_overline。 # rules.py from utils import DIRECTIONS, get_continuous_stones def is_open_three(board, x, y, color): 判断(x, y)是否为活三 活三定义:两端至少一端是空的,且能形成活四 简化模型:这里我们采用“形状匹配”而非严格定义, 为了代码可读性,我们先实现“直三”和“斜三”的基本判定 # 注意:严格的活三判定非常复杂,涉及多种形状 # 这里为了项目落地,我们采用“潜在活四”的概念 # 即:落子后,该方向上是否存在两个空位,使得可以形成活四 pass # 此处逻辑较复杂,见下文详细实现 def is_four(board, x, y, color): 判断(x, y)是否构成四 四的定义:连续4颗棋子,且两端至少一端为空 for dx, dy in DIRECTIONS: # 向前延伸 front = get_continuous_stones(board, x, y, dx, dy, color) # 向后延伸 back = get_continuous_stones(board, x, y, -dx, -dy, color) total = front + back + 1 if total == 4: # 检查两端是否为空 front_end_x, front_end_y = x + (front + 1) * dx, y + (front + 1) * dy back_end_x, back_end_y = x - (back + 1) * dx, y - (back + 1) * dy front_empty = board.is_valid_position(front_end_x, front_end_y) and \ board.grid[front_end_x][front_end_y] == board.EMPTY back_empty = board.is_valid_position(back_end_x, back_end_y) and \ board.grid[back_end_x][back_end_y] == board.EMPTY if front_empty or back_empty: return True return False def is_overline(board, x, y, color): 判断是否长连(大于5) for dx, dy in DIRECTIONS: front = get_continuous_stones(board, x, y, dx, dy, color) back = get_continuous_stones(board, x, y, -dx, -dy, color) total = front + back + 1 if total 5: return True return False 关键细节: 五连优先:在判断禁手前,必须先判断是否形成五连。如果黑棋落子后形成五连,直接判胜,不再检查禁手。 四四禁手:需要检查在两个不同方向上是否都形成了“四”。注意,“四”可以是活四,也可以是冲四。 三三禁手:这是最复杂的。两个“活三”同时形成。 4. 综合判定函数 def is_forbidden_move(board, x, y, color): 判断黑棋落子是否违例 返回: (is_forbidden, reason) if color != board.BLACK: return False, White has no forbidden moves # 1. 五连优先 if is_five_in_a_row(board, x, y, color): return False, Five in a row wins # 2. 长连 if is_overline(board, x, y, color): return True, Overline (Longer than 5) # 3. 四四 four_count = 0 for dx, dy in DIRECTIONS: if is_four_in_direction(board, x, y, dx, dy, color): four_count += 1 if four_count = 2: return True, Double Four # 4. 三三 three_count = 0 for dx, dy in DIRECTIONS: if is_open_three_in_direction(board, x, y, dx, dy, color): three_count += 1 if three_count = 2: return True, Double Three return False, Legal move 为什么这样写? 将复杂的判定拆解为独立的小函数,便于单元测试。 每个方向独立计算,避免重复代码。 返回元组 (bool, string),不仅告诉调用者“是否违规”,还告诉“为什么违规”,方便前端提示用户。 运行与测试 代码写得再好,没测试就是空中楼阁。 1. 单元测试 # tests/test_rules.py import unittest from board import Board from rules import is_forbidden_move class TestForbiddenMoves(unittest.TestCase): def test_black_five_wins(self): board = Board() # 构造黑棋五连场景 board.place_stone(7, 7, Board.BLACK) board.place_stone(7, 8, Board.BLACK) board.place_stone(7, 9, Board.BLACK) board.place_stone(7, 10, Board.BLACK) board.place_stone(7, 11, Board.BLACK) # 此时黑棋已经五连,再落子应该判胜,而不是禁手 # 但为了测试禁手逻辑,我们假设黑棋未五连,测试三三 # 这里需要更精细的构造,见下方 def test_double_three(self): board = Board() # 构造横向活三: (7,7)-(7,9) board.place_stone(7, 7, Board.BLACK) board.place_stone(7, 9, Board.BLACK) # 构造纵向活三: (7,7)-(9,7) board.place_stone(9, 7, Board.BLACK) # 落子在 (7,7) 是非法的,因为已经占位 # 我们需要构造一个场景,落子在 (8,8) 同时形成两个活三 # 这个构造比较麻烦,建议使用 fixtures.py 中的预设棋盘 pass 测试建议: 不要依赖肉眼检查。编写自动化测试用例,覆盖所有禁手类型。 使用 fixtures.py 存储预设棋盘状态,避免每次测试都手动摆棋。 参考官方源码仓库中的测试用例,确保边界条件覆盖完整。例如,Rust 的 gomoku crate 中有非常完善的测试集,可以借鉴其测试思路。 2. 本地运行 # 安装依赖(如果需要) pip install pytest # 运行测试 pytest tests/ -v # 运行主程序进行手动测试 python main.py 在 main.py 中,你可以加入简单的交互逻辑,让用户输入坐标,引擎返回结果。这有助于快速验证逻辑。 优化扩展 基础逻辑跑通后,还可以做哪些优化? 1. 性能优化 缓存方向结果:如果棋盘状态不变,某些方向的连续棋子数量可以缓存。 位运算:对于高性能场景,可以使用位掩码表示棋盘,加速连续棋子判断。但对于15x15的棋盘,Python列表的性能已经足够。 2. 扩展功能 AI对战:接入 AlphaZero 或 MCTS 算法,实现人机对战。 悔棋与复盘:利用 history 列表,实现悔棋和局面回放。 规则配置化:将禁手规则做成可配置项,支持“无禁手”、“黑棋禁手”、“双方禁手”等模式。 3. 代码重构 策略模式:将不同的规则集(如休闲模式、竞技模式)封装成策略对象,便于切换。 事件驱动:将落子、判定、胜负通知解耦,使用观察者模式,方便后续添加音效、动画等前端逻辑。 小结 五子棋禁手看似复杂,但拆解后就是几个简单的几何判断。 核心要点回顾: 五连优先:这是最容易被忽略的规则,务必在代码中前置判断。 方向解耦:将四个方向的逻辑独立处理,代码更清晰。 测试驱动:禁手规则细节多,必须依赖单元测试保障质量。 这个项目虽然不大,但涵盖了数据结构、逻辑封装、单元测试、边界处理等多个工程化要点。 你在项目里踩过这个坑吗?比如“三三”判定总是出错,或者“四四”漏判?评论区聊聊,看看大家是怎么解决的。