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

所有评论(0)