☰
计算机博弈竞赛备赛指南:搜索算法、评估函数与自对弈调参实战
2026/10/13 10:53:42 网站建设 项目流程

简介:在人工智能与算法工程交汇的竞赛场景中,博弈树搜索是决策系统的核心,而α-β剪枝与评估函数共同决定了程序的棋力上限。掌握极大极小搜索原理、理解启发式评估的建模方法,是构建稳定智能体的基础。从技术价值看,高效搜索配合可配置的时间控制,能显著提升对弈系统的实时性与鲁棒性。此类技术广泛适用于象棋、五子棋等确定性零和游戏的AI开发,也常用于教学实训与算法对比验证。在工程实践中,通过自对弈数据驱动参数调优,比依赖直觉调整更可靠,可有效避免胜率波动与超时风险。本文以计算机博弈竞赛备赛为场景,系统拆解从知识骨架搭建、项目框架设计到回归测试的完整流程,给出可直接落地的调参与避坑经验。

1. 计算机博弈竞赛到底在比什么:这不是算法题,是工程题

计算机博弈竞赛辅导资料这个标题,很容易让人误以为是一堆往年代码、PPT 和讲义的合集。实际带队几次之后我的看法变了:资料的价值不在“收藏”,而在能不能帮你在两三周内把一个能下棋的程序,从 0 推到比赛现场还能稳定完赛。计算机博弈竞赛的常见赛项是象棋、五子棋、黑白棋这类确定性零和游戏,比的是在限定步时内做出合格决策。它表面考搜索算法,实际考的是工程配平:搜索深度、评估函数、时间控制、异常处理,哪一个环节掉了链子都会直接判负。下面按备赛顺序拆:先立知识骨架,再给项目框架,然后讲怎么用自对弈数据调参,最后是高频坑。适合正在备赛的学生团队,也适合打算接手辅导工作的人。

2. 先搭知识骨架:搜索算法、评估函数与博弈树的三层递进

2.1 极大极小搜索与 α-β 剪枝:博弈树上的贪心与截断

计算机博弈的核心问题是:给定一个局面,找出对己方最优、对对方最差的走法。象棋、五子棋、黑白棋都是完全信息、零和博弈,理论上可以用博弈树穷举,但分支因子太大,大到没法完整展开。于是绝大多数备赛方案都落在极大极小搜索 + α-β 剪枝这条线上,它不依赖任何第三方库,逻辑清晰,出问题好排查,而且对裁判机的性能要求很低。

常见做法是固定深度搜索,递归到设定深度就用评估函数打分。实现顺序上我一般建议先写走法生成,再写评估函数,最后套搜索骨架——这个顺序反了,后面调试会很痛苦。下面是搜索骨架,alpha 是己方已知的最低收益下界,beta 是对手已知收益的上界,窗口越小剪枝越猛。

# 极小极大 + alpha-beta 剪枝的最小骨架 def alphabeta(board, depth, alpha, beta, is_maximizing): # 到达固定深度或终局:返回评估值,正值对先手有利 if depth == 0 or board.is_terminal(): return evaluate(board) if is_maximizing: best = -float("inf") for move in board.legal_moves(): board.make_move(move) best = max(best, alphabeta(board, depth - 1, alpha, beta, False)) board.undo_move(move) alpha = max(alpha, best) if beta <= alpha: break # beta 剪枝:对手不会让这个分支发生 return best else: best = float("inf") for move in board.legal_moves(): board.make_move(move) best = min(best, alphabeta(board, depth - 1, alpha, beta, True)) board.undo_move(move) beta = min(beta, best) if beta <= alpha: break # alpha 剪枝:自己不会让这个分支发生 return best

这段代码里最容易被新手忽略的是 make_move 和 undo_move 的配对。如果因为异常或提前返回漏了恢复棋盘,后续所有走法生成都会基于脏棋盘,棋力会以肉眼可见的速度崩掉。我的习惯是在每层调用外面包一个 try-finally,保证 undo 一定执行。

alpha 和 beta 的初值在根节点取正负无穷,窗口越窄剪枝越有效。备赛阶段可以先不管复杂的启发式排序,把走法按吃子分值粗排(比如将军、吃后、吃车优先),剪枝率就能显著提升。比较常见的翻车点是递归深度和语言运行时限制冲突,深搜超过 10 层时解释型语言容易爆栈,比赛前要做一次深度压测。

2.2 评估函数才是真正的“棋力”来源:从子力差到局面特征

很多团队把精力全放在搜索上,觉得层数越多越强。实际对局里层数拉不开时,评估函数的质量决定胜负。评估函数做的事很简单:给局面打分,正值表示对某一方有利,负值相反。搜索只是找到一条让这个分数最大化的路径,所以评估函数差,深搜出来的也是差的路线。

评估函数通常分三层叠加。第一层是子力分,每种棋子一个固定价值;第二层是位置分,用一张和棋盘同尺寸的位置表,奖励中心控制、惩罚边角死子;第三层是棋型分,比如五子棋的活四、冲四、活三,黑白棋的角、边、稳定子。备赛阶段第一层和第二层必须做,第三层看棋种,如果时间紧,可以先只做第一层。

# 评估函数:子力分 + 位置分,正值对先手有利 def evaluate(board): score = 0 PIECE_VALUE = { "pawn": 100, "knight": 320, "bishop": 330, "rook": 500, "queen": 900, } # POS[...][y][x] 为位置表,取值来自对公开棋谱的统计,也可手工调 for x in range(board.width): for y in range(board.height): piece = board.piece_at(x, y) if piece is None: continue value = PIECE_VALUE[piece.name] + POS[piece.name][y][x] score += value if piece.color == board.side_to_move else -value return score

这里的 POS 位置表可以用两种方式获得:一种是自己统计大量对局里棋子的实际分布,另一种是手工按常识填,比如马对中心位置加分、兵越靠近底线越值钱。需要注意的是评估函数的正负要和搜索函数的视角保持一致,否则会出现“越搜越差”的现象,这是备赛时最常见的逻辑错位。

还有一条血泪经验:不要一上来就堆特征。每加一个特征,就得重新调权重,自对弈验证一轮至少几百局。起步阶段三个特征以内最稳妥,等基线胜率稳定了再逐步加,每次只加一个。

2.3 MCTS 适合什么棋种、什么时候别用它

蒙特卡洛树搜索(MCTS)在围棋、黑白棋这类评估函数难写的游戏里表现突出,它的核心思想是用随机模拟的胜负结果代替人工评估函数,配合 UCT 公式在探索和利用之间找平衡。选择阶段用 UCB1 公式选孩子,扩展后做随机模拟,再把结果反向传播回路径上的节点。

# UCT 选择 + 随机模拟的最小流程 def uct_select(root, iteration_limit=2000): for _ in range(iteration_limit): node = root while node.children and not node.is_terminal(): node = best_child(node) # UCB1 选孩子 if not node.is_terminal(): node = expand(node) # 加一个未探索的走法 result = random_playout(node) # 随机模拟到终局 backpropagate(node, result) # 胜负结果回传 return best_child(root, exploration=0.0) # 最后一步不再鼓励探索

UCB1 公式里的常数 C 控制探索强度,常见做法取 1.41 附近,实际调参区间在 0.7 到 2.0 之间。C 越小越倾向走胜率高的固定分支,C 越大越倾向尝试冷门走法。这个参数和棋种强相关,换棋种必须重新标定。

选型上可以直接参考这个对比:

棋种类型推荐方案理由
中国象棋/国际象棋α-β + 评估函数规则复杂、分支因子高,MCTS 随机模拟质量差
五子棋α-β + 棋型评估开局有成熟定式,位置表收益明显
围棋/黑白棋MCTS 为主评估函数难写,模拟结果相对可信
亚马逊棋/角力棋MCTS规则特殊、先验知识少,模拟探索占优

常见误区是临赛前两周从 α-β 切到 MCTS,理由是“听说这个更强”。MCTS 的调参周期和模拟次数标定都很花时间,没有足够对局数据支撑,上线基本翻车。我的建议是:资料里两类算法都要会,但比赛主程序只带练过上千局的那一套,另一套最多做开局库辅助决策。每年备赛都能看到团队栽在这里。

3. 把辅导资料变成自己的框架:一份可用项目骨架与迭代顺序

3.1 框架目录怎么组织:从“能跑”到“能调”的文件划分

拿到辅导资料后最忌讳的事是把别人的代码直接编译进自己程序里。往年资料的共性问题是:搜索、评估、走法生成、对局记录全部堆在一个文件里,改一个参数要翻几百行。备赛时间紧,必须从第一天就按模块拆开,后面调参和排错才不痛苦。我一般建议按下面这个结构组织:

player/ ├── main.py # 入口:读配置、初始化引擎、进入对局循环 ├── config.yaml # 可调参数:深度、时间、开局库开关 ├── engine/ │ ├── search.py # alpha-beta / MCTS 搜索 │ ├── evaluate.py # 评估函数 │ └── move_gen.py # 走法生成、合法性检查 ├── game/ │ ├── board.py # 棋盘状态、吃子、将军/禁着 │ └── notation.py # 读棋谱、写对局记录 ├── opening/ │ └── book.py # 开局库读写与命中判断 └── logs/ └── play.log # 对局记录、时间戳、胜负

engine 和 game 分离是刻意为之:game 只负责规则和状态,engine 只负责决策。这样自对弈时可以让两个不同引擎实例共用同一套 game 模块,逻辑上不会打架。如果比赛规则允许程序接收图形界面输入,那 main.py 里再单独开一层界面适配,决策层不要掺 UI 代码。

很多团队决赛当天出问题,都出在“改了一个引擎文件,结果棋盘模块也跟着没法编译”。模块边界清晰的好处是:出问题时看报错路径,能立刻定位到是走法生成、搜索还是接口层的问题。

提示:如果赛事要求最终提交单文件,可以在模块调试完成后用一个打包脚本合并,不要在开发阶段就写成单文件。

3.2 第一步先实现最小可对弈版本:命令行对弈与随机对手

框架搭好后不要直接写深搜,第一步先让两个程序能在命令行里下一盘完整的棋。这个最小版本包含:棋盘初始化、走法生成、合法性检查、胜负判定、命令行输入输出。它跑通的意义在于,后面所有搜索和评估的调试都能基于一个可信的规则底座。

# 最小对弈版本:双方都是随机走棋,验证规则模块是否完整 python main.py --player-black random --player-white random --move-limit 200

如果这盘棋能正常分胜负,说明规则层基本可用。常见问题是走法生成漏了某些特殊规则(比如不能送将、禁着、升变),这类 bug 在随机对弈时会以“非法走棋被裁判拒收”的形式暴露出来。我的建议是在这个阶段就引入一个简单的合法性断言:每次 make_move 前检查目标走法是否在 legal_moves 列表里,不在就直接抛异常,宁可崩溃也不要静默放行。

随机对弈版本还能用来验证一个地基指标:先手胜率不能明显失衡。如果随机下先手胜率超过 60%,多半是规则实现里把某些子力的行为搞错了,在写评估函数之前先把这块修平。

3.3 第二步加数据与记录:对弈日志、胜负统计、回放文件

很多团队到了调参阶段才发现没有对局记录,改完代码不知道是变强还是变弱。第二步要做的就是把每盘棋完整写下来,这个日志文件就是后面调参的“黑匣子”。日志字段必须固定,下面这张表是我一直沿用的最小方案:

字段示例值用途
时间戳2024-05-01 14:22:31对局耗时与异常时刻定位
黑方引擎标识alpha-beta-v8版本追踪,必须与代码提交对应
白方引擎标识alpha-beta-v9版本追踪,必须与代码提交对应
单步限时2s记录实际配置
搜索深度8判断深度是否被动态下调
胜负结果win/loss/draw/timeout超时也要单独记录
走法序列e2e4 e7e5 …回放与复盘用

版本标识尤其重要,因为调参会频繁改代码,没有标识的话,胜率统计根本分不清是哪一版跑出来的。我的做法是每次修改引擎后自动生成一个版本后缀,比如 v8alpha,git 提交信息里也写同一个标识。

回放文件是给复盘用的。最简单格式是标准棋谱记法,一行一个走法,配合棋盘模块就能重新演算。比赛现场出现“这个局面以前见过但当时输了”的情况,能回放就能针对性补开局库。

3.4 第三步做可配置引擎:时间控制、搜索深度、开局库开关

第三步是把硬编码参数全部抽到配置文件里。这一步做完,调参就不用改代码再重新编译,对局时也能根据剩余时间动态调整策略。

engine: search: alphabeta # alphabeta | mcts max_depth: 8 # 搜索深度上限,终局阶段会自适应下调 time_per_move_ms: 2000 # 单步行时,按对局阶段自动放缩 use_opening_book: true # 是否先查开局库再进入搜索 opening: book_file: "book.bin" book_depth: 12 # 前 12 步命中开局库时直接走库着

配置字段最核心的是 search 和 time_per_move_ms。search 决定整套决策走哪条算法线,比赛现场不建议运行时切换,但训练时可以用来对比算法。time_per_move_ms 这个字段一般会配合动态策略:开局给足时间,残局适当放大,中盘如果剩余时间不足就降深度,宁可走浅一层也不能超时判负。

这块最容易被忽略的是开局库开关。训练自对弈时应该把开局库关掉,让引擎自己探索中后盘;正式比赛时再打开。如果开关做反了,自对弈数据会大量重复开局,中盘能力评估失真,胜率统计基本没有参考价值。

4. 对局测试与调参:用自对弈数据而不是直觉来改代码

4.1 自对弈是唯一可信的评估手段:怎么跑出有统计意义的数据

备赛阶段最常见的决策错误是拿几盘和队友的手下对局来评判引擎强弱。人手对局样本少、对手水平不稳定,几盘结果根本分不清是棋力变化还是运气波动。可靠做法是让改前和改后两个版本自对弈,控制变量只留一个改动点,跑足够盘数再看胜率。

我一般会把对局数压到 200 到 400 局起步。这个数量不是玄学,是胜率置信区间决定的:假设真实胜率就是 55%,对局数少于 100 时,统计上很可能落在 40% 到 70% 的区间里,等于没测。200 局能把区间压缩到大约 ±7%,400 局能到 ±5%。备赛时间紧的话最少 150 局,且中间不能换代码版本。

# 控制变量:老版本执黑、新版本执白,跑 200 局 for i in $(seq 1 200); do python play.py \ --player-black ./build/engine_v8 \ --player-white ./build/engine_v9 \ --time-per-move 1000 --opening off --seed $i \ >> logs/v8_vs_v9.log done python analyze.py --log logs/v8_vs_v9.log --threshold 0.55

这里每个对局用不同的随机种子,并且关闭开局库,目的是让每盘棋从不同局面展开,避免 200 盘全走同一个开局,导致测的其实是开局库而不是引擎实力。analyze 脚本只需要统计胜负平和超时次数,再加一个简单的二项检验函数。

自对弈还有一个前提是程序不死循环。比赛环境中对弈有一方掉线或崩溃就会直接判负,所以测试脚本里一定要设 move-limit 上限,超过步数判和。这个和棋判断要和比赛规则保持一致,不然统计结果会对不上现场判罚。

注意:自对弈数据采集期间不要改代码,中途换版本会让整批结果作废。换版本就重启一批新对局。

4.2 参数怎么调:搜索深度、节点数、时间分配的核心权衡

参数调优的顺序有讲究。我建议先调时间分配,再调搜索深度,最后调评估函数权重。时间分配是“地板”:如果连步时限都用不满、经常剩一大把时间就出子,说明还有搜索潜力没挖出来。深度是“天花板”:深度上不去,评估函数调得再好也出不了效果。权重在最后调,因为它最容易引入过拟合。

参数调节区间调参方向需要关注的副作用
单步限时0.2~5 s优先保证落子率 100%,逐步加压超时判负次数
搜索深度4~12 层深度每加 1 层,耗时近似翻倍递归耗时与栈溢出
剪枝窗口α-β 窗口窗口越小剪枝越狠,但可能剪错与对局胜率的关联
MCTS 迭代数500~5000迭代翻倍耗时近似线性前期步时严重不足
评估特征权重逐项 ±20%一次只动一个维度自对弈胜率波动

这里有个容易被忽略的点:不少赛事按“总用时 + 单步限时”双重要求判罚,比如总时长用完了即使单步没超时也算负。所以单步限时不能直接填满,要留 15% 到 20% 的余量给总时长。我的做法是比赛前统计最近 100 局的单步耗时分布,把 P95 耗时作为配置时的基准,再加一点安全垫。

调参时每次只改一个变量。如果同时改了深度和评估权重,赢了不知道是哪个起作用,输了更不知道。日志里的版本标识在这里价值极大,配合 4.1 的自对弈脚本,每个参数组合都有对应胜率记录,回头看能清楚地知道哪一步改动是收益来源。

4.3 用基线对手做回归测试:每次改动前必跑的三种对局

自对弈能测相对强弱,但测不了“绝对退化”。比如某次改动让引擎针对同版本策略过拟合,自对弈胜率很高,面对真实对手却一塌糊涂。所以我坚持在框架里保留三类固定基线对手,每次改动后都跑一轮回归。

第一类是随机棋手,验证规则完整性和基本稳定性,随机对手都赢不了说明规则层有 bug。第二类是固定策略棋手(比如只吃子、只占中心的简单策略),能反映引擎是否有基本的局面判断力。第三类是上一版引擎本体,也就是最常见的回归测试:旧版可能输给新版,但不应出现大比分倒退。

# 改动后三连跑:随机、固定策略、上一版 python play.py --black v10 --white random --rounds 50 --opening off python play.py --black v10 --white greedy-v3 --rounds 50 --opening off python play.py --black v10 --white v9 --rounds 200 --opening off

三类对局要分开看指标:随机棋手那组看非法走棋率,固定策略那组看胜率和平均步数,上一版那组看胜率曲线是否稳定。如果 v10 对 v9 胜率只有 45%,但单步耗时上升了,说明这次改动性价比不高,应该回退。

这一套回归测试从备赛第二周开始就要固定下来,每次大改动必跑,小改动可以只跑第三类。它不能完全替代正式比赛场景的测试,但能给改动上保险,让团队敢在赛前最后一夜还改代码。

5. 备赛避坑:从入坑到翻车的五个高频问题

下面五条是备赛阶段最容易反复出现的问题,每一条都值得写进自己的检查单。它们有一个共同特征:不是算法难度问题,而是工程细节问题。算法不会的地方可以查资料,工程细节错了,比赛现场连查资料的资格都没有。

5.1 现象:程序总是超时判负

比赛后期频繁出现超时判负,单步限时明明设得足够长。原因多半是时间控制写在搜索完成之后:程序先不管剩余时间,用满深度搜完,再检查用时,这时才发现已经超了。解决方法是把时间检查放进搜索循环内部,比如每搜索一层或每处理一定数量的节点就检查一次 clock,超时就立刻剪枝返回当前最优着法。我习惯在根节点调用前先做一次毫秒级计时,配合分层加深,让程序在时间快用完时主动降深度。

排查时先看日志里最后一步的耗时:如果单步耗时总是集中在一两步上,而其他步很快,基本就是搜索没有在循环内感知时间。加一个计时探针到每层搜索入口,输出递归入口时刻,就能定位是哪一层吞掉了时间。

5.2 现象:评估函数越改越强,自对弈胜率却纹丝不动

给评估函数加了新特征、调了权重,团队感觉“理应更强”,自对弈 200 局后胜率还是 50% 上下。原因有两个:一是新特征和原有特征高度相关,等于没有新增信息;二是搜索深度太低,评估函数的细微变化被搜索噪声掩盖。解决方法是先把搜索深度调上去,让评估差异能被放大,再做特征增减。每次只加一个特征,并且用 4.1 的置信区间判断:胜率变化在 ±5% 内一律视为无变化。

预防手段是维护一张特征变更表,记录每个特征加入的版本号和对局胜率。这样翻车时能快速回退到某个已知良好版本,不用把整个评估函数推倒重来。记住:评估函数不是越复杂越强,信息冗余的特征只会拖慢搜索。

5.3 现象:换机器跑结果不一样,种子与并行导致“玄学”波动

同一个程序在自己电脑上胜率 60%,换到比赛机器上只剩 40%,找不到代码差异。原因通常是两处:随机种子没有显式固定,导致 MCTS 模拟和开局选择每局都不同;或者并行线程数没配置,在核数不同的机器上迭代次数忽高忽低。解决方法是把种子作为显式参数传入,并在配置里固定线程数,保证同版代码在不同机器上的行为可复现。自对弈脚本里的 --seed $i 就是为这个准备的。

验证方法很直接:用同一个种子在比赛机器上跑同一盘对局,如果两次的走法序列不一致,说明程序里还有未被固定的随机源。逐处排查 rand、shuffle、时间相关逻辑,把它们全部收口到一个种子生成器。这一步做完,换机器问题基本消除。

5.4 现象:开局库加载后前期变强,中盘反而崩盘

加了开局库后前十几步几乎不耗时,棋也走得漂亮,但进入中盘后胜率明显下降。原因是开局库覆盖过深,导致中盘搜索的评估视角被开局库带偏,或者开局库本身质量参差,某些分支在残局阶段理论劣势。解决方法是把开局库深度控制在 8 到 12 步,超过这个深度强制交给搜索;同时定期用自对弈验证开局库胜率,低于 50% 的分支要手动删库着。

还有一个细节:开局库命中的走法要参与时间统计,不能因为走了库着就省下时间去深搜其他步,否则总用时分布会畸形。记住:开局库是减少开局耗时的工具,不是中盘棋力来源。

5.5 现象:崩溃、闪退与非法走棋——异常处理比棋力更先被扣分

程序棋力很强,但比赛中途闪退或走出非法着法,直接判负。原因是只测了正常对局流程,没有测异常分支:引擎超时需要返回兜底走法、读棋谱失败时要跳过、输入解析异常时要能恢复。解决方法是准备一个“保底走法”逻辑:任何异常发生时,从当前局面的合法走法里挑一个耗时最短的返回,并记录错误日志。这个逻辑要单独测试,因为比赛现场最不需要的就是“程序崩了之后靠人肉干预”。

测试时故意制造异常场景来验证兜底:把开局库文件改坏、断掉输入流、把搜索深度调到爆栈上限,看程序是否还能吐出一个合法走法。能吐出来,算合格;吐不出来,直接修到合格为止。

6. 把资料吃透:赛前检查单与一套可留用的调试工具箱

6.1 赛前检查单:十个必查项

备赛最后一天不要再看新资料,把时间花在检查单上。我每次出发前都会过一遍下面这张表:

检查项判定标准
单步限时余量P95 耗时不超过限时的 80%
总用时分配预留 15% 余量,不卡线
开局库深度8~12 步,避免中盘失控
异常兜底非法局面能返回安全走法
种子可复现同种子下对局结果一致
版本标识引擎回显版本号,日志一致
日志开关现场日志只记关键字段,避免爆盘
回归测试最近 200 局对上一版胜率 ≥55%
步数上限死局自动和棋,匹配赛事规则
人工对弈至少 5 盘完整下完,不崩不超时

每一项都不是空喊。版本标识最容易遗漏,现场如果出现诡异波动,没有版本号两盘棋根本没法定责。人工对弈不能省,因为它能暴露“自对弈没覆盖到的交互问题”,比如界面卡死、输入解析失败。

6.2 留一套调试工具箱:从“盲改”到“二分定位”

备赛后期最值钱的资料,不是收集来的往年代码,而是自己沉淀的调试工具箱。我长期保留三个脚本:自对弈批量脚本、对局日志分析脚本、配置对比脚本。前者负责跑数据,中间负责把胜率摊到每个改动上分析,后者负责在翻车时快速找出“上次能赢这次不能赢”之间的配置差异。

定位棋力退化的技巧,是把改动历史切成两半,先回退一半版本跑自对弈,再根据胜率决定继续回退还是前进。这个过程类似二分查找,一般两三轮就能锁定引起退化的那次修改。印象很深的一次,某位队友凌晨发消息说引擎突然不会下残局了,我用二分定位后确认是前一天临时加的位置表权重问题,回退那个提交再补测两小时就稳住了。

这个工具箱的投入产出比极高,建议在备赛第一周就建起来。它不需要多复杂的实现,保住对局记录、固定版本标识、每次只改一个参数,这三条纪律就是全部核心。

多年带队下来,我最深的体会是:计算机博弈竞赛辅导资料不是拿来背的,是拿来拆的。把资料拆成规则、算法、框架、数据、测试五个部分,按自己的节奏重装一遍,比赛就不会输在“别人的代码没看懂”上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询