详解 C 语言手撸一个五子棋:带你彻底搞懂游戏逻辑与实现细节

五子棋,作为一种深受喜爱的棋类游戏,其规则简单而富有策略性。对于 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;
}

  • 边界检查: 确保输入的 rowcol 都在 [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; // 没有连成五子
}

  • 方向数组: drdc 数组巧妙地定义了 8 个方向的单位步长。例如,dr[0]=-1, dc[0]=-1 表示左上方向。

  • 双向计数: 对于每个方向,我们从最新落子点开始,向该方向和其相反方向分别延伸计数。例如,检查水平方向时,会向左计数和向右计数,然后将两者之和加上当前棋子本身,得到总的连续棋子数。

  • 边界检查: 在延伸计数时,始终检查 rc 是否在棋盘范围内,避免越界访问。

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++ 扩展。

  1. 安装 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

  2. 安装 VS Code C/C++ 扩展:

    • 打开 VS Code,前往 Extensions 视图 (Ctrl+Shift+X)。

    • 搜索 "C/C++" 并安装由 Microsoft 提供的扩展。

  3. 创建 C 文件:

    • 在 VS Code 中创建一个新文件,例如 gomoku.c

    • 将上面提供的完整 C 语言代码复制粘贴到 gomoku.c 文件中。

  4. 编译与运行:

    • 通过终端运行 (推荐简单项目):

      • 打开 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. 进一步的优化与扩展

当前的五子棋游戏已经功能完整,但仍有许多可以改进和扩展的地方:

  1. 更友好的用户界面:

    • 可以使用一些简单的图形库(如 ncurses 或 PDCurses)来创建伪图形界面,实现更平滑的棋盘更新,避免频繁清屏带来的闪烁。

    • 实现光标移动来选择落子位置,而不是手动输入坐标。

  2. 人工智能 (AI) 对手:

    • 实现一个简单的 AI 玩家,例如基于随机选择空位落子,或者更复杂的算法,如 Minimax 或 Alpha-Beta 剪枝。

  3. 悔棋功能:

    • 使用栈或链表记录每一步棋的历史,实现悔棋功能。

  4. 保存/加载游戏进度:

    • 将当前棋盘状态和游戏信息保存到文件,以便下次继续游戏。

  5. 规则增强:

    • 实现禁手规则(如三三禁手、四四禁手、长连禁手),使游戏更符合专业五子棋规则。

  6. 错误处理健壮性:

    • 更细致的输入验证,例如处理非数字输入。

    • 使用 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;    // 程序正常退出
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值