五子棋,作为一种深受喜爱的棋类游戏,其规则简单而富有策略性。对于 C 语言初学者和爱好者而言,亲手实现一个控制台版本的五子棋游戏,是深入理解 C 语言基础、数组操作、函数设计、逻辑判断以及输入输出处理的绝佳实践。
今天,我将带大家从零开始,一步步构建一个功能完整的 C 语言五子棋游戏。将深入探讨游戏的核心逻辑、各模块的实现细节,并提供完整的可运行源代码,在 VS Code 等开发环境中轻松部署和体验。
1. 游戏设计概览
我们的目标是创建一个双人对弈的命令行五子棋游戏。核心功能包括:
-
棋盘: 一个 15x15 的标准五子棋盘。
-
玩家: 两名玩家轮流落子,分别用不同符号表示。
-
落子: 玩家通过输入行列坐标来确定落子位置。
-
胜负判断: 任意一方在横、竖、斜任意方向上,棋子连成五子(或更多)即获胜。
-
平局判断: 棋盘下满且无任何一方获胜,则为平局。
-
用户交互: 清晰的棋盘显示、操作提示、胜负或平局信息。
2. 核心数据结构
游戏的基石是对棋盘的表示。在 C 语言中,最自然的选择是使用一个二维数组。
#define BOARD_SIZE 15 // 定义棋盘边长
int board[BOARD_SIZE][BOARD_SIZE]; // 15x15 的整数数组
-
BOARD_SIZE
:宏定义,方便修改棋盘大小。 -
board[BOARD_SIZE][BOARD_SIZE]
:一个二维整数数组。-
我们约定:
-
EMPTY (0)
:表示该位置为空。 -
PLAYER1 (1)
:表示玩家 1 的棋子(通常为黑棋)。 -
PLAYER2 (2)
:表示玩家 2 的棋子(通常为白棋)。
-
-
此外,我们还需要一个变量来跟踪当前是哪位玩家的回合:
int currentPlayer; // 存储当前玩家的编号 (1 或 2)
为了方便显示,我们可以定义一个字符串数组来映射棋子类型到对应的字符:
const char *piece_chars[] = {" ", "●", "○"}; // piece_chars[0] -> " ", piece_chars[1] -> "●", piece_chars[2] -> "○"
3. 游戏逻辑模块详解
一个组织良好的程序应该由模块化的函数组成。以下是五子棋游戏的主要功能模块及其设计:
3.1 棋盘初始化 (initialize_board()
)
游戏开始前,棋盘需要被清空,并设定初始玩家。
void initialize_board() {
// 使用 memset 将整个棋盘数组的所有字节设置为 0 (EMPTY)
memset(board, EMPTY, sizeof(board));
currentPlayer = PLAYER1; // 默认玩家1先手
}
-
memset(board, EMPTY, sizeof(board))
:这是一个非常高效的清零操作。memset
函数将board
数组的sizeof(board)
字节全部填充为EMPTY
(即 0),从而快速清空棋盘。
3.2 棋盘显示 (print_board()
)
在每个回合开始时,棋盘需要被重新打印,以便玩家看到最新的局势。为了保持界面的整洁,每次打印前会清空控制台。
void clear_screen() {
#ifdef _WIN32
system("cls"); // Windows 系统命令
#else
system("clear"); // Linux/macOS 系统命令
#endif
}
void print_board() {
clear_screen();
// ... 打印棋盘的头部信息、列索引、边框和棋子 ...
}
-
clear_screen()
:使用条件编译(#ifdef _WIN32
)来判断操作系统类型,从而调用不同的清屏命令。 -
print_board()
:精心设计输出格式,包括列索引、行索引、棋盘边框,以及使用piece_chars
数组来打印棋子符号(" "
、"●"
、"○"
)。
3.3 检查落子有效性 (is_valid_move()
)
玩家输入落子位置后,我们需要验证这个位置是否合法。
bool is_valid_move(int row, int col) {
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
// ... 超出棋盘范围 ...
return false;
}
if (board[row][col] != EMPTY) {
// ... 位置已被占用 ...
return false;
}
return true;
}
-
边界检查: 确保输入的
row
和col
都在[0, BOARD_SIZE - 1]
的有效范围内。 -
占用检查: 确保目标位置
board[row][col]
当前是空的 (EMPTY
)。
3.4 执行落子 (make_move()
)
如果落子有效,就将当前玩家的棋子放置到棋盘上。
void make_move(int row, int col) {
board[row][col] = currentPlayer;
}
-
这只是一个简单的赋值操作,因为它在
is_valid_move()
之后被调用,所以我们已经确定了位置的合法性。
3.5 胜负判断 (check_win()
)
这是五子棋游戏最核心和最复杂的逻辑之一。当一名玩家落子后,我们需要检查他是否形成了五子连珠。这需要检查水平、垂直和两条对角线方向。
bool check_win(int row, int col) {
int count;
// 定义8个方向的行、列偏移量
int dr[] = {-1, -1, -1, 0, 0, 1, 1, 1}; // 行方向:左上、上、右上、左、右、左下、下、右下
int dc[] = {-1, 0, 1, -1, 1, -1, 0, 1}; // 列方向:左上、上、右上、左、右、左下、下、右下
// 遍历8个方向(水平、垂直、对角线)
for (int i = 0; i < 8; i++) {
count = 1; // 计数从当前落子开始,至少为1
int r, c;
// 向一个方向延伸计数
r = row + dr[i];
c = col + dc[i];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == currentPlayer) {
count++;
r += dr[i];
c += dc[i];
}
// 向相反方向延伸计数(形成一条直线)
r = row - dr[i];
c = col - dc[i];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == currentPlayer) {
count++;
r -= dr[i];
c -= dc[i];
}
if (count >= 5) { // 如果在任何一个方向上连续棋子数达到或超过5,则当前玩家获胜
return true;
}
}
return false; // 没有连成五子
}
-
方向数组:
dr
和dc
数组巧妙地定义了 8 个方向的单位步长。例如,dr[0]=-1, dc[0]=-1
表示左上方向。 -
双向计数: 对于每个方向,我们从最新落子点开始,向该方向和其相反方向分别延伸计数。例如,检查水平方向时,会向左计数和向右计数,然后将两者之和加上当前棋子本身,得到总的连续棋子数。
-
边界检查: 在延伸计数时,始终检查
r
和c
是否在棋盘范围内,避免越界访问。
3.6 平局判断 (check_draw()
)
如果棋盘上所有位置都被填满,并且没有人获胜,那么游戏就是平局。
bool check_draw() {
for (int r = 0; r < BOARD_SIZE; r++) {
for (int c = 0; c < BOARD_SIZE; c++) {
if (board[r][c] == EMPTY) {
return false; // 找到空位,不是平局
}
}
}
return true; // 遍历完所有位置都没有空位,是平局
}
-
这个函数非常简单,只需要遍历整个棋盘,如果找到任何一个空位,就说明还没平局;如果所有位置都被占用,则为平局。
3.7 切换玩家 (switch_player()
)
每个回合结束后,如果游戏没有结束,就需要切换到另一位玩家。
void switch_player() {
currentPlayer = (currentPlayer == PLAYER1) ? PLAYER2 : PLAYER1;
}
-
使用三元运算符简洁地实现了玩家切换。
3.8 显示胜利者 (display_winner()
)
游戏结束时,清晰地告知玩家胜负结果。
void display_winner(int winner) {
clear_screen(); // 清屏
print_board(); // 打印最终棋盘
// ... 打印恭喜信息 ...
}
3.9 游戏主循环 (game_loop()
)
这是整个游戏的驱动核心,它协调所有其他函数的执行顺序。
void game_loop() {
int row, col;
bool game_over = false;
initialize_board(); // 游戏开始
while (!game_over) {
print_board(); // 打印当前棋盘
printf("玩家 %s,请输入您的落子位置 (行 列,例如: 7 7): ", piece_chars[currentPlayer]);
// 读取玩家输入
if (scanf("%d %d", &row, &col) != 2) { // 检查是否成功读取两个整数
printf("无效输入!请输入两个整数 (行 列)。\n");
while (getchar() != '\n'); // 清空输入缓冲区
continue; // 重新请求输入
}
if (is_valid_move(row, col)) { // 验证输入
make_move(row, col); // 落子
if (check_win(row, col)) { // 检查获胜
display_winner(currentPlayer);
game_over = true;
} else if (check_draw()) { // 检查平局
// ... 打印平局信息 ...
game_over = true;
} else {
switch_player(); // 切换玩家
}
}
}
// 游戏结束后的选择:再玩一局或退出
char play_again_choice;
printf("是否再玩一局?(y/n): ");
scanf(" %c", &play_again_choice); // 注意前面的空格,用于跳过上一次 scanf 留下的换行符
if (play_again_choice == 'y' || play_again_choice == 'Y') {
game_loop(); // 递归调用,重新开始游戏
} else {
printf("感谢您的游玩!再见。\n");
}
}
-
核心循环:
while (!game_over)
确保游戏持续进行,直到有玩家获胜或平局。 -
输入处理:
scanf("%d %d", &row, &col)
用于读取玩家输入的行列坐标。特别注意:scanf
返回成功读取的项数。!= 2
表示读取失败,需要进行错误处理并清空输入缓冲区(while (getchar() != '\n');
)以防止无限循环。 -
游戏流程: 清屏 -> 打印棋盘 -> 获取输入 -> 验证输入 -> 落子 -> 检查胜负 -> 检查平局 -> 切换玩家。
4. 如何在 VS Code 中运行
在 VS Code 中运行 C 语言程序通常需要安装 GCC 编译器,并配置 VS Code 的 C/C++ 扩展。
-
安装 GCC/MinGW (Windows) 或 Clang/GCC (Linux/macOS):
-
Windows: 推荐安装 MinGW-w64。可以从 MinGW-w64 official website 下载并安装,确保将其
bin
目录添加到系统 PATH 环境变量。 -
Linux: 通常预装 GCC。如果没有,可以通过包管理器安装:
sudo apt update && sudo apt install build-essential
(Ubuntu/Debian)。 -
macOS: 安装 Xcode Command Line Tools,它会包含 Clang 编译器:
xcode-select --install
。
-
-
安装 VS Code C/C++ 扩展:
-
打开 VS Code,前往 Extensions 视图 (Ctrl+Shift+X)。
-
搜索 "C/C++" 并安装由 Microsoft 提供的扩展。
-
-
创建 C 文件:
-
在 VS Code 中创建一个新文件,例如
gomoku.c
。 -
将上面提供的完整 C 语言代码复制粘贴到
gomoku.c
文件中。
-
-
编译与运行:
-
通过终端运行 (推荐简单项目):
-
打开 VS Code 集成终端 (Ctrl+` )。
-
进入
gomoku.c
文件所在的目录。 -
使用 GCC 编译:
gcc gomoku.c -o gomoku.exe
(Windows) 或gcc gomoku.c -o gomoku
(Linux/macOS)。 -
运行编译后的程序:
./gomoku.exe
(Windows) 或./gomoku
(Linux/macOS)。
-
-
通过 VS Code 任务配置 (更自动化):
-
在 VS Code 中,按
Ctrl+Shift+P
打开命令面板,输入Tasks: Configure Default Build Task
。 -
选择
Create tasks.json file from template
,然后选择Others
。 -
tasks.json
文件会自动生成在.vscode
文件夹下。修改其内容如下:
{ "version": "2.0.0", "tasks": [ { "label": "build gomoku", // 任务名称 "type": "shell", "command": "gcc", // 你的编译器 "args": [ "gomoku.c", // 你的源文件 "-o", "gomoku", // 输出的可执行文件名称 (Windows 上可以是 gomoku.exe) "-g", // 启用调试信息 "-Wall" // 启用所有警告 ], "group": { "kind": "build", "isDefault": true }, "problemMatcher": [ "$gcc" ], "detail": "生成五子棋可执行文件" }, { "label": "run gomoku", // 运行任务 "type": "shell", "command": "./gomoku", // 运行可执行文件 (Windows 上是 ./gomoku.exe) "group": { "kind": "test", // 可以是test或其他,方便分类 "isDefault": false }, "dependsOn": "build gomoku", // 运行前先确保编译 "problemMatcher": [] } ] }
-
保存
tasks.json
。 -
现在,你可以通过
Ctrl+Shift+B
(或Terminal -> Run Build Task
) 编译程序。 -
通过
Ctrl+Shift+P
,输入Tasks: Run Task
,然后选择run gomoku
来运行游戏。
-
-
5. 进一步的优化与扩展
当前的五子棋游戏已经功能完整,但仍有许多可以改进和扩展的地方:
-
更友好的用户界面:
-
可以使用一些简单的图形库(如 ncurses 或 PDCurses)来创建伪图形界面,实现更平滑的棋盘更新,避免频繁清屏带来的闪烁。
-
实现光标移动来选择落子位置,而不是手动输入坐标。
-
-
人工智能 (AI) 对手:
-
实现一个简单的 AI 玩家,例如基于随机选择空位落子,或者更复杂的算法,如 Minimax 或 Alpha-Beta 剪枝。
-
-
悔棋功能:
-
使用栈或链表记录每一步棋的历史,实现悔棋功能。
-
-
保存/加载游戏进度:
-
将当前棋盘状态和游戏信息保存到文件,以便下次继续游戏。
-
-
规则增强:
-
实现禁手规则(如三三禁手、四四禁手、长连禁手),使游戏更符合专业五子棋规则。
-
-
错误处理健壮性:
-
更细致的输入验证,例如处理非数字输入。
-
使用
perror()
和exit()
处理内存分配失败等更严重的系统错误。
-
总结
通过本文,从零开始,使用 C 语言撸了一个功能完整的控制台五子棋游戏。包括棋盘表示、落子判断、胜负和平局检查等。主要其实是锻炼了解决问题、设计程序结构以及处理用户交互的能力。
C 语言的魅力在于它提供了对底层资源的直接控制,但也要求程序员承担起内存管理和错误处理的责任。通过这个五子棋项目,您不仅掌握了游戏编程的入门知识,也进一步巩固了 C 语言编程的核心技能。希望这个项目能激发您对 C 语言和游戏开发的更多兴趣!
*附录:
#include <stdio.h> // 包含标准输入输出库,用于printf和scanf
#include <stdlib.h> // 包含标准库,用于exit和system函数
#include <stdbool.h> // 包含布尔类型支持
#include <string.h> // 包含字符串处理函数,用于memset
// 定义棋盘大小
#define BOARD_SIZE 15 // 五子棋标准棋盘大小为 15x15
// 定义棋子类型
#define EMPTY 0 // 空
#define PLAYER1 1 // 玩家1 (黑棋)
#define PLAYER2 2 // 玩家2 (白棋)
// 棋盘数组,存储每个位置的棋子类型
// 使用全局变量简化函数间的传递
int board[BOARD_SIZE][BOARD_SIZE];
// 当前玩家,PLAYER1 或 PLAYER2
int currentPlayer;
// 棋盘显示字符
const char *piece_chars[] = {" ", "●", "○"}; // " ", "黑棋", "白棋"
// --- 函数声明 ---
void initialize_board();
void print_board();
bool is_valid_move(int row, int col);
void make_move(int row, int col);
bool check_win(int row, int col);
bool check_draw();
void switch_player();
void clear_screen();
void display_winner(int winner);
void game_loop();
// --- 函数实现 ---
/**
* @brief 初始化棋盘,所有位置设置为空
*/
void initialize_board() {
// 使用 memset 将整个棋盘数组的所有字节设置为 0 (EMPTY)
memset(board, EMPTY, sizeof(board));
currentPlayer = PLAYER1; // 默认玩家1先手
}
/**
* @brief 清空控制台屏幕
*/
void clear_screen() {
#ifdef _WIN32
system("cls"); // Windows 系统
#else
system("clear"); // Linux/macOS 系统
#endif
}
/**
* @brief 打印当前棋盘状态到控制台
*/
void print_board() {
clear_screen(); // 每次打印前清屏,保持界面整洁
printf("\n 五子棋游戏 - C 语言实现\n");
printf(" -----------------------\n");
printf(" 当前玩家: %s\n\n", piece_chars[currentPlayer]);
// 打印列索引
printf(" "); // 左侧留出空格用于行索引
for (int col = 0; col < BOARD_SIZE; col++) {
printf("%2d ", col); // 每列索引占两位,后跟一个空格
}
printf("\n");
// 打印上边框
printf(" ┌");
for (int col = 0; col < BOARD_SIZE; col++) {
printf("───"); // 每格上方横线
}
printf("┐\n");
// 打印棋盘内容
for (int row = 0; row < BOARD_SIZE; row++) {
printf("%2d │", row); // 打印行索引和左边框
for (int col = 0; col < BOARD_SIZE; col++) {
printf(" %s ", piece_chars[board[row][col]]); // 打印棋子(空、黑或白)
}
printf("│\n"); // 打印右边框
}
// 打印下边框
printf(" └");
for (int col = 0; col < BOARD_SIZE; col++) {
printf("───"); // 每格下方横线
}
printf("┘\n\n");
}
/**
* @brief 检查玩家输入的位置是否有效
* @param row 欲落子的行
* @param col 欲落子的列
* @return 如果位置有效(在棋盘内且为空),返回 true;否则返回 false。
*/
bool is_valid_move(int row, int col) {
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
printf("提示: 落子位置超出棋盘范围!请重新输入。\n");
return false;
}
if (board[row][col] != EMPTY) {
printf("提示: 该位置已有棋子!请选择其他位置。\n\n");
return false;
}
return true;
}
/**
* @brief 在指定位置落子
* @param row 落子行
* @param col 落子列
*/
void make_move(int row, int col) {
board[row][col] = currentPlayer; // 将当前玩家的棋子类型放置到棋盘上
}
/**
* @brief 检查当前落子后,是否有玩家获胜
* @param row 最新落子的行
* @param col 最新落子的列
* @return 如果有玩家获胜,返回 true;否则返回 false。
*/
bool check_win(int row, int col) {
int count;
// 定义8个方向:水平(左右), 垂直(上下), 对角线(左上-右下, 右上-左下)
int dr[] = {-1, -1, -1, 0, 0, 1, 1, 1}; // 行方向偏移量
int dc[] = {-1, 0, 1, -1, 1, -1, 0, 1}; // 列方向偏移量
// 遍历8个方向
for (int i = 0; i < 8; i++) {
count = 1; // 至少包含当前落子
int r, c;
// 向一个方向延伸
r = row + dr[i];
c = col + dc[i];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == currentPlayer) {
count++;
r += dr[i];
c += dc[i];
}
// 向相反方向延伸
r = row - dr[i];
c = col - dc[i];
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == currentPlayer) {
count++;
r -= dr[i];
c -= dc[i];
}
if (count >= 5) { // 如果同一方向上有连续5个或更多棋子,则获胜
return true;
}
}
return false;
}
/**
* @brief 检查棋盘是否已满(平局)
* @return 如果棋盘已满且无人获胜,返回 true;否则返回 false。
*/
bool check_draw() {
for (int r = 0; r < BOARD_SIZE; r++) {
for (int c = 0; c < BOARD_SIZE; c++) {
if (board[r][c] == EMPTY) {
return false; // 还有空位,不是平局
}
}
}
return true; // 棋盘已满
}
/**
* @brief 切换当前玩家
*/
void switch_player() {
currentPlayer = (currentPlayer == PLAYER1) ? PLAYER2 : PLAYER1;
}
/**
* @brief 显示胜利信息
* @param winner 获胜玩家的编号
*/
void display_winner(int winner) {
clear_screen(); // 清屏
print_board(); // 打印最终棋盘
printf("\n===================================\n");
printf(" 恭喜玩家 %s 获胜!\n", piece_chars[winner]);
printf("===================================\n\n");
}
/**
* @brief 游戏主循环
*/
void game_loop() {
int row, col;
bool game_over = false;
initialize_board(); // 初始化游戏
while (!game_over) {
print_board(); // 打印棋盘
printf("玩家 %s,请输入您的落子位置 (行 列,例如: 7 7): ", piece_chars[currentPlayer]);
// 读取玩家输入
// 注意:scanf的返回值应被检查,以确保成功读取了两个整数
if (scanf("%d %d", &row, &col) != 2) {
printf("无效输入!请输入两个整数 (行 列)。\n");
// 清空输入缓冲区,避免无限循环
while (getchar() != '\n');
continue; // 继续下一轮循环,重新请求输入
}
// 检查落子是否有效
if (is_valid_move(row, col)) {
make_move(row, col); // 落子
// 检查是否获胜
if (check_win(row, col)) {
display_winner(currentPlayer);
game_over = true;
}
// 检查是否平局 (只有在没有获胜的情况下才检查平局)
else if (check_draw()) {
clear_screen();
print_board();
printf("\n===================================\n");
printf(" 棋盘已满,平局!\n");
printf("===================================\n\n");
game_over = true;
}
// 如果没有获胜也没有平局,则切换玩家
else {
switch_player();
}
}
}
// 游戏结束,询问是否再玩一局
char play_again_choice;
printf("是否再玩一局?(y/n): ");
scanf(" %c", &play_again_choice); // 注意前面的空格,用于跳过上一个scanf留下的换行符
if (play_again_choice == 'y' || play_again_choice == 'Y') {
game_loop(); // 重新开始游戏
} else {
printf("感谢您的游玩!再见。\n");
}
}
/**
* @brief 主函数,程序入口
*/
int main() {
game_loop(); // 调用游戏主循环
return 0; // 程序正常退出
}