项目架构 (Project Architecture)
我们将项目分解为四个主要部分,每个部分用一个类来表示,以保持代码的模块化和整洁性。
-
GameBoard
类 (棋盘逻辑)- 职责: 管理棋盘的内部状态,如哪个位置有棋子,判断输赢,放置棋子等。它不关心视觉或AI,只负责游戏规则。
- 成员: 一个15x15的二维数组来存储棋盘状态 (
0
: 空,1
: 黑棋,2
: 白棋)。 - 方法:
place_stone()
,check_win()
,is_valid_move()
,reset()
。
-
GomokuAI
类 (AI算法)- 职责: 决定AI下一步该走哪里。这是程序的大脑。
- 核心算法: 我们将使用一个经典的启发式评估函数 (Heuristic Evaluation Function)。对于更高级的版本,可以使用Minimax算法 + Alpha-Beta剪枝。
- 方法:
find_best_move()
。
-
Vision
类 (计算机视觉)- 职责: 使用OpenCV处理来自摄像头的图像。包括识别棋盘网格、检测棋子位置、识别用户的落子。
- 方法:
detect_board_grid()
,detect_stones()
,get_human_move()
.
-
main.cpp
(主程序)- 职责: 程序的入口,负责初始化各个模块,并控制主游戏循环 (Game Loop)。
第一步:项目设置与环境
-
安装依赖:
- C++ 编译器: 如
g++
(Linux/MinGW) 或 MSVC (Windows)。 - OpenCV: 下载并安装OpenCV库 (推荐版本 4.x)。
- CMake: 用于管理项目和依赖,是C++项目的标准构建工具。
- C++ 编译器: 如
-
创建项目结构:
GomokuAI/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── GameBoard.h │ ├── GameBoard.cpp │ ├── GomokuAI.h │ ├── GomokuAI.cpp │ ├── Vision.h │ ├── Vision.cpp └── ... (build目录等)
-
编写
CMakeLists.txt
:
这是告诉CMake如何构建你的项目的配置文件。cmake_minimum_required(VERSION 3.10) project(GomokuAI)set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON)# 找到OpenCV库 # 你可能需要设置 OpenCV_DIR 环境变量指向你的OpenCV安装目录下的build文件夹 find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS})# 添加源文件 add_executable(GomokuAI src/main.cppsrc/GameBoard.cppsrc/GomokuAI.cppsrc/Vision.cpp)# 链接OpenCV库 target_link_libraries(GomokuAI ${OpenCV_LIBS})
第二步:实现 GameBoard
类 (游戏规则)
这是最基础的部分,定义了五子棋的规则。
GameBoard.h
#pragma once
#include <vector>const int BOARD_SIZE = 15;enum class Player { EMPTY = 0, BLACK = 1, WHITE = 2 };class GameBoard {
public:GameBoard();void place_stone(int row, int col, Player player);bool check_win(int row, int col);bool is_valid_move(int row, int col) const;void reset();const std::vector<std::vector<Player>>& get_board_state() const;private:std::vector<std::vector<Player>> board;Player last_player;int check_line(int r, int c, int dr, int dc); // 辅助检查函数
};
GameBoard.cpp
#include "GameBoard.h"GameBoard::GameBoard() {reset();
}void GameBoard::reset() {board.assign(BOARD_SIZE, std::vector<Player>(BOARD_SIZE, Player::EMPTY));last_player = Player::EMPTY;
}void GameBoard::place_stone(int row, int col, Player player) {if (is_valid_move(row, col)) {board[row][col] = player;last_player = player;}
}bool GameBoard::is_valid_move(int row, int col) const {return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE && board[row][col] == Player::EMPTY;
}// 核心:胜利条件检测
bool GameBoard::check_win(int row, int col) {if (last_player == Player::EMPTY) return false;// 检查四个方向: 水平, 垂直, 左上到右下, 右上到左下if (check_line(row, col, 0, 1) >= 5) return true; // 水平if (check_line(row, col, 1, 0) >= 5) return true; // 垂直if (check_line(row, col, 1, 1) >= 5) return true; // 左上-右下if (check_line(row, col, 1, -1) >= 5) return true; // 右上-左下return false;
}// 辅助函数,检查一个方向上的连子数
int GameBoard::check_line(int r, int c, int dr, int dc) {int count = 1;Player p = board[r][c];// 向正方向搜索for (int i = 1; i < 5; ++i) {int nr = r + i * dr;int nc = c + i * dc;if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc] == p) {count++;} else {break;}}// 向反方向搜索for (int i = 1; i < 5; ++i) {int nr = r - i * dr;int nc = c - i * dc;if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc] == p) {count++;} else {break;}}return count;
}const std::vector<std::vector<Player>>& GameBoard::get_board_state() const {return board;
}
第三步:实现 GomokuAI
类 (AI核心)
我们将使用基于棋型评分的启发式方法。AI会遍历所有可以落子的空位,计算每个位置的分数,然后选择分数最高的位置。
评分标准示例:
- 连五: 100000分
- 活四: 10000分 (两端都没被堵住的四个子)
- 冲四: 1000分 (一端被堵住的四个子)
- 活三: 1000分 (两端都没被堵住的三个子)
- 眠三: 100分 (一端被堵住的三个子)
- …等等
AI需要为自己(白棋)和对手(黑棋)都计算分数,通常一个位置的总分 = AI在此处落子后的得分
+ 对手在此处落子后的得分
。
GomokuAI.h
#pragma once
#include "GameBoard.h"
#include <opencv2/core.hpp>class GomokuAI {
public:GomokuAI();// 寻找最佳落子点,返回cv::Point(col, row)cv::Point find_best_move(const GameBoard& board);private:// 计算在(row, col)落子后的棋盘得分int evaluate_board(const GameBoard& board, Player player);int evaluate_point(const GameBoard& board, int row, int col, Player player);// 评估一个方向上的棋型int analyze_line(char line[9], Player player);
};
GomokuAI.cpp
(核心思路)
#include "GomokuAI.h"
#include <limits>// ... (构造函数等)cv::Point GomokuAI::find_best_move(const GameBoard& current_board) {int max_score = -std::numeric_limits<int>::max();cv::Point best_move(-1, -1);const auto& board_state = current_board.get_board_state();for (int r = 0; r < BOARD_SIZE; ++r) {for (int c = 0; c < BOARD_SIZE; ++c) {if (board_state[r][c] == Player::EMPTY) {// 尝试在此处为AI落子并计算分数GameBoard temp_board = current_board;temp_board.place_stone(r, c, Player::WHITE); // AI是白棋int ai_score = evaluate_point(temp_board, r, c, Player::WHITE);// 尝试在此处为玩家落子并计算分数(防守分)temp_board = current_board;temp_board.place_stone(r, c, Player::BLACK); // 玩家是黑棋int human_score = evaluate_point(temp_board, r, c, Player::BLACK);int current_score = ai_score + human_score;if (current_score > max_score) {max_score = current_score;best_move = cv::Point(c, r);}}}}return best_move;
}// 这是一个简化的评估函数,你需要详细实现它
int GomokuAI::evaluate_point(const GameBoard& board, int row, int col, Player player) {// 实际的实现会更复杂,需要检查4个方向(水平、垂直、2个对角线)// 并根据棋型(活四、冲四、活三等)返回分数。// 例如,如果(row, col)处落子后形成了一个活四,就返回10000分。// 这是整个AI最核心、最复杂的部分。// 你可以搜索 "Gomoku evaluation function" 来获取详细的棋型和分数设计。int score = 0;// ... 详细的棋型匹配和评分逻辑 ...return score;
}
第四步:实现 Vision
类 (视觉处理)
这是最具挑战性的部分。
Vision.h
#pragma once
#include <opencv2/opencv.hpp>
#include "GameBoard.h"class Vision {
public:Vision();bool initialize_camera(int index = 0);bool detect_board_grid(const cv::Mat& frame, std::vector<cv::Point2f>& grid_intersections);GameBoard detect_stones(const cv::Mat& frame, const std::vector<cv::Point2f>& grid_intersections);// 通过对比前后两帧的棋盘状态,找出新落子的位置cv::Point get_human_move(const GameBoard& prev_board, const GameBoard& current_board);private:cv::VideoCapture cap;
};
Vision.cpp
(实现思路)
-
棋盘网格检测
detect_board_grid()
:- 图像预处理:
cv::cvtColor
转为灰度图,cv::GaussianBlur
去噪,cv::Canny
边缘检测。 - 霍夫直线检测:
cv::HoughLinesP
找到图像中的所有直线。 - 直线筛选与分类:
- 根据斜率将直线分为水平线和垂直线。
- 对水平线按y坐标排序,对垂直线按x坐标排序。
- 去除距离过近的重复直线,最终应该得到大约15条水平线和15条垂直线。
- 计算交点: 计算这些水平线和垂直线的交点,这些交点就是棋盘的落子点。
- 图像预处理:
-
棋子检测
detect_stones()
:- 遍历所有棋盘交点。
- 在每个交点周围取一个小的ROI(感兴趣区域)。
- 方法一 (颜色): 计算ROI内的平均像素值。如果很低(暗),就是黑子;如果很高(亮),就是白子;如果在中间,就是空的。
- 方法二 (霍夫圆检测):
cv::HoughCircles
可以直接在图像中检测圆形。你可以用它来找到所有的黑子和白子,然后将它们的位置映射到最近的棋盘交点上。这个方法通常更鲁棒。
-
人类落子检测
get_human_move()
:- 在游戏循环中,保存上一帧检测到的棋盘状态
prev_board
。 - 获取当前帧的棋盘状态
current_board
。 - 遍历棋盘,找到那个在
prev_board
中为EMPTY
,但在current_board
中变为BLACK
的位置,这就是人类玩家的落子。
- 在游戏循环中,保存上一帧检测到的棋盘状态
第五步:整合到 main.cpp
主程序负责把所有模块串联起来,形成完整的游戏流程。
main.cpp
(伪代码)
#include "GameBoard.h"
#include "GomokuAI.h"
#include "Vision.h"
#include <opencv2/opencv.hpp>enum class GameState { HUMAN_TURN, AI_TURN, GAME_OVER };int main() {// 1. 初始化GameBoard board;GomokuAI ai;Vision vision;if (!vision.initialize_camera()) {std::cerr << "Error: Cannot open camera." << std::endl;return -1;}cv::Mat frame;std::vector<cv::Point2f> grid_points;GameState game_state = GameState::HUMAN_TURN;GameBoard last_detected_board;// 校准阶段:先检测到稳定的棋盘网格std.cout << "请将棋盘完整放入摄像头视野内,按'c'键进行校准..." << std::endl;while(true) {cap >> frame;if (frame.empty()) break;// 尝试检测棋盘,并在frame上画出预览// ...cv::imshow("Calibration", frame_with_preview);if (cv::waitKey(10) == 'c') {if (vision.detect_board_grid(frame, grid_points)) {std::cout << "校准成功!" << std::endl;break;} else {std::cout << "校准失败,请调整角度和光照后重试..." << std::endl;}}}cv::destroyWindow("Calibration");// 2. 主游戏循环while (true) {cap >> frame;if (frame.empty()) break;// 绘制棋盘和当前状态// draw_board(frame, grid_points, board.get_board_state());if (game_state == GameState::HUMAN_TURN) {GameBoard current_detected_board = vision.detect_stones(frame, grid_points);cv::Point human_move = vision.get_human_move(last_detected_board, current_detected_board);if (human_move.x != -1 && board.is_valid_move(human_move.y, human_move.x)) {board.place_stone(human_move.y, human_move.x, Player::BLACK);last_detected_board = current_detected_board;if (board.check_win(human_move.y, human_move.x)) {std::cout << "你赢了!" << std::endl;game_state = GameState::GAME_OVER;} else {game_state = GameState::AI_TURN;}}}else if (game_state == GameState::AI_TURN) {cv::Point ai_move = ai.find_best_move(board);board.place_stone(ai_move.y, ai_move.x, Player::WHITE);// 在屏幕上清晰地标记出AI的落子位置// draw_ai_move_suggestion(frame, grid_points[ai_move.y * 15 + ai_move.x]);if (board.check_win(ai_move.y, ai_move.x)) {std::cout << "AI赢了!" << std::endl;game_state = GameState::GAME_OVER;} else {game_state = GameState::HUMAN_TURN;// AI落子后,需要更新last_detected_board以防止错误检测last_detected_board = vision.detect_stones(frame, grid_points); }}cv::imshow("Gomoku AI", frame);char key = (char)cv::waitKey(20);if (key == 27) break; // ESC退出if (key == 'r') { // 'r'键重开游戏board.reset();game_state = GameState::HUMAN_TURN;last_detected_board.reset();}}return 0;
}
进阶与完善 (Roadmap)
- AI 强化: 实现 Minimax 算法和 Alpha-Beta 剪枝,让AI具备多步预判能力。
- 视觉鲁棒性: 提升棋盘和棋子检测的稳定性,例如使用透视变换(
cv::getPerspectiveTransform
和cv::warpPerspective
)将倾斜的棋盘图像校正为俯视图。 - 更好的UI: 在窗口中更清晰地绘制棋盘、序号、AI思考的提示等。
- 人机博弈: 允许人类选择执黑或执白,或者实现悔棋功能。
这个项目挑战与乐趣并存,祝你编码愉快!