news 2026/9/22 1:46:05

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
五子棋禁手逻辑重构:3小时搞定实战项目的保姆级教程

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

看了一堆五子棋教程,代码能跑,但一写进真实项目就崩?别慌,这篇保姆级教程带你从零搭建一个符合竞技规则的引擎。

很多人卡在“禁手”上,觉得规则复杂。其实只要拆解清楚,逻辑比想象中简单。我们直接看代码,不废话。

项目目标

我们要实现一个支持黑棋禁手判定的五子棋核心引擎。

目标很明确:

  1. 输入:15x15 棋盘状态,当前落子位置。
  2. 处理:判断黑棋是否形成“三三”、“四四”或“长连”禁手。
  3. 输出:合法/非法标识,以及具体的违规类型。

为什么强调“引擎”而不是“游戏”? 因为前端渲染是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_threeis_fouris_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 Three"return 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. 代码重构

  • 策略模式:将不同的规则集(如休闲模式、竞技模式)封装成策略对象,便于切换。
  • 事件驱动:将落子、判定、胜负通知解耦,使用观察者模式,方便后续添加音效、动画等前端逻辑。

小结

五子棋禁手看似复杂,但拆解后就是几个简单的几何判断。

核心要点回顾

  1. 五连优先:这是最容易被忽略的规则,务必在代码中前置判断。
  2. 方向解耦:将四个方向的逻辑独立处理,代码更清晰。
  3. 测试驱动:禁手规则细节多,必须依赖单元测试保障质量。

这个项目虽然不大,但涵盖了数据结构、逻辑封装、单元测试、边界处理等多个工程化要点。

你在项目里踩过这个坑吗?比如“三三”判定总是出错,或者“四四”漏判?评论区聊聊,看看大家是怎么解决的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 1:45:59

5分钟搞懂boystyle速查手册,公路工程数据避坑指南

5分钟搞懂boystyle速查手册,公路工程数据避坑指南 刚升级完数据处理库,发现之前写的脚本全报错了?别慌,这种版本迭代后的 API 断层,是公路工程数据分析新手最头疼的坑。很多人对着报错日志发呆,其实只需要一份精准的 速查手册 就能快速定位问题。…

作者头像 李华
网站建设 2026/9/22 1:45:55

5分钟吃透数据服务源码,新手避坑指南

5分钟吃透数据服务源码,新手避坑指南 翻开官方文档,你是否觉得像在看天书?几千页的 API 列表,新手根本抓不住重点,更别提理解底层逻辑了。别慌,今天咱们不背文档,直接拆源码。…

作者头像 李华
网站建设 2026/9/22 1:45:54

3个细节搞定一笔写成田,手写实现不再掉坑

3个细节搞定一笔写成田,手写实现不再掉坑 面试被问“一笔写成田”怎么实现,是不是脑子一片空白?很多后端或嵌入式开发者觉得这是前端 Canvas 的活,其实只要把路径算法理顺,用任何语言手写实现都不难。别慌,今天咱们就拆解这个经典图形绘制问题,把原理讲透,让你下次面对面试官能直接掏出代码写出来。…

作者头像 李华
网站建设 2026/9/22 1:45:48

DNF破损的刀刃重构实战:保姆级教程解决API变更痛点

DNF破损的刀刃重构实战:保姆级教程解决API变更痛点 版本升级后 API 全变了,你的代码是不是也炸了?别慌,这篇 DNF 破损的刀刃 保姆级教程,带你从零搭建一套抗版本冲击的底层架构。 项目目标 很多转行做游戏的开发者,一上来就盯着玩法,忽略了 基础设施的稳定性 。DNF…

作者头像 李华
网站建设 2026/9/22 1:45:35

qq等级排名避坑指南:大厂面试真题拆解与代码实战

qq等级排名避坑指南:大厂面试真题拆解与代码实战 官方文档翻了三遍还是抓不住重点?别急,这行混久了都知道,文档是写给上帝看的,不是给人看的。今天这份 qq等级排名 的避坑指南,专门为你这种准备面试的应届生整理,不绕弯子,直接上干货。很多人以为 QQ…

作者头像 李华
网站建设 2026/9/22 1:45:06

z0入门实战:搞定3个报错,搞定你的第一个机器学习项目

z0入门实战:搞定3个报错,搞定你的第一个机器学习项目 刚接触 z0 的学员,是不是经常对着满屏红色的报错信息发呆?特别是当程序跑了一半,突然抛出一个长长的 StackTrace ,里面全是看不懂的类名和行号,那种无力感真的让人想放弃。别慌,我见过太多学员卡在第一步,以为 z0…

作者头像 李华