
五子棋禁手逻辑重构: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. 代码重构
策略模式:将不同的规则集(如休闲模式、竞技模式)封装成策略对象,便于切换。
事件驱动:将落子、判定、胜负通知解耦,使用观察者模式,方便后续添加音效、动画等前端逻辑。
小结
五子棋禁手看似复杂,但拆解后就是几个简单的几何判断。
核心要点回顾:
五连优先:这是最容易被忽略的规则,务必在代码中前置判断。
方向解耦:将四个方向的逻辑独立处理,代码更清晰。
测试驱动:禁手规则细节多,必须依赖单元测试保障质量。
这个项目虽然不大,但涵盖了数据结构、逻辑封装、单元测试、边界处理等多个工程化要点。
你在项目里踩过这个坑吗?比如“三三”判定总是出错,或者“四四”漏判?评论区聊聊,看看大家是怎么解决的。