C++/C的OpenCV和五子棋(Gomoku)算法

发布于:2025-06-25 ⋅ 阅读:(17) ⋅ 点赞:(0)

项目架构 (Project Architecture)

我们将项目分解为四个主要部分,每个部分用一个类来表示,以保持代码的模块化和整洁性。

  1. GameBoard 类 (棋盘逻辑)

    • 职责: 管理棋盘的内部状态,如哪个位置有棋子,判断输赢,放置棋子等。它不关心视觉或AI,只负责游戏规则。
    • 成员: 一个15x15的二维数组来存储棋盘状态 (0: 空, 1: 黑棋, 2: 白棋)。
    • 方法: place_stone(), check_win(), is_valid_move(), reset()
  2. GomokuAI 类 (AI算法)

    • 职责: 决定AI下一步该走哪里。这是程序的大脑。
    • 核心算法: 我们将使用一个经典的启发式评估函数 (Heuristic Evaluation Function)。对于更高级的版本,可以使用Minimax算法 + Alpha-Beta剪枝
    • 方法: find_best_move()
  3. Vision 类 (计算机视觉)

    • 职责: 使用OpenCV处理来自摄像头的图像。包括识别棋盘网格、检测棋子位置、识别用户的落子。
    • 方法: detect_board_grid(), detect_stones(), get_human_move().
  4. main.cpp (主程序)

    • 职责: 程序的入口,负责初始化各个模块,并控制主游戏循环 (Game Loop)。

第一步:项目设置与环境

  1. 安装依赖:

    • C++ 编译器: 如 g++ (Linux/MinGW) 或 MSVC (Windows)。
    • OpenCV: 下载并安装OpenCV库 (推荐版本 4.x)。
    • CMake: 用于管理项目和依赖,是C++项目的标准构建工具。
  2. 创建项目结构:

    GomokuAI/
    ├── CMakeLists.txt
    ├── src/
    │   ├── main.cpp
    │   ├── GameBoard.h
    │   ├── GameBoard.cpp
    │   ├── GomokuAI.h
    │   ├── GomokuAI.cpp
    │   ├── Vision.h
    │   ├── Vision.cpp
    └── ... (build目录等)
    
  3. 编写 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 (实现思路)

  1. 棋盘网格检测 detect_board_grid():

    • 图像预处理: cv::cvtColor转为灰度图, cv::GaussianBlur去噪, cv::Canny边缘检测。
    • 霍夫直线检测: cv::HoughLinesP 找到图像中的所有直线。
    • 直线筛选与分类:
      • 根据斜率将直线分为水平线和垂直线。
      • 对水平线按y坐标排序,对垂直线按x坐标排序。
      • 去除距离过近的重复直线,最终应该得到大约15条水平线和15条垂直线。
    • 计算交点: 计算这些水平线和垂直线的交点,这些交点就是棋盘的落子点。
  2. 棋子检测 detect_stones():

    • 遍历所有棋盘交点。
    • 在每个交点周围取一个小的ROI(感兴趣区域)。
    • 方法一 (颜色): 计算ROI内的平均像素值。如果很低(暗),就是黑子;如果很高(亮),就是白子;如果在中间,就是空的。
    • 方法二 (霍夫圆检测): cv::HoughCircles可以直接在图像中检测圆形。你可以用它来找到所有的黑子和白子,然后将它们的位置映射到最近的棋盘交点上。这个方法通常更鲁棒。
  3. 人类落子检测 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)

  1. AI 强化: 实现 Minimax 算法和 Alpha-Beta 剪枝,让AI具备多步预判能力。
  2. 视觉鲁棒性: 提升棋盘和棋子检测的稳定性,例如使用透视变换(cv::getPerspectiveTransformcv::warpPerspective)将倾斜的棋盘图像校正为俯视图。
  3. 更好的UI: 在窗口中更清晰地绘制棋盘、序号、AI思考的提示等。
  4. 人机博弈: 允许人类选择执黑或执白,或者实现悔棋功能。

这个项目挑战与乐趣并存,祝你编码愉快!


网站公告

今日签到

点亮在社区的每一天
去签到