简介:搜索算法是人机博弈系统的核心技术之一,而五子棋AI正是典型的搜索应用场景。通过贪心策略生成候选点,能在大棋盘上快速筛出关键落子区域,大幅压缩状态空间;配合极大极小博弈树与alpha-beta剪枝,则能在有限深度内推演攻防、评估局势。两者串联,可兼顾响应速度与棋力,是许多游戏AI的通用范式。这类算法不仅适用于五子棋,也广泛用于棋牌类人机对弈、策略游戏决策等场景。本文从候选点生成、棋型评估、树搜索剪枝到线程超时处理,围绕五子棋人机博弈的完整实现展开,帮助读者掌握从算法原理到工程落地的关键路径。
1. 从“基于贪心策略直接搜索算法和极大极小博弈树算法的智能人机博弈五子棋游戏”这个标题里能看到什么
如果某导师在答辩现场问:你这个五子棋程序,到底是凭什么决定下一步的?你总不能只回一句“用了搜索”。而《基于贪心策略直接搜索算法和极大极小博弈树算法的智能人机博弈五子棋游戏》这个标题,已经把两个关键词送到了你面前:一个负责从 200 多个空位里快速挑出候选点,一个负责在候选点里推演对手的反击。这不是一套花哨包装,而是五子棋人机博弈项目里最实用的技术组合。
这个题目适合两类人:一类是做毕业设计,需要一份既能跑、又能讲出算法的完整源码;另一类是已经写过简单五子棋,但觉得 AI 太笨,想知道怎么把“搜索”和“评估”串起来的人。读懂这条主线之后,你至少能回答三个问题:候选点怎么来、博弈树怎么搜、评分表怎么定。
2. 先分清楚两种搜索策略:贪心直接搜索负责“快”,极大极小博弈树负责“深”
这个标题把两个算法并列,很容易让人以为是一套复杂流程。实际上它的分工非常朴素:贪心直接搜索负责把候选落点从两百多个压到二三十个;极大极小博弈树负责在这二三十个点里往后推演三四步。这两个算法是串行关系,不是竞争关系。没有贪心剪枝,纯极大极小在标准五子棋棋盘上跑不动;没有极大极小,贪心每次都只看一步,攻防水平太低。先理解这条主线,后面看代码才不会迷路。
2.1 为什么多数五子棋人机不用纯极大极小:搜索空间太大,先落子决定体验
纯极大极小的原理很简单:模拟双方轮流落子,假设对手每次都会选择对我方最不利的回应,在当前盘面上找出能让终局收益最大的那一步。问题出在分支数量上。15 路棋盘开局后空位大约 200 个,假设搜索深度为 4,穷举是 200 的 4 次方,也就是 16 亿个局面;即便每个局面评估很快,普通机器也无法在秒级返回。如果只搜 2 层,AI 能看到“对方下一步要连五”这种直接威胁,但看不到自己两步之后活三会被断,棋力很弱。因此实战中必须先把“每层要考虑的点”压缩到可控数量,这正是标题里“贪心策略直接搜索”要承担的任务。
这里有个常见的理解偏差:直接搜索不是“不走树搜索、直接下子”。它的意思是,先按某个打分函数把空位排序,只取前 N 个作为树搜索的子节点。它砍掉的是大量明显不重要的边角空位,而不是砍掉搜索深度。真正决定棋力的,还是后面那棵极大极小博弈树。
2.2 贪心直接搜索在五子棋里的真实角色:从候选点里找局部最优而不是全局乱搜
五子棋里,“好点”总是围绕已有棋子成线的地方。离所有棋子都超过两格的空位,一步之内不可能产生影响,把它纳入树搜索只会浪费算力。候选点生成就是把“值不值得算”这个决策先做掉。常见做法有两个约束:半径约束,只统计距任一棋子半径不超过 2 的空位;数量约束,对半径内所有空位先打一次分,取 Top 24 或 Top 32。
下面是一个简化但能跑通的候选点生成函数:
def generate_candidates(board, color, limit=24, radius=2): candidates = {} for r in range(len(board)): for c in range(len(board[0])): if board[r][c] == 0: continue for dr in range(-radius, radius + 1): for dc in range(-radius, radius + 1): if dr == 0 and dc == 0: continue nr, nc = r + dr, c + dc if not (0 <= nr < len(board) and 0 <= nc < len(board[0])): continue if board[nr][nc] != 0: continue # 先给每个候选点一个局部估值,既能排序又能帮助剪枝 candidates[(nr, nc)] = point_score(board, nr, nc, color) ordered = sorted(candidates.items(), key=lambda kv: kv[1], reverse=True) return [pos for pos, _ in ordered[:limit]]参数上,limit 我一般不会超过 32,因为 4 层搜索下 32 个候选仍然有约 100 万节点,必须靠 alpha-beta 剪枝才能压到可接受范围。机器较差就先取 20。radius 设成 2 通常够用;设成 1 会让 AI 对两格之外的棋型反应迟钝,比如对手在两个方向各放了两颗子,中间隔一格的进攻点会被漏掉。
注意:point_score 只是贪心评分,不是最终决策。它可以让候选点排序靠前、优先被树搜索扩展,但被扩展之后是否被选中,最终由极大极小博弈树决定。这就是“贪心直接搜索”和“全局穷举搜索”的差别:贪心只负责缩窄窗口,不负责下最终结论。
2.3 极大极小博弈树和 alpha-beta 剪枝:怎么在有限深度内反推对手意图
极大极小的核心是“轮换视角”:自己走一步时取子树最大值,对手走一步时取子树最小值。完整博弈树要能算到终局,但五子棋棋盘不允许;实际工程都用有限深度。深度为 3 的含义是:AI 当前落子 → 对手最优回应 → 我方最优应对。深度为 4 还要再模拟一轮对手的干扰,进攻会明显更稳。深度超过 4 之后收益不再线性增加,成本却指数上升。
配合 alpha-beta 剪枝,节点数可以大幅下降。alpha 是当前我方已经能保证的最低收益下限,beta 是对手能接受的最低损失上限。只要某个子节点的返回值让 beta 降到 alpha 以下,后面的兄弟节点就不再扩展。前提是子节点排序足够好,否则剪枝效果会大打折扣。下面是一个带剪枝的中枢函数:
INF = 10 ** 9 def minimax(board, depth, alpha, beta, is_maximizing, ai_color, time_keeper): winner = get_winner(board) if winner != 0 or depth == 0 or time_keeper.expired(): return evaluate_board(board, ai_color) if is_maximizing: best = -INF for move in generate_candidates(board, ai_color): board[move[0]][move[1]] = ai_color best = max(best, minimax(board, depth - 1, alpha, beta, False, ai_color, time_keeper)) board[move[0]][move[1]] = 0 alpha = max(alpha, best) if beta <= alpha: break return best else: best = INF opponent = 3 - ai_color for move in generate_candidates(board, opponent): board[move[0]][move[1]] = opponent best = min(best, minimax(board, depth - 1, alpha, beta, True, ai_color, time_keeper)) board[move[0]][move[1]] = 0 beta = min(beta, best) if beta <= alpha: break return best几个值得说的细节:第一,胜负判断要放在深度判断之前,否则盘面已经五连,AI 还会继续计算。第二,轮到对手时使用 generate_candidates(board, opponent),让对手的候选点也按它的棋型排序,剪枝才更准确。第三,time_keeper.expired() 放进递归入口,保证线程不会卡死。第四,根节点要用带返回最优落点的包装函数,因为 minimax 本身只返回数值。
到这里,搜索主干已经完整了。搜索质量取决于 generate_candidates 和 evaluate_board 是否真的符合棋理,这是后面两章要展开的重点。
3. 把完整源码拆成三层:复盘一个可答辩的五子棋人机项目该怎么组织
拿到一套“完整源码”后,第一步不是反复翻主文件,而是看目录。毕业设计答辩里,老师除了问算法,还会问工程怎么组织。如果 AI、界面、规则判断全塞在同一个文件里,就算算法能跑,也很难让人相信你是“设计”出来的。我更喜欢把项目拆成三层:游戏状态层、AI 决策层、界面表现层。这样做能给后续换界面、换难度、加复盘都留出余地。
3.1 目录与模块划分:选一个能讲清楚的最小结构
结构不用复杂,但必须有边界。我常见的最小结构如下:
gobang/ ├── main.py # 程序入口,拼装各模块 ├── game/ │ ├── board.py # 棋盘状态、落子、悔棋 │ └── judge.py # 胜负判断 ├── ai/ │ ├── evaluator.py # 棋型识别和评分 │ ├── candidates.py # 候选点生成,也就是贪心直接搜索 │ └── search.py # 极大极小博弈树 + alpha-beta 剪枝 └── ui/ ├── console_ui.py # 控制台演示版本 └── tkinter_ui.py # 图形界面版本main.py 只做一件事:从 ui 里选一个界面类实例化,再把 Board 对象和 AI 对象按依赖关系传进去。为什么要分这么细?因为答辩时你演示控制台版,能更容易跟踪 AI 的每一步决策;演示图形版,负责“看起来完整”。两套入口共用同一套 AI 模块,这本身就是可验收的工程性证据。
3.2 数据模型:棋盘、落子、胜负与悔棋的状态怎么设计
棋盘状态我用二维数组,0 表示空,1 表示黑,2 表示白。为什么不用字符串或位棋盘?因为五子棋棋盘小,二维数组最直观,调试时可以打印,评分函数也容易按方向遍历。位棋盘更高效,但除非你已经很熟练,否则在毕业设计阶段容易引入不必要的复杂度。
单靠二维数组不够,我还维护一个落子列表。这个列表直接支撑悔棋、复盘和“最后一步高亮”三个 UI 功能。最小实现:
EMPTY, BLACK, WHITE = 0, 1, 2 class Board: def __init__(self, size=15): self.size = size self.grid = [[EMPTY] * size for _ in range(size)] self.moves = [] def place(self, row, col, color): if not (0 <= row < self.size and 0 <= col < self.size): return False if self.grid[row][col] != EMPTY: return False self.grid[row][col] = color self.moves.append((row, col, color)) return True def undo(self, steps=2): for _ in range(steps): if not self.moves: break row, col, _ = self.moves.pop() self.grid[row][col] = EMPTY def snapshot(self): # 传给 AI 的浅拷贝,防止 AI 在思考时污染正式棋盘 return [row[:] for row in self.grid]undo 默认回退两步,是因为“玩家-电脑”交替下棋时,悔棋通常要同时撤销双方最近一手。snapshot 也很关键:AI 搜索时会不断尝试落子再撤销,如果直接操作正式棋盘,一旦某条路径没撤销干净,棋局就毁了。让 AI 在自己拷贝的棋盘上推演,界面层只负责展示 Board 对象,是更稳的做法。
3.3 界面层与 AI 引擎的接口:不要把你的 AI 写死在按钮事件里
我在不少学生源码里看到的典型问题:点击棋盘后,事件处理函数里直接写了一大段搜索逻辑。这样做的后果是:想加难度选择,发现要复制三份;想换图形库,发现逻辑和按钮绑死;想在控制台里调试,发现无从下手。正确的接口应该长这样:
# ai/engine.py def request_move(board_snapshot, ai_color, depth=2): """ 返回 (row, col)。 board_snapshot 是二维数组,ai_color 是 AI 执的棋子颜色。 搜索期间不修改原始棋盘。 """ candidates = generate_candidates(board_snapshot, ai_color) move, score = search(board_snapshot, candidates, depth, ai_color) return move界面只调用 request_move,拿到结果再调用 board.place。这样一来,控制台界面可以用同一条链路跑;换一个 UI 库不会动任何 AI 代码;写单元测试时可以直接构造棋面数组调用 request_move,不需要打开窗口。
难度也不需要重新实现算法,只需要把 depth 和 limit 做成参数。我给“简单、普通、困难”三档的做法是:简单 depth=1,limit=8;普通 depth=2,limit=20;困难 depth=4,limit=24,并加 2 秒超时。这样设计的好处是,答辩现场把难度从普通切到困难,观众不需要看懂代码,也能明显感到 AI 的防守和进攻节奏变了。
4. 从贪心候选到极大极小搜索:一段可以直接抄的 AI 核心实现
接口定好之后,接下来要做的就是把三个核心函数填实:评估函数、候选点生成、带剪枝的搜索。这三部分各自有坑,但配合起来以后,“智能感”就会出来。
4.1 评估函数:棋型拆解与分值表
评估函数最忌讳的是“看单个棋子周围有几颗子”,因为五子棋的威胁是沿线的。正确思路是:对一个空点,分别沿横、竖、两条斜线共四个方向,看如果在这里放进一颗棋子,会形成什么棋型。所谓棋型,就是“连续同色棋子数 + 两端开放性”的组合。我常用的评分表如下:
| 棋型 | 分值 | 含义 |
|---|---|---|
| 连五 | 10000000 | 已经赢了,任何防守都不如它 |
| 活四 | 500000 | 两端都开放,下一手必然成五 |
| 冲四 | 20000 | 只有一端开放,或跳形四连 |
| 活三 | 4000 | 两端开放的三连,下一步能变活四 |
| 眠三 | 300 | 被堵住一端的三连或跳三 |
| 活二 | 200 | 两端开放的二连 |
| 眠二 | 10 | 没有形成连续威胁的二连 |
为什么活四和冲四差距要拉这么大?活四意味着无论对方下哪里,你都能靠另一个端点直接成五,对手无法用一手棋同时堵死。而冲四虽然能逼对手下必应手,但只是给自己创造先手权,并不代表已经获胜。如果活四给 100,冲四给 90,AI 就可能为了连续冲四而放弃活四的必杀。评分表一旦出现倒挂,后面再调搜索深度也补不回来。
一个能直接用的方向统计函数可以这样写:
def line_shape(board, r, c, dr, dc, color): # 假设 (r, c) 已经放入了 color 棋子,统计该方向的棋型 length = 1 open_ends = 0 for sign in (1, -1): step = 1 while True: nr, nc = r + sign * step * dr, c + sign * step * dc if not (0 <= nr < len(board) and 0 <= nc < len(board[0])): break if board[nr][nc] == color: length += 1 step += 1 elif board[nr][nc] == EMPTY: open_ends += 1 break else: break if length >= 5: return "FIVE" if open_ends == 2: return {4: "OPEN_FOUR", 3: "OPEN_THREE", 2: "OPEN_TWO"}.get(length, "NONE") if open_ends == 1: return {4: "FOUR", 3: "THREE", 2: "TWO"}.get(length, "NONE") return "NONE"注意这里把两个方向的信息合并在 length 和 open_ends 里。open_ends 只在第一次遇到空位时加 1,继续走到底遇到边界或对方棋子就停止。这个简化模型不能识别“一侧是空、再往外一格又是同色棋子”的跳活三。对毕业设计而言可以先接受,后面第 6 章会讲怎么加强。
评估整个盘面时,我会遍历全部空点或候选点,对每个点分别计算自己四种方向和对手四种方向的分数,再汇总。判断是否连五时,必须用全盘扫描而不是只扫候选点。
4.2 候选点生成:贪心策略怎么剪掉绝大多数无效落点
候选点生成在前文已有基础版本,这里给出攻守合一的版本。做法是:对每个候选空位,分别计算“如果我在这里落子”的得分和“如果对手在这里落子”的威胁,用加权和排序。这样 AI 不仅会找自己的进攻点,也会把对手的关键点优先搜出来。
def point_score_for(board, r, c, color): score = 0 for dr, dc in ((1, 0), (0, 1), (1, 1), (1, -1)): board[r][c] = color shape = line_shape(board, r, c, dr, dc, color) board[r][c] = EMPTY score += SCORES[shape] return score def generate_candidates(board, color, limit=24, radius=2): opponent = WHITE if color == BLACK else BLACK result = {} # 第一步:按半径收集空点 for r in range(len(board)): for c in range(len(board[0])): if board[r][c] == EMPTY: continue for dr in range(-radius, radius + 1): for dc in range(-radius, radius + 1): nr, nc = r + dr, c + dc if not (0 <= nr < len(board) and 0 <= nc < len(board[0])): continue if board[nr][nc] != EMPTY or (nr, nc) in result: continue attack = point_score_for(board, nr, nc, color) defense = point_score_for(board, nr, nc, opponent) result[(nr, nc)] = attack + 0.9 * defense # 第二步:按分数排序并截断 ordered = sorted(result.items(), key=lambda kv: kv[1], reverse=True) return [pos for pos, _ in ordered[:limit]]这里 0.9 是防守权重经验值。设成 1.0 也不是不行,但棋力会偏保守,AI 经常不主动进攻;设成太低,AI 又对必然要输的冲四视而不见。0.9 的意思是在同等强度下优先考虑自己赢,但对手形成活四或连五时,防守分因为分值表差距足够大,会排到前面。
候选点数量要按深度联动。如果 depth=2,limit 可以放宽到 32;如果 depth=4,limit 我会降到 20 甚至 16。这个关系不需要每次调得很辛苦,可以固定为“深度加 1,limit 减 4”这样的经验规则。
4.3 带 alpha-beta 剪枝的极大极小搜索:深度、排序与超时控制
有了候选点,就可以搭树搜索。为了避免把树搜索写成所有空位全展开的穷举,我通常把“根节点”和“子节点”分开处理。根节点调用方要拿到落子坐标;子节点递归只返回评估值。
def search(board, candidates, depth, ai_color, time_limit_ms=2000): time_keeper = TimeKeeper(time_limit_ms) best_move = candidates[0] best_score = -INF for move in candidates: r, c = move board[r][c] = ai_color score = minimax(board, depth - 1, -INF, INF, False, ai_color, time_keeper) board[r][c] = EMPTY if score > best_score: best_score = score best_move = move if time_keeper.expired(): break return best_move, best_score在 minimax 里,is_maximizing=True 代表 AI 回合,False 代表对手回合。用 3 - ai_color 取得对手颜色,前面 2.3 的递归版本已经涵盖。这里要强调两个参数:
- 深度:普通对局用 2,关键防守局用 4。深度 1 的 AI 只懂得“吃掉眼前最大棋型”,会被人用双三甚至四三轻松取胜。
- 超时:五子棋 AI 的智能感不只是“算得深”,还包括“算得快”。我一般把单步时间限制在 1.5 到 3 秒。超过这个限度,界面会显得笨重,答辩演示也容易尴尬。
关于迭代加深,我会在时间允许时采用:先从深度 1 开始搜索并保存结果,剩余时间足够就升到深度 2,再升到深度 3。这样即使深度 4 没跑完,至少用深度 3 的结果落子,不会出现“因为深度 4 超时而随便落子”的随机感。
def iterative_search(board, ai_color, time_limit_ms=2000): time_keeper = TimeKeeper(time_limit_ms) candidates = generate_candidates(board, ai_color) best_move, best_score = search(board, candidates, 1, ai_color, time_limit_ms) depth = 1 while depth < 4 and not time_keeper.expired(): depth += 1 move, score = search(board, candidates, depth, ai_color, time_limit_ms) if time_keeper.expired(): break best_move = move best_score = score return best_move注意 time_keeper 要在迭代加深外层创建,不能每层都重新计时。搜索层数加深时,候选点按上一轮搜索的子节点分值排序,可以显著增强剪枝效果。这种“先用贪心排序,再在树上剪枝”的模式,就是这个项目标题里“贪心 + 极大极小”的完整落地。
5. 让代码在答辩和评测前不翻车:五子棋 AI 的常见坑与排查清单
写这里之前先说一个现象:大部分能跑的五子棋人机源码,第一次看得懂,放到自己电脑上跑两局就露馅。不是算法不会写,而是坑都藏在算法之外。下面五条是我反复见过、也自己踩过的,按“现象 → 原因 → 解决”的格式写,可以直接对号入座。
5.1 坑一:白棋从不防守,AI 只按自己的棋型打分
现象:AI 快赢了还要去别处造三,对对手已经形成的冲四视而不见。原因:候选点生成时只计算了己方分数,搜索时轮到对手也只是在对手落点里取最小值,但评估函数整体没有把对手威胁计入叶子节点。解决:评估盘面时同时计算 AI 和对手的四大棋型分,AI 最终得分等于己方总分减去对手总分乘防御系数;候选点生成时同样采用“进攻分 + 0.9 × 防守分”的排序。防御系数不建议超过 1.0,否则局面会变成“谁后手谁被动”。
5.2 坑二:搜索深度一提高就卡死,界面完全无响应
现象:深度 2 秒出,深度 3 基本秒回,深度 4 直接转圈 10 秒。原因:候选点没限制;时间控制没进入递归;或者搜索跑在 UI 线程里。解决:第一,generate_candidates 必须把结果截断到 20~24 个以内;第二,minimax 入口增加 time_keeper.expired() 判断;第三,把 request_move 放到线程池执行,计算完成再通过队列回抛结果。最容易被忽略的是:剪枝只有在候选点排序好时才有效,如果你把所有空位按字典序作为子节点,alpha-beta 基本退化成穷举,深度 4 必然爆炸。
5.3 坑三:活三和冲四的分值倒挂,AI 走出假先手
现象:明明有一个能形成活三的点,AI 却选了一个冲四。原因:评分表里活三分值只有 500,冲四给了 6000,AI 认为冲四收益更大;它不理解冲四之后对手必须应,自己并没有真正形成双威胁。解决:检查棋型定义,活三应当是两端开放的三连,冲四则是被堵住一端或跳形的四连;在分值表里保持活四 > 冲四 > 活三 > 眠三的量级差。如果冲四分值过大而活三偏小,AI 的棋风会变得“蛮干”,不断用冲四换先手,最终丢节奏。
5.4 坑四:悔棋后 AI 后续行为混乱,棋盘状态与 AI 内部状态不同步
现象:正常下几手后点悔棋,再继续下,AI 有时重复落子,有时把棋下到已经存在的棋子上。原因:界面把 grid 数组某个位置清零了,但 Board.moves 没有同步;或者 AI 内部缓存了上次搜索的候选点,悔棋后还用旧盘面的候选点。解决:悔棋逻辑统一由 Board.undo 完成,pop 一次落子记录并同步把 grid 对应位设回 EMPTY;AI 的 request_move 每次从 board.snapshot() 重新生成候选点,不用任何跨次缓存。特别提醒:不要为了省时间给 AI 维护“上次计算好的候选点列表”,悔棋场景下它会变成黑匣子。
5.5 坑五:界面线程和 AI 线程打架,窗口假死
现象:图形界面下点完落子后窗口立刻变灰,甚至系统弹“程序未响应”。原因:AI 在事件回调里同步运行,占用了 UI 主线程;或者线程里直接操作 Tkinter 控件的状态,导致竞态。解决:用独立线程跑 request_move,计算完成后用队列把结果返回到主线程。最小做法如下:
from threading import Thread from queue import Queue def on_board_click(row, col): board.place(row, col, HUMAN_COLOR) q = Queue() Thread(target=worker, args=(q,), daemon=True).start() def worker(q): snapshot = board.snapshot() move = request_move(snapshot, AI_COLOR, depth=3) q.put(move) root.after(0, apply_ai_move, move) # 回到主线程执行queue 是线程间传递数据的简单可靠方式;root.after 是 GUI 主线程调度,能避免在子线程操作控件。控制台版不需要线程,但保留同样的函数接口,调试起来更方便。
排查顺序:先关界面直接用命令行对局;再减少候选点数确认能跑;再加入 alpha-beta 和超时;最后再挂 UI。这样能把“算法慢”和“界面卡”两个问题分开看,不会越查越乱。
6. 拿什么验证你的算法做到位了:三张测试棋面加两个调优习惯
写这类项目最怕自我感觉良好。我习惯用“固定棋面 + 期望落点”来验证,而不是和 AI 下几局就下结论。
6.1 三种必须能赢的残局棋面
第一种是基础活三成四:黑棋有一组两端开放的三连,期望 AI 下一步补成活四。第二种是双三必杀:黑棋同时有两个活三,期望 AI 走其中任何一个的连四点,因为对手无法一手同时堵两个。第三种是被动防守:白棋已经形成冲四,期望 AI 第一反应是堵住成五点而不是继续进攻。我每改一次评分表,都会把这三盘棋作为回归用例跑一遍,任何一盘不通过就不继续调其它参数。
6.2 用“落子特征表”把 AI 的行为变成可验收的证据
把每次测试断言记录成一张表:局面编号、期望落点、AI 实际落点、评估函数输出、耗时。这张表既是自测记录,也是答辩时可以展示的“调试证据”。老师问你怎么证明 AI 真的会防守时,你不需要解释半天下棋手感,直接展示表里“对方冲四局面,AI 落点是否堵住五子”这一行即可。
6.3 调优习惯:每次只改一个参数
调优评分表时,最忌讳一次改三个分值再跑一遍。我吃过一次亏:改完以后棋力变强了,但不知道是活三分值起作用,还是防守系数起作用;后来改回原参数对比,才发现这两者互相抵消。后来我给自己定了一个规矩:每次只改一个参数,固定同一组测试棋面,记录前后落点和耗时;确认没有副作用后,再动下一个参数。这个习惯帮我少走很多弯路。
最后说一句我的真实体会:五子棋 AI 的代码量不大,但它的价值在于“算法和工程长在一起”。搜索树写得再漂亮,棋盘状态不同步一样白搭;评分表再精准,线程卡死一样让老师失去耐心。你把候选点、剪枝、超时、线程这几件事都验证清楚,这个毕业设计在答辩现场就是能打的状态。希望帮到你。
本文还有配套的精品资源,点击获取