项目架构 (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.cpp src/GameBoard.cpp src/GomokuAI.cpp src/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思考的提示等。
- 人机博弈: 允许人类选择执黑或执白,或者实现悔棋功能。
这个项目挑战与乐趣并存,祝你编码愉快!