2.1 从 3×3 到 15×15
欢迎来到第二章,也是整门课的重头戏。第一章我们在井字棋上把每个算法都备齐了,但它们在那个小棋盘上几乎都是“杀鸡用牛刀”。这一章棋盘从 9 个格子暴涨到 225 个交叉点,你会亲眼看到:第一章那些游刃有余的方法瞬间失效,逼着我们一件件造出真正的“重武器”。本节先把规则讲清、把“为什么变难”量化,再写好棋盘的地基。
一、五子棋规则与本课的 v1 约定
五子棋的英文是 Gomoku(也叫 Gobang / Five-in-a-Row),规则简单到一句话:双方轮流在棋盘的交叉点上落子,谁先让自己的棋子在横、竖、或斜方向上连成 5 个,谁就赢。标准棋盘是 15×15,黑子先行。
不过五子棋有不少规则变体,为了循序渐进,我们给第一版(v1)定个明确范围:
- 15×15 棋盘、自由规则(freestyle)、无禁手:随便下,先连成 5(或更多)即胜,不设任何限制。
- 自由开局:先手第一手可以下在任意位置。
- 专业竞技用的 连珠禁手(限制黑棋)和 swap2 开局平衡,我们留到本章后面的 2.9 作为进阶专门讲——它们是规则的“加层”,引擎主体不受影响。
还有个好消息:第一章定稿的 GameEngine 接口,这里原样复用。五子棋引擎照样要实现 legal_moves / make_move / is_terminal / winner / evaluate / best_move / info——这正是我们当初费心统一接口的回报,换了棋种,外面的界面和擂台一行都不用改。
二、复杂度爆炸:第一章的方法为什么瞬间失效
先用数字把“变难”说清楚,你才会理解后面那些复杂技巧的必要性。还记得第一章 Minimax 的命门吗——它得把博弈树搜到底。我们来算算五子棋的树有多大:
- 分支因子:井字棋开局只有 9 个落点,五子棋却有 225 个,之后也长期维持在一两百。
- 对局长度:井字棋最多 9 步就结束,五子棋一局动辄几十步。
搜索 d 层要看的节点数大约是“分支因子的 d 次方”。井字棋满打满算 9! ≈ 36 万,眨眼就搜完;五子棋呢?哪怕只往下搜 4 层,就是 225⁴ ≈ 25 亿个节点——而 4 层在五子棋里浅得可怜,连一个简单的连续进攻都看不全。整棵树的规模约 10⁷⁰,搜到底所需的时间远超宇宙年龄。
一句话定调:在五子棋上,“把树搜到底”彻底不可能了。这一条直接决定了本章的全部走向——我们必须想尽办法让第一章的算法在“搜不到底”的现实里活下来:① 砍掉绝大多数不值得看的落点(候选生成);② 搜不到终局就用评估函数估分;③ 把剪枝和排序做到极致;④ 最终干脆让神经网络来学评估。本章后面每一节,都是在回答“树太大”这同一个问题。
三、高效的棋盘表示与五连检测
地基先打牢。棋盘还是沿用第一章那套编码——一个 15×15 的 NumPy 数组,0 空、+1 黑(先手)、−1 白(后手),连正负号编码都不变,后面评估和搜索照样占便宜。
真正要动脑的是胜负判定。井字棋只有 8 条线,每次全查一遍无所谓;五子棋有上百条潜在的五连线,每步都全盘扫描太浪费。这里有个关键优化:胜负只可能由刚落下的那一子造成,所以只需以这一点为中心、朝四个方向数一数连子。这样每步判负是常数级的开销,而不是全盘级:
import numpy as np
N = 15
DIRECTIONS = [(1, 0), (0, 1), (1, 1), (1, -1)] # 横、竖、两条斜
def check_win(board, r, c):
"""刚在 (r,c) 落子的一方是否连成 5?只需看这一点的四个方向。"""
player = board[r, c]
for dr, dc in DIRECTIONS:
count = 1
# 沿正方向数同色子
rr, cc = r + dr, c + dc
while 0 <= rr < N and 0 <= cc < N and board[rr, cc] == player:
count += 1; rr += dr; cc += dc
# 再沿反方向数
rr, cc = r - dr, c - dc
while 0 <= rr < N and 0 <= cc < N and board[rr, cc] == player:
count += 1; rr -= dr; cc -= dc
if count >= 5: # 正反加起来够 5 个就赢了
return True
return False
体会一下这个“只看落子点四方向”的思路——它在五子棋里无处不在。下一节做评估函数时,我们识别“活三、冲四”靠的也是同样的方向扫描;只不过那时不是数“够不够 5”,而是数“连成了几个、两头堵没堵”。方向扫描,是五子棋一切棋型分析的原子操作。
(如果你追求极致速度,还可以用位棋盘 bitboard——把每一行压进整数的二进制位,用位运算一次判完一条线。它快得多,但也更烧脑,属于进阶优化;本课用清晰的方向扫描来讲原理,你完全可以在跑通后再去换 bitboard 提速。)
四、小结与下一节
- 规则与范围:15×15、freestyle 无禁手、自由开局为 v1;禁手/swap2 留到 2.9;接口复用第一章的
GameEngine。 - 复杂度爆炸:分支因子 ~225、对局长、博弈树 ~10⁷⁰,搜到底彻底不可能——这是本章一切技巧的根源。
- 地基:±1 编码的 15×15 数组;胜负判定只扫落子点的四个方向,常数级开销;方向扫描是后续棋型分析的原子操作。
地基铺好了,下一节 2.2 搜索框架工程化,我们先把第一章的 α-β 原样搬过来、亲眼看它在大盘上慢到不可用,然后动第一刀——只在已有棋子附近落子,把那 225 个候选点砍到可以接受的数量。