本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:【源程序_regionj2k_五子棋_C++五子棋游戏】是一个基于C++语言实现的五子棋对弈项目,适合作为游戏编程初学者的学习案例。项目涵盖了从基础数据结构到复杂游戏逻辑的完整实现,包括棋盘表示、落子规则、胜负判断、禁手处理、用户交互及可扩展的AI算法设计。通过命令行或图形界面展示,结合递归搜索与剪枝优化,帮助开发者掌握游戏开发核心技术。项目还强调代码组织、调试测试、版本控制与文档编写,全面提升C++编程与软件工程实践能力。

C++五子棋项目:从零构建高性能对弈引擎

在智能家居设备日益复杂的今天,确保无线连接的稳定性已成为一大设计挑战。不过我们今天不聊物联网,而是换个画风——回到最纯粹的代码世界,聊聊一个能让程序员心跳加速的经典项目: 用现代C++打造一款高性能五子棋对弈系统

你没看错,就是那个小时候在纸上画格子、用黑白石子下着玩的五子棋。但别小瞧它,这背后藏着不少门道:数据结构的选择、算法优化的艺术、AI决策树的设计,甚至还有多线程并发控制……堪称是“麻雀虽小,五脏俱全”的工程实践典范。


设想这样一个场景:你正坐在电脑前调试你的AI对手,突然它走出一步妙手,直接封死了你的所有进攻路线。那一刻,你会不会有一种“我创造的生命正在超越我”的错觉?😎 这正是编程的魅力所在。

而我们要做的,就是从最基础的棋盘建模开始,一步步搭建起这套完整的对弈系统。整个过程不仅考验编码能力,更是一场关于 工程思维与架构权衡 的深度训练。

开发环境初始化与跨平台编译配置

先别急着写代码,工欲善其事,必先利其器。咱们得先把开发环境搭好,不然后面迭代起来效率低得让人抓狂。

现代C++项目早已告别了 g++ main.cpp -o game 这种原始方式,取而代之的是像 CMake 这样的现代化构建工具。它不仅能帮你管理依赖、生成Makefile,还能轻松实现跨平台编译——Windows、Linux、macOS一套配置走天下 ✈️。

推荐使用 GCC 9+ 或 Clang 10+ 编译器,并启用 C++17 标准(或更高),以便使用 constexpr if 、结构化绑定等现代特性。

下面是一个典型的 CMakeLists.txt 示例:

cmake_minimum_required(VERSION 3.14)
project(GomokuGame CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(gomoku 
    src/main.cpp 
    src/Board.cpp 
    src/Game.cpp
    src/AI.cpp
)

目录结构也得规范一点,毕竟谁也不想几个月后打开项目时面对一堆混乱的 .cpp 文件发呆:

gomoku/
├── src/            # 源码
├── include/        # 头文件
├── test/           # 单元测试
├── build/          # 编译输出
└── CMakeLists.txt

进入 build/ 目录执行:

cmake .. && make

从此告别手动编译,享受一键构建的快感 💥


棋盘数据结构设计:为什么二维数组仍是王者?

好了,重头戏来了——棋盘怎么表示?

这个问题看似简单,实则暗藏玄机。不同的数据结构选择,会直接影响后续胜负判断、AI评估、内存占用等一系列关键性能指标。

五子棋的本质是什么?

标准五子棋棋盘是 15×15 的网格 ,每个交叉点有三种状态:
- 空(EMPTY)
- 黑子(BLACK)
- 白子(WHITE)

换句话说,这就是一个二维离散空间上的状态映射问题。那么自然地,我们会想到几种可能的数据结构:

数据结构 特点 是否适合
静态二维数组 连续存储,缓存友好 ✅ 强烈推荐
vector<vector<Piece>> 动态可变,但多层跳转 ⚠️ 可接受
map<pair<int,int>, Piece> 稀疏高效,查找慢 ❌ 不适合高频访问
一维数组模拟二维 内存紧凑,索引需转换 ✅ 折中方案

结论很明确:对于固定尺寸、密集填充的棋盘场景, 静态二维数组依然是最优解

内存布局优势分析

C/C++采用行主序(row-major order)存储,意味着相邻列元素在内存中也是连续的。举个例子:

Piece board[15][15];
// (i,j) 对应的地址为 &board[0][0] + i * 15 + j

当你遍历某一行时,CPU缓存会一次性加载多个连续字节,极大提升命中率 🚀。而在胜负检测这种需要频繁扫描局部区域的操作中,这种缓存友好性至关重要。

相比之下, map unordered_map 虽然节省空间,但每次访问都要经过哈希计算和节点跳转,延迟高得多。

📌 小贴士:现代CPU缓存行通常是64字节,若每格占1字节,则一次可加载64个格子。15×15=225字节 ≈ 4个缓存行就能装下整个棋盘!

枚举类型增强语义表达

别再用 int board[15][15]; 里用 0、1、2 表示状态了!这样写出来的代码简直就是天书。

我们应该用强类型枚举来提升可读性和安全性:

enum class Piece : uint8_t {
    EMPTY = 0,
    BLACK = 1,
    WHITE = 2
};

看到 board[x][y] == Piece::BLACK 是不是舒服多了?而且 uint8_t 作为底层类型,每个格子只占1字节,比默认的 int (4字节)省了75%内存!

配合 constexpr 定义常量:

constexpr int BOARD_SIZE = 15;
static_assert(BOARD_SIZE > 0 && BOARD_SIZE <= 20, "棋盘大小必须合理");

编译器能在编译期完成边界检查和偏移计算,运行时零开销。

下面是 Board.h 的完整定义:

#pragma once
#include <cstdint>

constexpr int BOARD_SIZE = 15;

enum class Piece : uint8_t {
    EMPTY = 0,
    BLACK = 1,
    WHITE = 2
};

class Board {
public:
    Board();
    void reset();
    Piece get(int x, int y) const;
    bool set(int x, int y, Piece p);

private:
    Piece data[BOARD_SIZE][BOARD_SIZE];
};

简洁、清晰、安全,完美 👌

访问模式优化技巧

即便选择了二维数组,我们仍可以通过一些技巧进一步提升性能。

✅ 推荐:行主序遍历
for (int i = 0; i < BOARD_SIZE; ++i)
    for (int j = 0; j < BOARD_SIZE; ++j)
        process(board[i][j]);  // 缓存友好
❌ 不推荐:列主序遍历
for (int j = 0; j < BOARD_SIZE; ++j)
    for (int i = 0; i < BOARD_SIZE; ++i)
        process(board[i][j]);  // 每次跳跃15个元素,缓存不友好

前者地址递增1,后者跳跃 15 * sizeof(Piece) 字节,容易导致 cache miss。

使用指针加速批量操作

在胜负检测这类高频扫描中,可以将二维索引转为一维指针操作:

const Piece* base = &data[0][0];
for (int offset = 0; offset < BOARD_SIZE * BOARD_SIZE; ++offset) {
    if (*(base + offset) != Piece::EMPTY) {
        // 快速跳过空位
    }
}

避免双重循环的索引计算开销,特别适合后期接入 SIMD 指令优化。


面向对象封装:打造高内聚低耦合的 Board 类

有了底层数据结构,下一步就是把它包装成一个真正的“类”。

C++ 的类机制让我们可以把数据和操作封装在一起,形成一个独立模块。这对于后期维护、测试、扩展都极其重要。

接口设计原则

一个好的 Board 类应该提供以下核心接口:

方法 参数 返回值 功能说明
Board() —— 构造函数 初始化为空棋盘
reset() —— void 清空所有格子
get(x,y) int x, y Piece 获取某位置状态
set(x,y,p) int x,y, Piece p bool 设置状态,失败返回false

注意几个细节:
- get() 应标记为 const ,保证不修改对象;
- set() 返回 bool 用于反馈是否越界;
- 构造函数自动调用 reset() ,确保初始状态一致。

实现如下:

// Board.cpp
#include "Board.h"
#include <cstring>

Board::Board() {
    reset();
}

void Board::reset() {
    std::memset(data, 0, sizeof(data));  // 比嵌套循环快得多
}

Piece Board::get(int x, int y) const {
    return data[x][y];
}

bool Board::set(int x, int y, Piece p) {
    if (x < 0 || x >= BOARD_SIZE || y < 0 || y >= BOARD_SIZE)
        return false;
    data[x][y] = p;
    return true;
}

其中 std::memset 利用底层汇编指令快速清零,效率远高于手动赋值。

私有成员保护与资源管理

data 声明为 private 是基本要求,防止外部随意篡改内部状态。如果真想暴露原始数据供AI使用(比如做模式匹配),可以加个只读接口:

const Piece (*raw_data() const)[BOARD_SIZE] {
    return data;
}

这样既能满足高性能需求,又能保持封装性。

至于析构函数?目前不需要。因为 data 是栈上分配的静态数组,生命周期由对象本身管理,符合 RAII 原则。

但如果将来要支持动态尺寸,就得改成堆分配:

class DynamicBoard {
    Piece* data_;
    int size_;
public:
    explicit DynamicBoard(int n) : size_(n) {
        data_ = new Piece[n * n]();  // 零初始化
    }
    ~DynamicBoard() {
        delete[] data_;
    }
    // 注意拷贝构造和赋值操作的安全性!
};

这时候就必须小心处理深拷贝问题,否则会出现内存泄漏或双重释放。


边界检查与坐标合法性验证机制

任何对外部输入的坐标都必须进行严格校验,否则轻则程序崩溃,重则被黑客利用搞出缓冲区溢出 😱

越界判断函数

虽然 set() 已经做了检查,但在胜负检测、AI搜索等高频调用场景中,最好单独抽离判断逻辑以复用:

inline bool is_valid(int x, int y) {
    return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE;
}

加上 inline 关键字避免函数调用开销,适合频繁调用。

落子前双重验证

除了坐标合法,还得确认目标位置为空。我们可以新增一个方法:

bool Board::place_piece(int x, int y, Piece p) {
    if (!is_valid(x, y) || data[x][y] != Piece::EMPTY)
        return false;
    data[x][y] = p;
    return true;
}

这个方法整合了“有效性”和“可用性”两层检查,适合作为外部落子的统一入口。

流程图如下:

flowchart LR
    Start[接收落子请求 (x,y,p)] --> Valid{is_valid(x,y)?}
    Valid -- 否 --> Reject1[拒绝: 越界]
    Valid -- 是 --> Empty{data[x][y] == EMPTY?}
    Empty -- 否 --> Reject2[拒绝: 位置已被占用]
    Empty -- 是 --> Set[执行落子]
    Set --> Success[返回成功]

层层过滤,稳如老狗 🐶


扩展能力预留:模板化与稀疏矩阵演进路径

优秀的软件设计不仅要满足当前需求,还要为未来留出空间。

模板化支持多种棋盘尺寸

现在是15×15,明天说不定就要支持13×13或19×19。怎么办?模板化!

template<int N>
class Board {
public:
    static constexpr int SIZE = N;
    Board() { reset(); }

    void reset() {
        std::memset(data, 0, sizeof(data));
    }

    Piece get(int x, int y) const;
    bool set(int x, int y, Piece p);

private:
    Piece data[N][N];
};

使用时:

Board<15> standard;
Board<13> small;

不同尺寸互不影响,且仍然是零运行时开销,编译期确定一切。

稀疏矩阵应对超大棋盘场景

如果未来要做“百米级巨型五子棋”,传统数组显然撑不住。此时可考虑稀疏存储:

class SparseBoard {
    std::map<std::pair<int, int>, Piece> pieces;
public:
    Piece get(int x, int y) const {
        auto it = pieces.find({x, y});
        return it != pieces.end() ? it->second : Piece::EMPTY;
    }
    void set(int x, int y, Piece p) {
        if (p == Piece::EMPTY) {
            pieces.erase({x, y});
        } else {
            pieces[{x, y}] = p;
        }
    }
};

代价是访问时间变为 O(log n),不适合实时性强的场景。因此是否采用要看具体用途。


回合控制与玩家状态管理

棋盘有了,接下来要让游戏动起来。

五子棋是典型的回合制游戏,两名玩家交替落子。我们需要一个状态机来追踪当前是谁的回合。

Player 枚举与 TurnManager 设计

enum class Player {
    NONE = 0,
    BLACK = 1,  // 先手
    WHITE = 2   // 后手
};

然后封装一个轻量级的状态管理器:

class TurnManager {
    Player currentPlayer;
public:
    TurnManager() : currentPlayer(Player::BLACK) {}

    Player getCurrentPlayer() const { return currentPlayer; }

    void switchPlayer() {
        currentPlayer = (currentPlayer == Player::BLACK) ? 
                        Player::WHITE : Player::BLACK;
    }

    void resetToBlack() { currentPlayer = Player::BLACK; }
};

简单、高效、无副作用。

状态流转可以用 mermaid 描述:

stateDiagram-v2
    [*] --> BlackTurn
    BlackTurn --> WhiteTurn: 成功落子
    WhiteTurn --> BlackTurn: 成功落子
    BlackTurn --> GameOver: 黑方胜
    WhiteTurn --> GameOver: 白方胜
    GameOver --> [*]

是不是有种“我在设计一个真实系统”的感觉?😄

防御性编程:禁止重复落子

任何时候都不能允许在同一位置落两次子。除了 Board 层面的防护, Game 类也要加强检查:

bool Game::makeMove(int x, int y) {
    if (!board.isValidPosition(x, y)) {
        std::cerr << "错误:坐标越界\n";
        return false;
    }
    if (!board.isEmpty(x, y)) {
        std::cerr << "错误:该位置已有棋子\n";
        return false;
    }

    board.setCell(x, y, turnMgr.getCurrentPlayer());

    if (checkWin(x, y)) {
        gameStatus = GAME_OVER;
        winner = turnMgr.getCurrentPlayer();
        return true;
    }

    turnMgr.switchPlayer();
    return true;
}

这种“事务式”操作保证了状态一致性:要么完整执行,要么回滚。


用户交互设计:命令行也能玩得很爽

谁说命令行就不能有良好体验?只要用心,照样能做出流畅的人机交互。

输入解析:从”A1”到(0,0)

用户习惯输入 "A1" "H8" 这种格式。我们需要将其转换为数组索引。

借助正则表达式:

std::pair<int, int> parseInput(const std::string& input) {
    std::regex pattern(R"(^([A-O])(\d{1,2})$)", std::regex_constants::icase);
    std::smatch matches;

    if (!std::regex_match(input, matches, pattern))
        throw std::invalid_argument("请输入有效格式,如 A1");

    char colChar = std::toupper(matches[1].str()[0]);
    int row = std::stoi(matches[2].str()) - 1;
    int col = colChar - 'A';

    return {row, col};
}

例如 "H8" (7,7) ,刚好对应中心点。

错误处理与重试机制

bool readValidMove(int& x, int& y) {
    std::string input;
    while (true) {
        std::cout << "请输入落子位置 (如 A1): ";
        std::cin >> input;

        if (input == "QUIT") return false;

        try {
            auto [r, c] = parseInput(input);
            x = r; y = c;
            return true;
        } catch (...) {
            std::cerr << "格式错误,请重新输入\n";
        }
    }
}

支持 QUIT 指令退出,其他错误自动提示重试。


棋盘可视化:ASCII艺术也能赏心悦目

虽然没有图形界面,但我们依然可以用字符画出漂亮的棋盘。

void Board::print() const {
    std::cout << "   ";
    for (char c = 'A'; c < 'A' + BOARD_SIZE; ++c)
        std::cout << " " << c;
    std::cout << "\n";

    for (int i = 0; i < BOARD_SIZE; ++i) {
        std::cout << (i + 1 < 10 ? " " : "") << (i + 1) << " ";
        for (int j = 0; j < BOARD_SIZE; ++j) {
            switch (get(i, j)) {
                case Piece::EMPTY: std::cout << ". "; break;
                case Piece::BLACK: std::cout << "● "; break;
                case Piece::WHITE: std::cout << "○ "; break;
            }
        }
        std::cout << "\n";
    }
}

输出效果:

    A B C D E F G H I J K L M N O
 1  . . . . . . . . . . . . . . .
 2  . . . . . . . . . . . . . . .
 ...
 8  . . . . . . . ● ○ . . . . . .
 ...

搭配清屏刷新,简直就像真的GUI一样:

void clearScreen() {
#ifdef _WIN32
    system("cls");
#else
    system("clear");
#endif
}

void renderFrame(const Board& b, const TurnManager& t) {
    clearScreen();
    b.print();
    std::cout << "当前玩家: "
              << (t.getCurrentPlayer() == Player::BLACK ? "黑方(●)" : "白方(○)")
              << "\n";
}

胜负判定:五连珠检测算法详解

这是整个游戏中最关键的算法之一。

不能遍历全图,那样太慢。我们应该 以落子点为中心,向四个方向延伸扫描

四个方向分别是:
- 横向(→)
- 纵向(↓)
- 主对角线(↘)
- 副对角线(↙)

bool Game::checkWin(int cx, int cy) {
    Player piece = board.get(cx, cy);
    if (piece == Player::NONE) return false;

    const int dx[] = {1, 0, 1, 1};
    const int dy[] = {0, 1, 1, -1};

    for (int i = 0; i < 4; ++i) {
        int count = 1;  // 包含自己

        // 正向扩展
        for (int step = 1; step < 5; ++step) {
            int nx = cx + dx[i] * step, ny = cy + dy[i] * step;
            if (!board.isValid(nx, ny) || board.get(nx, ny) != piece)
                break;
            count++;
        }

        // 反向扩展
        for (int step = 1; step < 5; ++step) {
            int nx = cx - dx[i] * step, ny = cy - dy[i] * step;
            if (!board.isValid(nx, ny) || board.get(nx, ny) != piece)
                break;
            count++;
        }

        if (count >= 5) return true;
    }
    return false;
}

时间复杂度 O(1),实测纳秒级响应 ⚡


禁手规则实现:竞技级公平保障

在专业五子棋中,为了平衡黑方先手优势,引入了“禁手”规则:
- 三三禁 :黑方同时形成两个活三
- 四四禁 :同时形成两个活四
- 长连禁 :连六及以上判负

这些都需要通过局部模式匹配来识别。

bool isForbiddenMove(const Board& board, int x, int y) {
    if (board.get(x, y) != Piece::BLACK) return false;

    int live3 = 0, live4 = 0;

    for (auto [dx,dy] : dirs) {
        auto [l,r] = scanLine(board, x, y, dx, dy);
        int total = l.count + r.count + 1;
        bool openL = l.isOpen, openR = r.isOpen;

        if (total == 3 && openL && openR) live3++;
        else if (total == 4 && openL && openR) live4++;
        else if (total >= 6) return true;  // 长连禁
    }

    return live3 >= 2 || live4 >= 2;
}

结合AI时尤其要注意:不能让AI犯禁手!


AI对战系统:minimax + alpha-beta剪枝

终于到了最激动人心的部分:让程序学会思考!

Minimax 框架

int minimax(Board board, int depth, bool isMaximizing) {
    if (depth == 0 || board.isGameOver())
        return evaluate(board);

    if (isMaximizing) {
        int maxEval = INT_MIN;
        for (auto move : board.getMoves()) {
            board.makeMove(move);
            int eval = minimax(board, depth - 1, false);
            maxEval = std::max(maxEval, eval);
            board.undoMove(move);
        }
        return maxEval;
    } else {
        // 类似逻辑...
    }
}

Alpha-Beta 剪枝优化

加入剪枝后性能提升数倍:

int alphabeta(Board& b, int d, int alpha, int beta, bool m) {
    if (d == 0 || b.win()) return evaluate(b);

    if (m) {
        for (...)
            alpha = std::max(alpha, alphabeta(...));
            if (beta <= alpha) break;
    } else {
        for (...)
            beta = std::min(beta, alphabeta(...));
            if (beta <= alpha) break;
    }
    return m ? alpha : beta;
}

配合迭代加深和移动排序,实战表现非常强劲!


多线程与调试保障

为了让AI思考时不卡住UI,我们可以开启后台线程预计算:

std::thread ai_thread([&]{
    while (running) {
        if (needThink) bestMove = findBestMove();
        std::this_thread::sleep_for(100ms);
    }
});

共享资源用 mutex 保护:

std::mutex mtx;
void safePlace(int x, int y) {
    std::lock_guard lock(mtx);
    board.place(x, y);
}

单元测试也不落下:

TEST(ForbiddenTest, ThreeThree) {
    Board b;
    setupThreeThree(b);
    EXPECT_TRUE(isForbidden(b, 7,7));
}

GDB调试更是家常便饭:

gdb ./gomoku
(gdb) break checkWin
(gdb) condition 1 cx==7 && cy==7
(gdb) run

结语

从一个简单的二维数组出发,我们逐步构建出了一个功能完整、性能优异、可扩展性强的五子棋系统。这其中涉及的知识点几乎涵盖了C++开发的核心技能栈:

✅ 数据结构选型
✅ 面向对象设计
✅ 算法优化
✅ AI决策
✅ 多线程
✅ 测试与调试

这种“由浅入深、层层递进”的构建过程,正是大型软件系统演化的缩影。

更重要的是,它提醒我们: 真正优秀的代码,不只是让它跑起来,而是让它活得久、长得大、变得快

所以,下次当你面对一个新的项目时,不妨问问自己:

“我的第一个 Board 类,够不够健壮?” 🤔

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:【源程序_regionj2k_五子棋_C++五子棋游戏】是一个基于C++语言实现的五子棋对弈项目,适合作为游戏编程初学者的学习案例。项目涵盖了从基础数据结构到复杂游戏逻辑的完整实现,包括棋盘表示、落子规则、胜负判断、禁手处理、用户交互及可扩展的AI算法设计。通过命令行或图形界面展示,结合递归搜索与剪枝优化,帮助开发者掌握游戏开发核心技术。项目还强调代码组织、调试测试、版本控制与文档编写,全面提升C++编程与软件工程实践能力。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐