欢迎光临
我们一直在努力

C语言小游戏项目:三子棋和五子棋的基本实现逻辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

游戏项目第二篇|用 C 语言实现三子棋与五子棋,练透棋盘程序设计

上一篇扫雷项目里,我们练习了二维数组、随机数和多文件函数设计。这一篇继续用棋盘做游戏,但把重点放在另一个问题上。

玩家每走一步,程序应当怎样检查操作、更新状态并判断胜负?

三子棋和五子棋很适合回答这个问题。它们共用同一套程序结构,主要差别只有棋盘大小和获胜所需的连续棋子数。我们甚至可以只写一份代码,通过修改三个常量切换游戏。

一、先把两个游戏的共同规则找出来

两个游戏都需要这些功能。

  • 初始化一张空棋盘
  • 打印棋盘和坐标
  • 两名玩家交替落子
  • 拒绝越界坐标和重复落子
  • 每次有效落子后检查胜负
  • 棋盘已满且无人获胜时判为平局

可以用下面的数据表示游戏状态。

数据作用
board[ROW][COL] 保存每个位置的棋子
player 保存当前玩家,取值为 X 或 O
moves 保存有效落子次数
ROW、COL 决定棋盘大小
WIN_LEN 决定获胜所需的连续棋子数

空格用字符 ' ' 表示。玩家输入从 1 开始的坐标,数组下标从 0 开始,因此写入数组前必须减一。

三子棋使用 3 × 3 棋盘,WIN_LEN 为 3。五子棋使用 15 × 15 棋盘,WIN_LEN 为 5。

当两个项目拥有相同的数据和动作时,应当优先寻找可复用的实现,避免复制两份代码后分别修补。

二、函数按照游戏动作拆分

函数职责
InitBoard 把棋盘全部置为空格
DisplayBoard 打印当前棋盘
PlacePiece 检查坐标并完成一次落子
CountDirection 统计某个方向上的连续棋子数
IsWin 检查四个方向是否达到获胜长度
PlayGame 组织落子、胜负判断和玩家轮换

这里有一个重要的设计决定。

PlacePiece 只负责检查并落子,不打印提示,也不切换玩家。IsWin 只读取棋盘并返回结果,不修改游戏状态。PlayGame 负责把这些函数按规则串起来。

函数职责越清楚,主流程就越接近自然语言描述的游戏规则。

三、胜负判断只围绕最后一步展开

新手常写的方案是每走一步就扫描整个棋盘。三子棋只有 9 个格子,这样写没有问题。换成五子棋后,仍可以利用一个更直接的规律。

新形成的连线一定经过刚刚落下的棋子。

因此,只需以最后落子位置为中心,检查四个方向。

  • 水平方向
  • 垂直方向
  • 主对角线方向
  • 副对角线方向
  • 每个方向都要向两端统计。例如水平方向需要同时检查左边和右边,再加上当前棋子本身。

    连续数量 = 1 + 正方向数量 + 反方向数量;

    当某个方向的连续数量大于或等于 WIN_LEN,当前玩家获胜。这个函数既能处理三子棋,也能处理五子棋,无需为两个游戏分别写判断逻辑。

    四、完整代码

    下面的程序默认运行三子棋。两名玩家共用键盘,依次输入行号和列号。

    #include <stdio.h>
    #include <stdbool.h>

    #define ROW 3
    #define COL 3
    #define WIN_LEN 3

    // 初始化棋盘
    static void InitBoard(char board[ROW][COL])
    {
    for (int i = 0; i < ROW; i++)
    for (int j = 0; j < COL; j++)
    board[i][j] = ' ';
    }

    // 打印棋盘,坐标从 1 开始显示
    static void DisplayBoard(const char board[ROW][COL])
    {
    printf("\\n ");
    for (int j = 1; j <= COL; j++)
    printf("%2d ", j);
    printf("\\n");

    for (int i = 0; i < ROW; i++)
    {
    printf("%2d ", i + 1);
    for (int j = 0; j < COL; j++)
    printf(" %c ", board[i][j]);
    printf("\\n");
    }
    printf("\\n");
    }

    // 合法落子返回 true,失败时不修改棋盘
    static bool PlacePiece(char board[ROW][COL], int row, int col, char player)
    {
    if (row < 1 || row > ROW || col < 1 || col > COL)
    return false;

    int x = row – 1; // 玩家坐标转换为数组下标
    int y = col – 1;

    if (board[x][y] != ' ')
    return false; // 已经有棋子的位置不能重复落子

    board[x][y] = player;
    return true;
    }

    // 从当前棋子的相邻位置开始,统计单一方向上的连续棋子
    static int CountDirection(const char board[ROW][COL], int x, int y,
    int dx, int dy, char player)
    {
    int count = 0;
    x += dx;
    y += dy;

    // 同时检查数组边界和棋子是否属于当前玩家
    while (x >= 0 && x < ROW && y >= 0 && y < COL &&
    board[x][y] == player)
    {
    count++;
    x += dx;
    y += dy;
    }

    return count;
    }

    // x、y 是最后落子的数组下标
    static bool IsWin(const char board[ROW][COL], int x, int y, char player)
    {
    const int directions[4][2] = {
    {0, 1}, // 水平
    {1, 0}, // 垂直
    {1, 1}, // 主对角线
    {1, –1} // 副对角线
    };

    for (int i = 0; i < 4; i++)
    {
    int dx = directions[i][0];
    int dy = directions[i][1];

    int count = 1; // 当前棋子先算一个
    count += CountDirection(board, x, y, dx, dy, player);
    count += CountDirection(board, x, y, –dx, –dy, player);

    if (count >= WIN_LEN)
    return true;
    }

    return false;
    }

    // 组织一局游戏
    static void PlayGame(void)
    {
    char board[ROW][COL];
    char player = 'X';
    int moves = 0;

    InitBoard(board);

    while (true)
    {
    DisplayBoard(board);
    printf("玩家 %c,请输入行和列,例如 1 2\\n", player);

    int row = 0;
    int col = 0;

    if (scanf("%d %d", &row, &col) != 2)
    {
    printf("输入无效,请输入两个整数\\n");

    // 清理本行残留输入,避免错误内容被反复读取
    while (getchar() != '\\n')
    ;
    continue;
    }

    if (!PlacePiece(board, row, col, player))
    {
    printf("坐标越界或位置已有棋子,请重新输入\\n");
    continue;
    }

    moves++; // 只有有效落子才增加次数

    if (IsWin(board, row – 1, col – 1, player))
    {
    DisplayBoard(board);
    printf("玩家 %c 获胜\\n", player);
    return;
    }

    if (moves == ROW * COL)
    {
    DisplayBoard(board);
    printf("棋盘已满,双方平局\\n");
    return;
    }

    // 当前玩家没有获胜,也没有平局,交换执子方
    player = (player == 'X') ? 'O' : 'X';
    }
    }

    int main(void)
    {
    int choice = 0;

    do
    {
    printf("1. 开始游戏\\n0. 退出游戏\\n请选择\\n");

    if (scanf("%d", &choice) != 1)
    {
    while (getchar() != '\\n')
    ;
    choice = –1;
    }

    if (choice == 1)
    PlayGame();
    else if (choice != 0)
    printf("选择错误,请重新输入\\n");
    } while (choice != 0);

    return 0;
    }

    使用 GCC 编译和运行。

    gcc board_game.c -std=c11 -Wall -o board_game
    ./board_game

    使用 Visual Studio 时,将代码加入一个空项目即可。MSVC 可能对 scanf 给出安全提醒,学习阶段可按课程环境选择 GCC、Clang 或相应的安全输入函数。

    五、改成五子棋只需三行

    找到文件开头的常量定义,改成下面的内容。

    #define ROW 15
    #define COL 15
    #define WIN_LEN 5

    其余代码保持不变。棋盘打印、坐标检查、落子、轮换玩家、四方向统计和平局判断都能继续工作。

    这就是参数化设计带来的收益。把会变化的规则提取成常量,把不变的处理过程留在函数里。 如果以后想做一个 10×10 的四连棋,也只需重新调整配置。

    需要说明的是,这里的五子棋采用基础规则,连续五颗或更多棋子都算获胜,暂未加入禁手、交换规则等正式竞赛内容。学习项目可以逐步增加规则,没必要第一版就把整本规则书塞进 main。

    六、这组测试比正常玩一局更重要

    代码写完以后,建议主动构造下面的测试。

    测试内容要确认的结果
    输入 0 1 或超出棋盘的坐标 拒绝落子,玩家不切换
    两次输入同一坐标 第二次被拒绝,moves 不增加
    输入字母 提示输入错误,程序不会反复刷屏
    横向、纵向连成目标长度 正确获胜
    两条对角线连成目标长度 正确获胜
    连线位于棋盘边缘 正确判断且没有数组越界
    棋盘填满且无人获胜 正确判为平局
    五子棋出现六连 按本文基础规则判为获胜

    还可以故意把数组下标的减一删掉,观察程序发生什么,再恢复代码。这样比单纯记住结论更容易理解坐标转换和边界检查。

    测试的目标是让规则在各种输入下都成立,而不只是证明自己精心挑选的一步棋能运行。

    七、下一步可以怎样扩展

    完成基础版本后,可以选择一个方向继续练习。

  • 加入电脑玩家。 第一版让电脑随机选择空位,第二版再实现获胜位置检查和防守位置检查。
  • 增加悔棋。 每次有效落子时保存坐标,悔棋时恢复最近一步并切换玩家。
  • 拆分多个文件。 将棋盘函数放入 board.c,将游戏流程放入 game.c,把声明集中到对应头文件。
  • 记录棋谱。 把每一步的玩家和坐标写入文件,重新启动程序后可以回放。
  • 改造界面。 保留棋盘和规则函数,只替换输入输出部分,逐步接入图形库。
  • 其中最值得先做的是拆分文件和加入简单电脑玩家。前者训练工程组织,后者训练把策略拆成明确步骤。

    写在最后

    三子棋和五子棋的代码不算长,却包含了游戏程序反复出现的几个动作。读取输入、验证操作、修改状态、检查结果、切换回合,再继续下一轮。

    如果你能脱离这份代码,独立画出程序流程并重新实现一次,就已经在完成一次真正的程序设计训练。遇到问题时,先把故障缩小到一次落子或一个方向,逐步打印坐标和计数。编译器可能偶尔让人头疼,但它至少不会偷偷挪动你的棋子,耐心查下去,问题通常比想象中具体。在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » C语言小游戏项目:三子棋和五子棋的基本实现逻辑
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址