news 2026/8/16 5:33:35

从零构建五子棋AI:基于C++的博弈树搜索与α-β剪枝实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建五子棋AI:基于C++的博弈树搜索与α-β剪枝实战

1. 五子棋AI的基本原理

五子棋作为一款经典的策略游戏,其AI实现的核心在于模拟人类棋手的思考过程。想象一下,当你下棋时,会考虑"如果我下这里,对方可能会下那里,然后我可以这样应对..."。这正是博弈树搜索的精髓所在。

博弈树就像是一棵巨大的可能性之树,每个节点代表一个棋局状态,分支代表可能的落子选择。对于15×15的标准棋盘,第一步就有225种可能,第二步有224种...这样的组合爆炸使得穷举所有可能性变得不现实。这时候就需要极大极小算法来帮忙。

极大极小算法的思想很直观:假设对手总是做出对你最不利的选择,而你会选择对自己最有利的走法。在算法实现中,MAX节点(你的回合)会选择子节点中的最大值,MIN节点(对手回合)会选择最小值。这就形成了一个相互制约的决策过程。

2. 从零搭建C++项目框架

我们先来搭建最基本的项目结构。建议使用CMake来管理项目,这样跨平台会方便很多。创建一个基本的项目目录结构:

FiveInARowAI/ ├── CMakeLists.txt ├── include/ │ ├── Board.h │ ├── Evaluator.h │ └── Search.h └── src/ ├── Board.cpp ├── Evaluator.cpp ├── Search.cpp └── main.cpp

Board.h中,我们定义棋盘的基本表示:

#pragma once #include <array> constexpr int BOARD_SIZE = 15; enum class Piece { EMPTY, BLACK, WHITE }; class Board { public: Board(); bool placePiece(int x, int y, Piece piece); Piece checkWinner() const; bool isFull() const; void display() const; private: std::array<std::array<Piece, BOARD_SIZE>, BOARD_SIZE> grid; };

这个棋盘类会处理落子、胜负判断等基本逻辑。注意我们使用了std::array而不是原生数组,这样更安全且易于使用。

3. 实现博弈树搜索

现在来到最核心的部分——博弈树搜索的实现。我们先定义一个基础的Node类:

class Node { public: Node(const Board& board, bool isMaximizing); int evaluate(); std::vector<Node> generateChildren() const; Board getBoard() const { return board; } int getValue() const { return value; } private: Board board; bool isMaximizing; int value; };

基础版本的极大极小搜索可以这样实现:

int minimax(Node node, int depth, bool isMaximizing) { if (depth == 0 || node.getBoard().checkWinner() != Piece::EMPTY) { return node.evaluate(); } if (isMaximizing) { int maxEval = INT_MIN; for (auto& child : node.generateChildren()) { int eval = minimax(child, depth - 1, false); maxEval = std::max(maxEval, eval); } return maxEval; } else { int minEval = INT_MAX; for (auto& child : node.generateChildren()) { int eval = minimax(child, depth - 1, true); minEval = std::min(minEval, eval); } return minEval; } }

这个基础版本虽然能工作,但效率极低。在我的测试中,搜索深度超过4层时,响应时间就开始变得不可接受。这就是我们需要优化的地方。

4. α-β剪枝优化

α-β剪枝是博弈树搜索中最经典的优化技术。它的核心思想是:如果已经确定某个分支不会比已知的最佳选择更好,就可以提前终止对该分支的搜索。

想象你在跟朋友下棋,考虑走A位置时,发现无论后续怎么走都会输,那你就会立即放弃考虑A位置,转而去评估其他可能性。α-β剪枝就是让计算机也具备这种"直觉"。

改进后的算法:

int alphabeta(Node node, int depth, int alpha, int beta, bool isMaximizing) { if (depth == 0 || node.getBoard().checkWinner() != Piece::EMPTY) { return node.evaluate(); } if (isMaximizing) { int value = INT_MIN; for (auto& child : node.generateChildren()) { value = std::max(value, alphabeta(child, depth - 1, alpha, beta, false)); alpha = std::max(alpha, value); if (alpha >= beta) break; // β剪枝 } return value; } else { int value = INT_MAX; for (auto& child : node.generateChildren()) { value = std::min(value, alphabeta(child, depth - 1, alpha, beta, true)); beta = std::min(beta, value); if (beta <= alpha) break; // α剪枝 } return value; } }

在实际测试中,加入α-β剪枝后,搜索效率提升了约10-50倍,具体取决于棋局状态。这意味着我们可以用相同的计算时间探索更深的层级。

5. 设计高效的估价函数

估价函数是AI的"棋感",决定了它如何评估局面的好坏。一个糟糕的估价函数会让AI做出愚蠢的决定,即使搜索深度很深。

五子棋中常用的方法是分析棋盘上的各种棋型。我们可以定义一些基本模式:

enum class Pattern { FIVE, // 五连 OPEN_FOUR, // 活四 HALF_FOUR, // 冲四 OPEN_THREE, // 活三 HALF_THREE, // 眠三 OPEN_TWO, // 活二 HALF_TWO // 眠二 };

然后为每种模式分配分数:

int Evaluator::evaluatePattern(Pattern pattern, bool isBlack) { const int sign = isBlack ? 1 : -1; switch (pattern) { case Pattern::FIVE: return sign * 1000000; case Pattern::OPEN_FOUR: return sign * 100000; case Pattern::HALF_FOUR: return sign * 10000; case Pattern::OPEN_THREE: return sign * 1000; case Pattern::HALF_THREE: return sign * 100; case Pattern::OPEN_TWO: return sign * 10; case Pattern::HALF_TWO: return sign * 1; default: return 0; } }

实际评估时,我们需要扫描棋盘上所有可能的五元组(连续五个点),统计各种模式的出现次数。这里有个优化技巧:不需要每次评估都全盘扫描,可以只计算最新落子影响到的区域。

6. 优化搜索策略

除了α-β剪枝,我们还可以采用其他优化策略:

  1. 迭代加深搜索:先浅层搜索得到初步结果,再逐步加深,这样可以在时间有限时也能得到一个不错的走法。

  2. 移动顺序优化:将更有潜力的走法优先评估,这样能提高剪枝效率。可以根据上一步的评估分数排序。

  3. 开局库:对于常见开局,直接使用预存的优秀走法,避免不必要的计算。

  4. 置换表:缓存已经评估过的局面,避免重复计算。

实现迭代加深搜索的示例:

Move findBestMove(Board board, int maxDepth, TimeLimit timeout) { Move bestMove; auto start = std::chrono::steady_clock::now(); for (int depth = 1; depth <= maxDepth; ++depth) { if (std::chrono::steady_clock::now() - start > timeout) { break; } auto [move, eval] = searchDepth(board, depth); if (move.isValid()) { bestMove = move; } } return bestMove; }

7. 实现人机对弈界面

最后,我们需要一个交互界面来测试我们的AI。一个简单的控制台界面可以这样实现:

void runGame() { Board board; AIPlayer ai(Board::Piece::BLACK); // AI执黑 while (true) { board.display(); if (board.getCurrentPlayer() == Board::Piece::WHITE) { // 玩家回合 int x, y; std::cout << "Your move (x y): "; std::cin >> x >> y; if (!board.placePiece(x, y, Board::Piece::WHITE)) { std::cout << "Invalid move!\n"; continue; } } else { // AI回合 std::cout << "AI is thinking...\n"; auto move = ai.getBestMove(board); board.placePiece(move.x, move.y, Board::Piece::BLACK); } auto winner = board.checkWinner(); if (winner != Board::Piece::EMPTY) { board.display(); std::cout << (winner == Board::Piece::WHITE ? "You" : "AI") << " win!\n"; break; } } }

对于更友好的界面,可以考虑使用SFML或Qt等图形库来实现图形化界面。

8. 性能测试与调优

在实际测试中,我发现几个关键性能瓶颈:

  1. 棋盘拷贝开销:在生成子节点时,频繁的棋盘拷贝消耗了大量时间。解决方法是为Board类实现高效的拷贝操作,或者使用指针共享不变的部分。

  2. 评估函数效率:最初的实现是全盘扫描,后来改为增量式评估,只计算受最新落子影响的区域,速度提升了约8倍。

  3. 内存分配:频繁的节点创建和销毁导致内存分配成为瓶颈。采用对象池技术后,性能提升了约30%。

一个简单的性能测试结果:

优化措施搜索深度平均响应时间
基础版本4层12.5秒
+α-β剪枝4层1.8秒
+移动顺序优化4层0.9秒
+增量评估5层1.2秒
全部优化6层1.5秒

9. 进阶改进方向

完成基础版本后,可以考虑以下进阶改进:

  1. 并行搜索:利用多线程同时评估不同的分支。需要注意线程安全和α-β值的共享问题。

  2. 机器学习增强:使用强化学习训练估价函数,或者用蒙特卡洛树搜索(MCTS)替代传统的博弈树搜索。

  3. 开局库和残局库:收集专业棋手的开局走法,对于特定残局局面使用预计算的必胜走法。

  4. Zobrist哈希:为棋盘局面生成唯一哈希值,用于快速查重和置换表。

  5. 时间控制:根据剩余时间动态调整搜索深度,确保不会超时。

实现并行搜索的一个简单示例:

std::vector<std::future<int>> futures; for (auto& child : root.generateChildren()) { futures.push_back(std::async(std::launch::async, [&]{ return alphabeta(child, depth-1, alpha, beta, false); })); } int bestValue = INT_MIN; for (auto& fut : futures) { int value = fut.get(); if (value > bestValue) { bestValue = value; // 更新最佳走法... } }

10. 实际对弈测试与分析

经过多次测试,我发现这个AI在业余级别已经表现不错,但还存在一些典型问题:

  1. 长连陷阱:有时会忽视对手正在形成的多个活三,导致防守不力。

  2. 先手优势:由于五子棋本身的特性,先手优势明显,需要加入平衡机制。

  3. 终局判断:在接近终局时,搜索深度不足可能导致错过必胜走法。

针对这些问题,我调整了估价函数,给对手的潜在活三和冲四赋予更高的威胁分数,同时增加了终局阶段的特殊处理。经过调整后,AI的防守能力明显提升。

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

ESP32-S3-CAM:认识引脚

越来越发现做什么硬件接入都要先初始化引脚&#xff0c;板子自带的SD卡槽&#xff0c;摄像头插座&#xff0c;还有些板载的LED灯之类的&#xff0c;其实都是默认会占用一些引脚&#xff0c;看来需要系统的学习一下。 刚开始发现淘宝卖家里头有张这样的图&#xff0c;是介绍各个…

作者头像 李华
网站建设 2026/7/14 16:05:39

5步攻克B站缓存播放难题:m4s转MP4完全指南

5步攻克B站缓存播放难题&#xff1a;m4s转MP4完全指南 【免费下载链接】m4s-converter 将bilibili缓存的m4s转成mp4(读PC端缓存目录) 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 你是否曾在旅行途中想重温缓存的B站教学视频&#xff0c;却发现文件格式…

作者头像 李华
网站建设 2026/7/14 16:05:41

探索 6 个电池均衡的 Buck - Boost 电路:高精度与快速均衡的奥秘

6个电池均衡&#xff0c;buckboost电路&#xff0c;精度高&#xff0c;均衡速度快在电池管理系统&#xff08;BMS&#xff09;中&#xff0c;电池均衡技术是确保电池组性能和寿命的关键环节。今天咱就来唠唠 6 个电池均衡且基于 Buck - Boost 电路实现高精度、快速均衡的那些事…

作者头像 李华
网站建设 2026/7/14 16:05:42

EagleEdge场景应用:DAMO-YOLO TinyNAS在仓储盘点中的实战案例

EagleEdge场景应用&#xff1a;DAMO-YOLO TinyNAS在仓储盘点中的实战案例 1. 从“人眼数”到“AI看”&#xff1a;仓储盘点的效率革命 想象一下这个场景&#xff1a;一个中型仓库&#xff0c;货架上堆放着数千个规格不一的纸箱。月底盘点&#xff0c;两名员工拿着手持终端&am…

作者头像 李华
网站建设 2026/7/14 16:05:41

Verilog实战:8位加法器流水线设计从入门到精通(附2级/4级完整代码)

Verilog实战&#xff1a;8位加法器流水线设计从入门到精通&#xff08;附2级/4级完整代码&#xff09; 在数字电路设计中&#xff0c;加法器是最基础也最关键的运算单元之一。随着系统时钟频率的不断提升&#xff0c;传统的组合逻辑加法器已经难以满足高性能计算的需求。这时&a…

作者头像 李华