news 2026/9/23 12:41:09

五子棋人机AI实战:评估函数与alpha-beta剪枝搜索实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
五子棋人机AI实战:评估函数与alpha-beta剪枝搜索实现

简介:一款五子棋人机对战系统的VC++完整工程包,面向学习博弈算法与MFC界面开发的学生和开发者,用于理解Minimax搜索、Alpha-Beta剪枝及启发式评估在棋类AI中的落地方式。压缩包共29个文件、约367KB,主要包含.h与.cpp源码、bmp棋盘棋子素材、ico图标以及可直接运行的exe程序;其中dsp/dsw为Visual Studio工程文件,txt和doc用来说明项目使用与开发笔记,db为运行数据文件,结构清晰便于对照学习。目前已有133人学习下载。价值点在于完整源代码配合可执行程序,可边运行边改代码,直观感受计算机决策过程;结合描述中提到的剪枝策略与启发式函数思路,适合在搭建五子棋AI场景时参考,帮助加深对棋类搜索算法的理解。

1. 解压 wuziqi.rar 之后,这份五子棋人机代码该从哪下手

拿到 wuziqi.rar,先别急着改代码,多数打着“五子棋人机”旗号的资源,拆开一看就是“能成五就赢、看见活三就堵”的规则堆叠。这种 AI 前几手像那么回事,一旦对手不按固定棋型走,它立刻变盲人,甚至会在必胜局面下跑去堵一个无关紧要的活二。真正值得投入的是另一条路线:评估函数加博弈树搜索,让 AI 在双方都有可能成五的局面里,通过多步预判选出一条既不送死、又留后手的路。这篇文章不碰网页版人机的前端封装,只讲怎样把这份代码的决策核心跑通、调好、填坑。适合课程设计、做能陪人下棋的小程序,以及想入门博弈搜索的开发者。

2. 先跑通最小人机闭环:棋盘、胜负判定与命令行入口

2.1 两条主流人机路线,为什么先选搜索而非规则表

见过太多同类项目,第一版都写成“如果对方活三就堵,如果自己活四就下”。规则表写起来确实快,半小时能堆出几十条 if,但缺陷也明显:棋型组合几乎是无限的,“跳活三”“眠三加冲四”这些复合局面根本枚举不完。更麻烦的是,规则多了会互相打架,比如“优先成五”和“必须先防守”同时成立时,程序不知道听谁的,只能靠调整 if 顺序来碰运气。

搜索路线不一样。它把“下棋好坏”拆成三个可以独立调整的旋钮:评估函数、搜索深度、候选宽度。评估函数负责回答“当前局面到底谁占优”,搜索负责回答“如果我下这,对手最可能怎么回”,alpha-beta 剪枝只是让这个回答来得足够快。哪怕你以后想上深度学习或者蒙特卡洛树搜索,这套评估函数依然可以作为先验特征接着用。

我的经验是:解压 wuziqi.rar 后,如果发现 AI 代码全是 if else,不要继续打补丁。把它重构成“棋盘层 + 评估层 + 搜索层”三块,后面所有优化都是在这三个模块上做替换,而不是在一坨规则里找 bug。

2.2 落盘后的最小文件划分与运行环境

先解压,再把目录整理成四份代码。这一步的作用是让棋盘逻辑和 AI 逻辑彻底解耦,后面你换成图形界面或者网页版人机,AI 部分一行都不用动。

# 解压 rar 包,二选一 unzip wuziqi.rar # 或者 7z x wuziqi.rar # 建议的最小工程结构 # five/ # ├── main.py # 人机对弈入口,负责交互 # ├── board.py # 棋盘、落子、胜负判定 # ├── eval.py # 评估函数 # └── ai.py # 候选点生成与搜索 python3 --version # 3.8 以上即可,不需要第三方库

代码只依赖 Python 标准库,这是故意为之。五子棋棋盘逻辑很简单,引入 numpy 和 pygame 反而增加环境配置成本。等 AI 逻辑稳定了,再考虑用 pygame 做可视化也不迟。

2.3 棋盘逻辑是地基:十五路棋盘、落子与五连判定

很多人一上来就写 AI,结果棋盘的合法落子判断是错的,后面所有搜索都白跑。这个基础模块必须一次写对。

# board.py class Board: def __init__(self, size=15): self.size = size self.grid = [[0] * size for _ in range(size)] self.move_stack = [] self.last_move = None def play(self, row, col, player): if not (0 <= row < self.size and 0 <= col < self.size): return False if self.grid[row][col] != 0: return False self.grid[row][col] = player self.last_move = (row, col) self.move_stack.append((row, col, player)) return True def undo(self): if not self.move_stack: return row, col, player = self.move_stack.pop() self.grid[row][col] = 0 self.last_move = self.move_stack[-1][:2] if self.move_stack else None def is_full(self): return len(self.move_stack) == self.size * self.size def get_winner(self): if not self.last_move: return 0 r, c = self.last_move return self.grid[r][c] if self._check_five(r, c) else 0 def _check_five(self, row, col): player = self.grid[row][col] directions = ((0, 1), (1, 0), (1, 1), (1, -1)) for dr, dc in directions: count = 1 for sign in (1, -1): r, c = row + sign * dr, col + sign * dc while 0 <= r < self.size and 0 <= c < self.size and self.grid[r][c] == player: count += 1 r += sign * dr c += sign * dc if count >= 5: return True return False def print_board(self): chars = {0: '.', 1: 'O', -1: 'X'} for r in range(self.size): print(' '.join(chars[self.grid[r][c]] for c in range(self.size)))

这里用 1 和 -1 表示黑白双方,而不是用字符串 “B” 和 “W”。原因在后面会体现:negamax 搜索里切换视角就是乘一个 -1,字符串判断会平白多出一堆 if。_check_five以刚落的子为中心,朝某个方向的正反两边数连续同色棋子,合起来达到 5 就赢,起点本身算一枚,所以 count 初始值是 1。

undo方法依赖 move_stack 记录每一步,这样 AI 在搜索时模拟落子后可以精确回退,不需要复制整个棋盘对象。复制棋盘不是不行,但 225 个格子的对象在深度 4、每层 10 个候选点的情况下会被复制上万次,内存和 GC 压力都不小。

2.4 命令行对弈主循环:先让人和 AI 都能“动起来”

棋盘逻辑写好后,先接一个最蠢的随机 AI。这一步不是为了验证棋力,而是验证“人下棋 → AI 下棋 → 判胜负 → 再循环”这条链路是否通顺。

# main.py import random from board import Board def random_move(board): empties = [(r, c) for r in range(board.size) for c in range(board.size) if board.grid[r][c] == 0] return random.choice(empties) if empties else None def play_human_vs_ai(): board = Board() human, ai = 1, -1 human_turn = True while not board.is_full(): board.print_board() winner = board.get_winner() if winner: print('winner:', '黑(你)' if winner == human else '白(AI)') return if human_turn: try: text = input('输入坐标 row,col(如 7,7): ') r, c = map(int, text.strip().split(',')) except Exception: print('格式错误,重新输入') continue if not board.play(r, c, human): print('该位置不可落子,重新输入') continue else: move = random_move(board) if move: board.play(*move, ai) print('AI 落子:', move) human_turn = not human_turn print('平局') if __name__ == '__main__': play_human_vs_ai()

坐标输入统一走 “row,col” 带逗号的格式,避免命令行列坐标分不清。board.play返回 False 时,说明越界或者位置已占用,主循环里的人机分支负责捕获并重新输入。AI 分支不需要处理这个问题,因为随机选点不会选到非空位。

这版跑通后,你的五子棋地基就算立住了。后面把random_move换成贪心、再换成搜索,主循环基本不用动。如果将来要做网页版人机,区别只是输入从终端变成 HTTP 或 WebSocket 帧,棋盘判定和 AI 逻辑原样保留。

3. 让 AI 学会“看棋形”:评估函数与第一版贪心落子

3.1 棋形翻译成数字:为什么“活三”比“冲四”分数难定

人下五子棋时看的是活四、冲四、活三、眠三这些棋形。机器没有棋感,只能看数字。评估函数要做的就是把局面翻译成一个数值:数值越大,对当前玩家越有利。

分数设计的核心是拉开数量级差距。活四两端都开放,下一步无论对手堵哪头,你都能从另一头成五,基本等于胜利,所以分数是十万级。冲四只有一端开放,对手还能堵,但你不下就会被对方抢先,所以给一万级。活三给五千,因为它下一步能变成活四,威胁真实存在;眠三只有一千,因为它想成活四需要更多手数,对手有反应时间。

棋形直观形状分值建议
五连XXXXX1000000
活四.XXXX.100000
冲四.XXXXX 或 XXXX.10000
活三.XXX.5000
眠三XX.X. 等1000
活二.XX.200
眠二X.X. 等50

注意这是比例关系,不是标准答案。相邻棋形之间必须保持数量级差距,比如活三和眠三至少差 3 到 5 倍。如果活三和眠三只差 1.5 倍,AI 在实战中会为了一个并不急迫的眠三,放弃真正能形成杀棋的活三。

3.2 方向扩展法评估:从落子点出发数棋形

五子棋棋形是方向性的。横、竖、两条对角线一共四个方向,评估函数必须同时覆盖。这里的实现方式是:对棋盘上每个己方棋子,朝四个方向分别数“连续同色棋子数”和“开放端数”,然后按棋形表加分。

# eval.py def evaluate_point(board, row, col, player): score = 0 directions = ((0, 1), (1, 0), (1, 1), (1, -1)) for dr, dc in directions: count = 1 open_ends = 0 for sign in (1, -1): r, c = row + sign * dr, col + sign * dc while 0 <= r < board.size and 0 <= c < board.size and board.grid[r][c] == player: count += 1 r += sign * dr c += sign * dc if 0 <= r < board.size and 0 <= c < board.size and board.grid[r][c] == 0: open_ends += 1 if count >= 5: score += 1000000 elif count == 4 and open_ends == 2: score += 100000 elif count == 4 and open_ends == 1: score += 10000 elif count == 3 and open_ends == 2: score += 5000 elif count == 3 and open_ends == 1: score += 1000 elif count == 2 and open_ends == 2: score += 200 elif count == 2 and open_ends == 1: score += 50 return score def evaluate_board(board, player): total = 0 for r in range(board.size): for c in range(board.size): if board.grid[r][c] == player: total += evaluate_point(board, r, c, player) return total

这段逻辑的核心是“从当前棋子出发,朝一个方向延伸到边界或者对手棋子为止”。延伸过程中遇到空位,说明这一端是开放的,open_ends 加一;遇到对手棋子或者棋盘边界,说明被堵死。比如棋盘上有一排白棋 “.XXX.”,对中间那个 X 和两边的 X 分别做方向扩展,每个点都会得到 count=3、open_ends=2,于是这个活三总共贡献 15000 分。

一个不得不提的细节:上面的实现对同一个棋段会重复计分。一个活三里的三个棋子,每个都能扩展出这个活三,所以总分会是单点分值的 3 倍。这个重复放大对黑白双方是等价的,所以在排序和搜索里不影响相对局面判断。如果你强迫症发作想去重,需要在棋段的起始端点开始只统计一次,但那样代码会复杂不少,对棋力提升有限,建议先留着。

3.3 候选点生成:AI 的第一版贪心落子

评估函数有了,AI 就能“看”局面了。第一版 AI 不做搜索,只做一件事:遍历所有空位,模拟自己落子后调用评估函数打分,选分数最高的点落下。这就是贪心落子。

# ai.py from eval import evaluate_board def generate_candidates(board, player, top_n=10): scored = [] for r in range(board.size): for c in range(board.size): if board.grid[r][c] != 0: continue board.play(r, c, player) attack = evaluate_board(board, player) defense = evaluate_board(board, -player) board.undo() scored.append((attack - 0.9 * defense, r, c)) scored.sort(key=lambda x: x[0], reverse=True) return [(r, c) for _, r, c in scored[:top_n]] def greedy_move(board, player, top_n=10): if not board.move_stack: # 先手第一手直接落天元,避免全盘 0 分导致随机乱选 return board.size // 2, board.size // 2 return generate_candidates(board, player, top_n)[0]

注意 generate_candidates 里打分用的是attack - 0.9 * defense,不是只算己方攻击分。如果只看 attack,AI 会对一个角落里的眠二情有独钟,因为那里“全是自己的棋子”,而对手已经摆好的活三反而没人管。0.9 就是防守权重,含义是“对手的威胁略微优先于我自己的发展”,默认 0.9 是因为五子棋防守反击比盲目进攻更稳健。

替换 main.py 里的random_move

from ai import greedy_move # 把 AI 分支改成 r, c = greedy_move(board, ai) board.play(r, c, ai) print('AI 落子:', r, c)

跑几盘你会发现,贪心 AI 会堵活三、会占中心,但它的棋没有连贯性:它看不到“我现在冲四,对手必须堵,然后我可以在另一个方向成活三”。这正是第 4 章搜索要解决的问题。

4. 加深度才有棋感:minimax 加 alpha-beta 剪枝的实现与三个调参点

4.1 为什么是搜索而不是“多看几步的贪心”

贪心的本质是只看当前盘面,对“对手下一手会怎么走”完全没有建模。五子棋里的强制交换很常见:我冲四,你只能堵;我乘机在另一个方向形成活三,你又要来堵。这种多步棋型只有搜索才能看到。

minimax 模拟“我走一步 → 对手走一步 → 我再走一步”的对抗序列。它假设对手永远选择对我最不利的那手,而我选择在对手最坏回应下依然对我最有利的那手。五子棋没有围棋里的劫争,规则简单,搜索树结构非常规整,用 negamax 写法最顺手。

alpha-beta 剪枝是搜索的加速器。核心判断是:如果当前分支已经比之前找到的最优解更差,就停止展开这个分支。它不改变搜索结果,只是把无效分支剪掉。开局阶段搜索空间接近 225 的阶乘,不剪枝的话深度 4 都跑不动,剪完之后深度 4 加候选宽度 10 可以稳定在秒级返回。

4.2 负极大值加剪枝:搜索模块完整实现

negamax 的关键是视角反转。当前玩家视角下的正分,在对手看来就是负分,所以递归调用时把返回值取负、搜索结果取负,alphabeta也要交换位置并取负。

# ai.py 追加以下内容 def negamax(board, depth, alpha, beta, player): winner = board.get_winner() if winner == player: return 1000000 + depth if winner == -player: return -1000000 - depth if depth == 0 or board.is_full(): return evaluate_board(board, player) best = -float('inf') for r, c in generate_candidates(board, player, top_n=10): board.play(r, c, player) score = -negamax(board, depth - 1, -beta, -alpha, -player) board.undo() if score > best: best = score if score > alpha: alpha = score if alpha >= beta: break return best def best_move(board, player, depth=4, top_n=10): if not board.move_stack: return board.size // 2, board.size // 2 alpha = -float('inf') beta = float('inf') best_score = -float('inf') best = None for r, c in generate_candidates(board, player, top_n): board.play(r, c, player) score = -negamax(board, depth - 1, -beta, -alpha, -player) board.undo() if score > best_score: best_score = score best = (r, c) if score > alpha: alpha = score return best

几个细节要解释清楚。

胜负分支返回的分数带上了 depth,是为了让 AI 在同样能赢的分支里选择“更快赢”的那条。因为越深的地方发现胜利,剩余深度越小,1000000 + depth会略微偏大一点,搜索会优先走这条短路径。

generate_candidates在每层递归都会重新对空位打分排序,这个开销不小,但换来的剪枝收益更大。排序好的候选让 alpha-beta 能更早触发剪枝,效率反而比“不排序但展开全部节点”高一个量级。

best_move的顶层没有在alpha >= beta时 break,这是故意的。顶层少剪一个分支对性能影响微乎其微,但我可以在后续版本里改成收集所有同分候选然后随机挑一个,避免 AI 棋风死板。

4.3 把搜索接回命令行对弈

现在把第 3 章的贪心替换成best_move

# main.py 中替换 AI 分支 from ai import best_move # ... else: r, c = best_move(board, ai, depth=4, top_n=10) board.play(r, c, ai) print('AI 落子:', r, c)

这版跑起来你会立即感受到两个变化。第一是 AI 落子明显变慢,深度 4 加候选 10 大约要 1 到 3 秒,具体取决于分支剪枝效率。第二是它的棋风变了,不再像贪心那样“哪里分高下哪里”,而是会刻意制造双威胁。

注意主循环调用best_move前必须先判断棋局未结束、棋盘未满,否则空棋盘或已胜负分的情况下调用会产生奇怪结果。前面 main.py 的 while 循环已经做了这两个检查,所以直接接入即可。

4.4 三个必调参数:深度、候选宽度、攻防权重

参数是这份代码的命门,调参顺序比调参本身更重要。

参数默认值建议区间调大表现调小表现
depth 搜索深度42~6棋力明显增强,耗时指数增长反应快,但看漏连杀
top_n 候选宽度105~16不容易漏掉关键防守点,慢速度快,但可能无视对手活三
攻防权重 0.90.90.7~1.2更敢进攻,偶尔漏防偏保守,光堵不攻

深度 4 加候选 10 是性价比最高的起步组合。想上深度 6,候选宽度必须降到 6 到 7,否则单步思考时间会飙到十几秒。深度再往上,就要做迭代加深和历史启发,让 AI 先快速搜一个浅层结果作为参考排序,再逐步加深,超时就直接用上一次的结果。这是普通课程设计用不到的优化,不展开。

一个容易被忽略的是候选点的空间范围。如果棋盘已经下了二十几手,大部分空位离战场很远,让 AI 遍历全部 225 个空位纯属浪费。常见的做法是把候选限制在“已有棋子周围两格”的区域内,超出区域的空位直接跳过。这样做对棋力几乎没有损失,但搜索时间可能缩短一半。

5. 五子棋 AI 常见的五个翻车点:现象、原因与排查

5.1 现象一:AI 面对双活三不防守,跑去扩展自己的棋

双活三是必杀局面,只要对手走出来,你怎么堵都堵不住两个头。如果 AI 此时还在角落下自己无关紧要的眠二,说明它根本没意识到威胁。

原因多半出在候选点生成上。generate_candidatesattack - 0.9 * defense排序,如果己方某个区域的攻击分因为重复计分被过分放大,防守点就会被挤到 10 名之外。

解决方法是先调参验证:把 top_n 从 10 改成 16 试试,如果防守点进来了,就是候选窗口太小;如果还是进不来,把 defense 的系数临时改成 1.2,再跑同一局面。更直接的排查手段是打印generate_candidates的前 10 个候选,肉眼看防守点是否在列。这个打印习惯能省掉大量瞎猜的时间。

5.2 现象二:棋盘上明明有成五点,AI 却不下

这个翻车现象最有迷惑性,看起来像是 AI 智商掉线,实际是候选排序问题。成五点虽然能得 1000000 分,但如果对手也有一个高威胁棋形,defense 分量会把防守点排到成五点前面,AI 为了“防守”而放弃必胜一手。这在深度浅的时候尤其常见。

解决方法是给成五点绝对优先级。在generate_candidates开头先扫描一遍空位,模拟落子后如果board.get_winner()等于当前玩家,直接返回这个点,不做任何打分排序:

def find_winning_move(board, player): for r in range(board.size): for c in range(board.size): if board.grid[r][c] != 0: continue board.play(r, c, player) if board.get_winner() == player: board.undo() return (r, c) board.undo() return None

best_move开头调用这个函数,有必胜点就先走。这个模块同时也能加速搜索,因为成五点根本不需要进入 alpha-beta 去算。

5.3 现象三:同一局面,改成深度 6 后 AI 反而更差

深度调大棋力应该更强,这是直觉。但实际测试中经常出现深度 4 能赢的棋,深度 6 反而走错。原因多数在胜负分支的返回值上。

negamax 里winner == player返回1000000 + depthwinner == -player返回-1000000 - depth。这两个值看起来对称,但如果 depth 在不同层数传入时没有保持一致,就会出现“黑棋越深越觉得自己要输”的错觉。排查办法是在 negamax 入口加一行临时打印,记录 depth、player、alpha、beta 和返回分数,然后用固定棋谱复盘。看到符号异常,基本就是胜负分支的返回值不对称。

5.4 现象四:AI 每次同一局面走同一手,被人抓住套路

这不是 bug,但影响对弈体验。best_move里一旦找到分数更高的候选,就直接替换掉旧候选,同分情况下保留先遇到的,所以棋风极其固定。

解决方法是收集所有同分候选,随机选一个:

best_moves = [] for r, c in generate_candidates(board, player, top_n): board.play(r, c, player) score = -negamax(board, depth - 1, -beta, -alpha, -player) board.undo() if score > best_score: best_score = score best_moves = [(r, c)] elif score == best_score: best_moves.append((r, c)) return random.choice(best_moves)

这样 AI 在多个等效着法之间随机选择,人类的“针对性开局”就没那么奏效了。这五子棋里尤其有用,很多布局思路都是针对固定应手设计的。

5.5 现象五:单步耗时忽快忽慢,完全没法预估

alpha-beta 剪枝是一个非线性过程。候选排序质量高,剪枝触发早,1 秒出棋;候选排序紊乱,搜索几乎全量展开,8 秒也不奇怪。

要降低方差,先保证候选排序的质量。generate_candidates里的排序分数已经是攻防综合分,但如果发现耗时波动很大,可以改成先按 attack 降序排列,再在 attack 相近的候选里按 defense 微调。攻击点优先占据搜索树的上层,剪枝效果通常更好。

另一招是限时搜索。记录搜索开始时间,在 negamax 每层递归开头判断是否超时,超时就抛出异常或者返回当前已有最优解。这个方案实现简单,实战效果不错,代价是超时的那一层可能只搜了一半,结果会略微不稳定。

注意:以上五个现象全部建立在棋盘逻辑正确的前提下。如果_check_five有 bug,AI 会基于错误信息做决策,所有排查方向都白费。遇到诡异棋谱,先用第 5.6 节的回归方法验证棋盘层。

5.6 手工棋谱回归:改完参数怎么验证

给 AI 摆一个“白棋活三,黑棋必须堵端头”的简单棋谱,用脚本自动断言 AI 的落子位置。以后每次改权重、改搜索,都跑一遍这批用例,快速发现退化。

# regression.py from board import Board from ai import best_move cases = [ { "grid": [ [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0], ], "player": -1, "expect": [(0, 2), (4, 2)] }, ] for i, case in enumerate(cases): board = Board(size=5) board.grid = [row[:] for row in case["grid"]] board.move_stack = [ (r, c, board.grid[r][c]) for r in range(5) for c in range(5) if board.grid[r][c] != 0 ] board.last_move = board.move_stack[-1][:2] move = best_move(board, case["player"], depth=2, top_n=5) assert move in case["expect"], f"case {i} failed, got {move}" print(f"case {i} passed: {move}")

回归棋谱不用多,十几个经典局面就够。每加一条就相当于给 AI 上了一道保险。

6. 进阶:自对弈质检、必杀检测和开局库,让 AI 从能下到敢下

自对弈是最好的质检手段。把 AI 拆成两半,黑方深度 4、白方深度 6,如果白方不能稳定赢下大多数对局,说明评估函数或权重比例有问题。配合最简单的主循环,就能跑出结果。

from board import Board from ai import best_move def play_game(depth_black, depth_white, top_n=6): board = Board() turn = 0 while not board.is_full(): player = 1 if turn % 2 == 0 else -1 depth = depth_black if player == 1 else depth_white r, c = best_move(board, player, depth=depth, top_n=top_n) board.play(r, c, player) if board.get_winner(): return player turn += 1 return 0

深度高的那一方如果胜率不到七成,问题基本出在评估函数而不是搜索。比如活三分数给得太高,AI 只顾自己发展,忽略了对手的连续冲四。

必杀检测是性价比最高的加速手段。搜索之前先看一眼有没有直接成五的点,有就立刻返回,没有才进入 alpha-beta。这一点在第 5 章已经实现过,直接加在best_move开头即可。对“双活三”“冲四活三”这类复合必杀,需要额外枚举棋形,工作量翻倍,一般课程设计不推荐做到那一步。

开局库则是个讨巧的技巧。五子棋前四手基本是定式,不值得用搜索慢慢算。把常见开局存成一个列表,前几步随机挑一个走,第五手开始交给搜索,既省时间又让 AI 看起来更像人类:

OPENINGS = [ [(7, 7), (7, 8), (8, 7)], [(7, 7), (8, 8), (7, 8)], [(7, 7), (8, 7), (6, 8)], ]

按照board.move_stack的步数查表,查不到就进搜索。预算充足的话,可以往后存到十步,AI 在开局阶段的棋力会突然显得很强。

我以前第一次写五子棋人机时也走的是纯规则表,三百多行 if 堆完,AI 还是被一个跳活三骗得团团转。后来改成评估加搜索,才理解这类博弈 AI 的棋力不在灵光一现,而在评估函数是否诚实、搜索是否把对手的反击算进去。调防守权重时我翻过一次车,AI 从积极进攻变成无限防守,最后是靠自对弈棋谱定位到问题。多下几盘、多打印候选,比猜参数靠谱得多。希望帮到你。

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

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

5个坑点:cdr12实战项目里API全变了的避坑指南

5个坑点:cdr12实战项目里API全变了的避坑指南 版本升级后 API 全变了,这是很多开发者在接手旧项目或升级环境时的噩梦。特别是在涉及 cdr12 这类底层通信或特定框架封装的实战项目中,看似简单的版本迭代,背后往往是接口定义、回调机制甚至内存管理逻辑的彻底重构。如果你正在维护一个依赖…

作者头像 李华
网站建设 2026/9/23 12:40:52

20000大写处理卡死?重构这段代码,性能提升90%

20000大写处理卡死?重构这段代码,性能提升90% 上周一个老学员在群里炸了,说项目上线后,财务模块的账单导出功能直接卡死,服务器 CPU 飙到 100%。他查了半天,发现是那个把数字转成“人民币大写”的函数在作怪。 这种 版本升级后 API 全变了 或者逻辑重构导致性能雪崩的情况,在面试里也是…

作者头像 李华
网站建设 2026/9/23 12:40:36

Android 10 刷新率切换机制详解:从 Display.Mode 到应用层实践

1. 从 Android 10 开始&#xff0c;刷新率不再是一个“只读属性”如果你在 Android 9 及以前做过显示相关的开发&#xff0c;大概率会有这样一个印象&#xff1a;屏幕刷新率是系统底层和硬件之间的事&#xff0c;应用层能做的事情非常有限。大多数情况下&#xff0c;你只能通过…

作者头像 李华
网站建设 2026/9/23 12:40:36

单片机程序设计面试避坑:3个核心原理配完整示例

单片机程序设计面试避坑:3个核心原理配完整示例 面试官盯着你,眼神锐利:“说说中断优先级怎么定的?为什么你的代码在调试时正常,一上电就死机?”你脑子里一片浆糊,明明背了八股文,但一到具体场景就卡壳。这种 面试被问原理答不上来…

作者头像 李华
网站建设 2026/9/23 12:40:31

面试必问:状态观测器3大经典报错,90%的人没搞懂

面试必问:状态观测器3大经典报错,90%的人没搞懂 上周陪一个学员模拟面试,他对着白板手舞足蹈讲了半天,面试官只问了一句:“你的状态观测器在异步任务里怎么同步状态的?”他愣了五秒,眼神开始飘忽。这就是典型的“背了八股文,但没踩过坑”。 状态观测器(State Observer)这个词,在 Java…

作者头像 李华
网站建设 2026/9/23 12:40:31

做主播需要什么设备?3类高频面试题拆解

做主播需要什么设备?3类高频面试题拆解 官方文档往往长篇大论,初学者很难在第一时间抓住核心配置逻辑。面对“做主播需要什么设备”这类看似生活化实则考察系统思维与硬件底层原理的高频面试题,许多应届生容易陷入参数堆砌的误区。…

作者头像 李华