简介:这是一份面向C++初学者与游戏编程爱好者的五子棋实战源码,同时支持人机对战与人人对战两种模式,适合用来理解面向对象设计、AI决策与图形交互等核心概念。压缩包共7个文件,约106KB,包含3个cpp源码文件、3个可执行程序与1份说明文档,源码按鼠标版、键盘版、人对人版、人对机版分别组织,便于对照不同交互方式与对战逻辑的实现差异。已有180人学习下载。读者可从中掌握棋盘与棋子类的抽象方式、人机模式下基于规则或搜索的落子决策思路、玩家输入的捕获与校验流程,以及界面渲染与点击事件响应机制。对于希望提升C++编程技巧、了解游戏开发基本流程的开发者而言,这份源码可作为动手分析与二次优化的实践素材。
1. 从一份 C++ 五子棋源码说起:人机对弈和双人模式到底怎么落地
很多刚学完 C++ 语法的人都会卡在同一个地方:语法书翻完了,循环、数组、函数、类都认识,但打开 IDE 却不知道从哪写起。这份 C++ 五子棋源码就是冲着这个痛点来的——它把「五子棋 游戏 源代码」这个关键词背后的东西拆成了两个可运行的模式:人机对弈和人人对弈。棋盘渲染、落子判定、胜负检测、简单 AI 决策,全在一个工程里跑通。适合谁?适合刚学完 C++ 基础、想找一个完整小项目练手的人,也适合想拿它当课程设计底稿、再往上加悔棋、加存档、加难度选择的开发者。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份源码拆开讲。
2. 工程结构与技术选型:控制台棋盘、二维数组和极小化决策
2.1 目录结构:一个 main 加三个核心模块
拿到源码包后,先别急着编译。我一般会先扫一遍目录,确认入口在哪、哪些文件是核心逻辑、哪些是辅助。这份五子棋工程的典型结构如下:
gomoku/ ├── main.cpp // 程序入口,模式选择菜单 ├── board.h // 棋盘类声明:落子、判胜、打印 ├── board.cpp // 棋盘类实现 ├── ai.h // AI 决策接口 ├── ai.cpp // 人机模式下的落子评分逻辑 └── Makefile // 编译脚本main.cpp只负责一件事:让用户选「人机」还是「人人」,然后进入对应的游戏循环。board模块管棋盘状态和规则,ai模块只在人机模式下被调用。这种拆分的好处是,你想换 AI 算法,只动ai.cpp;想换界面,只动board的打印函数,互不干扰。
提示:如果你的源码包里没有 Makefile,用 g++ 直接编译也可以,命令在下一节给出。
2.2 棋盘表示:15×15 二维数组与状态枚举
五子棋棋盘是 15×15,最自然的表示就是一个二维数组。源码里通常用int或枚举来标记每个交叉点的状态:
// board.h const int BOARD_SIZE = 15; enum CellState { EMPTY = 0, // 空位 BLACK = 1, // 黑子(先手) WHITE = 2 // 白子(后手) }; class Board { public: Board(); bool placeStone(int row, int col, CellState player); // 落子 bool checkWin(int row, int col, CellState player); // 判胜 void print() const; // 打印棋盘 CellState getCell(int row, int col) const; private: CellState grid[BOARD_SIZE][BOARD_SIZE]; };这里有几个参数值得注意。BOARD_SIZE写成常量而不是硬编码 15,是为了后面想改成 19×19 时只改一处。CellState用枚举而不是裸int,是为了让placeStone的调用方一眼看出传的是黑还是白。checkWin接收最后落子的坐标,而不是全盘扫描——这是性能上的常见做法,只检查落子点周围四个方向是否连成五子,复杂度从 O(n²) 降到 O(1)。
2.3 胜负判定:四方向延伸检测
判胜是整个游戏最核心的规则逻辑。源码里通常这样实现:
// board.cpp bool Board::checkWin(int row, int col, CellState player) { // 四个方向:横、竖、主对角线、副对角线 const int dr[4] = {0, 1, 1, 1}; const int dc[4] = {1, 0, 1, -1}; for (int d = 0; d < 4; ++d) { int count = 1; // 当前落子本身算一个 // 正方向延伸 for (int step = 1; step < 5; ++step) { int r = row + dr[d] * step; int c = col + dc[d] * step; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break; if (grid[r][c] != player) break; ++count; } // 反方向延伸 for (int step = 1; step < 5; ++step) { int r = row - dr[d] * step; int c = col - dc[d] * step; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break; if (grid[r][c] != player) break; ++count; } if (count >= 5) return true; } return false; }dr和dc两个数组定义了四个检测方向:(0,1)是横向,(1,0)是纵向,(1,1)是主对角线,(1,-1)是副对角线。每个方向从落子点向两侧各延伸最多 4 格,累计同色棋子数。一旦达到 5,立即返回胜利。边界检查放在循环内部,避免数组越界——这是新手最容易翻车的地方,少一个r >= BOARD_SIZE判断,程序就可能读到非法内存。
2.4 AI 决策:评分表加一层浅搜索
人机模式的 AI 不需要 AlphaGo 那种复杂度,源码里用的是一种「评分 + 选最优」的策略。核心思路是:对每个空位,分别计算如果黑子下在这里值多少分、白子下在这里值多少分,取两者中较大的作为该点的威胁值,然后选威胁值最高的位置落子。
// ai.cpp int evaluatePoint(const Board& board, int row, int col, CellState player) { // 简化版评分:统计四个方向上同色连子的潜在长度 int score = 0; const int dr[4] = {0, 1, 1, 1}; const int dc[4] = {1, 0, 1, -1}; for (int d = 0; d < 4; ++d) { int count = 1; // 正向和反向统计连续同色棋子 for (int dir = -1; dir <= 1; dir += 2) { for (int step = 1; step < 5; ++step) { int r = row + dr[d] * step * dir; int c = col + dc[d] * step * dir; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break; if (board.getCell(r, c) == player) ++count; else break; } } // 连子越长,分数指数上升 if (count >= 5) score += 100000; else if (count == 4) score += 10000; else if (count == 3) score += 1000; else if (count == 2) score += 100; else if (count == 1) score += 10; } return score; } std::pair<int, int> chooseBestMove(const Board& board, CellState aiPlayer) { CellState opponent = (aiPlayer == BLACK) ? WHITE : BLACK; int bestScore = -1; std::pair<int, int> bestMove = {-1, -1}; for (int r = 0; r < BOARD_SIZE; ++r) { for (int c = 0; c < BOARD_SIZE; ++c) { if (board.getCell(r, c) != EMPTY) continue; // 进攻分 + 防守分,防守权重略高 int attackScore = evaluatePoint(board, r, c, aiPlayer); int defendScore = evaluatePoint(board, r, c, opponent); int total = attackScore + defendScore * 2; // 防守优先 if (total > bestScore) { bestScore = total; bestMove = {r, c}; } } } return bestMove; }evaluatePoint里的分数梯度是经验值:连五 100000,活四 10000,活三 1000,活二 100。chooseBestMove里把对手的得分乘以 2 再加到自己的进攻分上,意思是「堵对手比我自己连更紧急」。这个 AI 不算强,但对付休闲玩家够用,而且代码短、好改——你想加难度,把防守权重从 2 调到 3 或 4,AI 就会更保守。
3. 编译与运行:从源码到可执行文件的完整步骤
3.1 环境准备与编译命令
这份源码只依赖标准 C++ 库,不需要额外装图形库或网络库。Linux 和 macOS 下用 g++ 直接编译:
# 进入源码目录 cd gomoku # 编译所有 cpp 文件,输出可执行文件 gomoku g++ -std=c++11 -O2 main.cpp board.cpp ai.cpp -o gomoku # 运行 ./gomoku-std=c++11是因为源码里用了std::pair和范围 for 循环的常见写法;-O2开优化,AI 遍历 225 个空位时能快一点。Windows 下如果用 MinGW,命令一样;如果用 Visual Studio,新建空项目把 cpp 文件加进去,直接 F5 就行。
注意:如果编译报
undefined reference to Board::placeStone,说明board.cpp没被加进编译列表,检查命令里有没有漏掉某个 cpp 文件。
3.2 模式选择与游戏主循环
main.cpp的逻辑很直白:先打印菜单,读入用户选择,然后进入对应的循环。
// main.cpp int main() { Board board; int mode = 0; std::cout << "1. 人机对战\n2. 人人对战\n请选择: "; std::cin >> mode; CellState current = BLACK; // 黑先 while (true) { board.print(); if (mode == 1 && current == WHITE) { // AI 回合 auto move = chooseBestMove(board, WHITE); board.placeStone(move.first, move.second, WHITE); if (board.checkWin(move.first, move.second, WHITE)) { board.print(); std::cout << "AI 获胜\n"; break; } } else { // 人类回合 int r, c; std::cout << "轮到 " << (current == BLACK ? "黑" : "白") << " 落子 (行 列): "; std::cin >> r >> c; if (!board.placeStone(r, c, current)) { std::cout << "非法落子,请重试\n"; continue; } if (board.checkWin(r, c, current)) { board.print(); std::cout << (current == BLACK ? "黑" : "白") << " 获胜\n"; break; } } current = (current == BLACK) ? WHITE : BLACK; // 换手 } return 0; }placeStone返回bool,用来处理非法输入——比如用户输入了已占用的位置或超出棋盘范围的坐标。返回false时不换手,让用户重新输入。这个细节很多新手会漏,导致程序在非法输入后直接崩溃或状态错乱。
3.3 棋盘打印:控制台对齐的常见做法
控制台棋盘要好看,关键是列号对齐。源码里通常这样打印:
void Board::print() const { std::cout << " "; for (int c = 0; c < BOARD_SIZE; ++c) std::cout << std::setw(2) << c << " "; std::cout << "\n"; for (int r = 0; r < BOARD_SIZE; ++r) { std::cout << std::setw(2) << r << " "; for (int c = 0; c < BOARD_SIZE; ++c) { char ch = '.'; if (grid[r][c] == BLACK) ch = 'X'; else if (grid[r][c] == WHITE) ch = 'O'; std::cout << " " << ch << " "; } std::cout << "\n"; } }std::setw(2)保证行号和列号占两个字符宽度,个位数和两位数都能对齐。黑子用X,白子用O,空位用.。如果你觉得X和O不够直观,改成●和○也行,但要注意终端编码——UTF-8 下没问题,GBK 下可能乱码。
4. 常见问题排查:编译报错、AI 卡死和落子越界
4.1 编译时报‘setw’ is not a member of ‘std’
现象:编译到board.cpp时提示setw找不到。原因:std::setw定义在<iomanip>头文件里,源码可能只在board.h里包含了<iostream>,没包含<iomanip>。解决:在board.cpp或board.h顶部加一行#include <iomanip>。
4.2 AI 落子后程序卡住不动
现象:人机模式下,AI 回合结束后光标一直闪,不换手也不结束。原因:chooseBestMove返回了{-1, -1},说明棋盘上没有空位了,但主循环没有处理平局。解决:在chooseBestMove返回前加一个判断,如果bestMove.first == -1,说明棋盘已满,主循环里检测到这个情况就打印「平局」并break。
4.3 用户输入坐标后提示非法,但坐标明明是对的
现象:输入7 7,程序说非法落子。原因:源码里的坐标可能从 0 开始计数,而用户习惯从 1 开始。或者std::cin读入失败后进入了错误状态,后续所有读取都失败。解决:在std::cin >> r >> c后面加std::cin.clear()和std::cin.ignore()清理错误状态;同时在菜单里明确提示「坐标从 0 开始」。
4.4 判胜时数组越界导致崩溃
现象:在棋盘边缘落子后,程序直接段错误。原因:checkWin里的边界检查写成了r < 0 || r > BOARD_SIZE,应该是r >= BOARD_SIZE。>和>=差一个,就会读到grid[15][...],越界。解决:把所有边界判断统一改成r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE,并且用常量BOARD_SIZE而不是硬编码 15。
4.5 人人模式下黑白不分,落子颜色错乱
现象:人人对战时,黑子下完下一步还是黑子。原因:换手逻辑写成了current = BLACK而不是current = (current == BLACK) ? WHITE : BLACK。解决:检查主循环末尾的换手语句,确保每次落子后都切换current。
5. 二次开发改造:从评分 AI 到极小化搜索的升级路径
这份源码的 AI 用的是「评分选最优」,优点是代码短、好懂,缺点是只看一步,容易被双三、双四的复合威胁骗过去。如果你想把它改成能「想两步」的 AI,最直接的路径是加一层极小化搜索(Minimax)。核心改动在ai.cpp:把chooseBestMove从「遍历空位选最高分」改成「遍历空位,对每个空位模拟落子,然后递归评估对手的最佳回应」。
// ai.cpp 改造示意:带一层前瞻的选点 int minimax(Board& board, int depth, bool isMax, CellState aiPlayer) { // 达到深度或分出胜负时返回评估值 if (depth == 0) return evaluateBoard(board, aiPlayer); CellState current = isMax ? aiPlayer : opponentOf(aiPlayer); int best = isMax ? -INF : INF; for (int r = 0; r < BOARD_SIZE; ++r) { for (int c = 0; c < BOARD_SIZE; ++c) { if (board.getCell(r, c) != EMPTY) continue; board.placeStone(r, c, current); if (board.checkWin(r, c, current)) { board.undo(r, c); // 需要给 Board 加 undo 方法 return isMax ? WIN_SCORE : -WIN_SCORE; } int val = minimax(board, depth - 1, !isMax, aiPlayer); board.undo(r, c); best = isMax ? std::max(best, val) : std::min(best, val); } } return best; }这里有两个关键改动。第一,Board类需要加一个undo(row, col)方法,把grid[row][col]重置为EMPTY,否则模拟落子会污染真实棋盘。第二,evaluateBoard要改成全盘评估,而不是单点评估——通常做法是遍历所有棋子,按连子长度和开放程度累加分数。深度建议从 2 开始试,深度 4 在 15×15 棋盘上已经能明显感觉到 AI 变强,但耗时也会上去,需要配合 Alpha-Beta 剪枝。
另一个改造方向是加悔棋。思路很简单:在Board里维护一个落子历史栈,每次placeStone把坐标压栈,undo时弹栈并清空对应格子。主循环里加一个「输入 -1 -1 表示悔棋」的分支,连续弹两次(撤销 AI 和玩家各一步)。这个功能对新手体验提升很大,代码量却不到 30 行。
我自己的习惯是,每次改完 AI 逻辑,都强制走一遍「黑棋先手连下五子」的测试用例,确认 AI 至少能堵住活四。从那以后我每次拿到类似的小项目源码,都先跑通默认模式,再动一行改一行,绝不一次性重写。希望这份拆解能帮你把这份 C++ 五子棋源码真正跑起来、改起来。
本文还有配套的精品资源,点击获取