ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

五子棋禁手逻辑重构:3小时搞定实战项目的保姆级教程

五子棋禁手逻辑重构: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 = 0BLACK = 1WHITE = 2def __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.sizedef 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] = colorself.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 = 0cx, cy = x + dx, y + dywhile board.is_valid_position(cx, cy):if board.grid[cx][cy] == color:count += 1cx += dxcy += dyelse:breakreturn count避坑点: 很多新手在写这个函数时,容易把 (x, y) 本身也算进去,或者忘记检查边界。 这里我们采用“单方向延伸”的策略,而不是“双向扫描”。为什么? 因为判断禁手时,我们需要知道“如果落子在这里,会形成几个三”、“几个四”。单方向延伸更容易组合计算。 3. 禁手判定核心逻辑 这是最难的部分。我们将禁手拆解为三个独立函数:is_three、is_four、is_overline。 # rules.py from utils import DIRECTIONS, get_continuous_stonesdef 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 + 1if total == 4:# 检查两端是否为空front_end_x, front_end_y = x + (front + 1) * dx, y + (front + 1) * dyback_end_x, back_end_y = x - (back + 1) * dx, y - (back + 1) * dyfront_empty = board.is_valid_position(front_end_x, front_end_y) and \board.grid[front_end_x][front_end_y] == board.EMPTYback_empty = board.is_valid_position(back_end_x, back_end_y) and \board.grid[back_end_x][back_end_y] == board.EMPTYif front_empty or back_empty:return Truereturn Falsedef 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 + 1if total 5:return Truereturn 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 = 0for dx, dy in DIRECTIONS:if is_four_in_direction(board, x, y, dx, dy, color):four_count += 1if four_count = 2:return True, Double Four# 4. 三三three_count = 0for dx, dy in DIRECTIONS:if is_open_three_in_direction(board, x, y, dx, dy, color):three_count += 1if three_count = 2:return True, Double Threereturn False, Legal move为什么这样写?将复杂的判定拆解为独立的小函数,便于单元测试。 每个方向独立计算,避免重复代码。 返回元组 (bool, string),不仅告诉调用者“是否违规”,还告诉“为什么违规”,方便前端提示用户。运行与测试 代码写得再好,没测试就是空中楼阁。 1. 单元测试 # tests/test_rules.py import unittest from board import Board from rules import is_forbidden_moveclass 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. 代码重构策略模式:将不同的规则集(如休闲模式、竞技模式)封装成策略对象,便于切换。 事件驱动:将落子、判定、胜负通知解耦,使用观察者模式,方便后续添加音效、动画等前端逻辑。小结 五子棋禁手看似复杂,但拆解后就是几个简单的几何判断。 核心要点回顾:五连优先:这是最容易被忽略的规则,务必在代码中前置判断。 方向解耦:将四个方向的逻辑独立处理,代码更清晰。 测试驱动:禁手规则细节多,必须依赖单元测试保障质量。这个项目虽然不大,但涵盖了数据结构、逻辑封装、单元测试、边界处理等多个工程化要点。 你在项目里踩过这个坑吗?比如“三三”判定总是出错,或者“四四”漏判?评论区聊聊,看看大家是怎么解决的。
返回列表