【C语言】扫雷游戏

摘要:扫雷把二维数组、函数封装和多文件编程放进一个完整项目。本文从 9×9 棋盘和 10 个雷的需求出发,先定两个字符棋盘和外扩一圈的哨兵边界,再拆出初始化、打印、布雷、统计、排查五个函数,最后处理重复排查、非法输入和胜利判定,并给出展开一片的递归扩展。

文章目录

  • [1. 需求先变成棋盘](#1. 需求先变成棋盘)
  • [2. 三个文件各放什么](#2. 三个文件各放什么)
  • [3. 初始化与打印](#3. 初始化与打印)
  • [4. 布雷与统计](#4. 布雷与统计)
  • [5. 排查与胜负](#5. 排查与胜负)
  • [6. 容易漏掉的几处](#6. 容易漏掉的几处)
  • [7. 扩展:展开一片](#7. 扩展:展开一片)
  • [8. 小结](#8. 小结)

1. 需求先变成棋盘

控制台版扫雷(Minesweeper)要满足这些条件:

需求 具体行为
菜单 输入 1 开始一局,输入 0 退出
棋盘 9×9 格,随机放 10 个雷
排查 输入行、列;不是雷就显示周围雷数,是雷就结束本局
胜利 把 10 个雷之外的非雷格全部找出来

需求里最先要定的是数据怎么存。棋盘是二维的,每个格子要放"有没有雷"和"周围有几颗雷"两类信息。最直接的想法是一个 9×9 的二维数组,雷存 1,无雷存 0。

这个方案有两个问题。第一,统计边角格子的周围雷数时会越界:排查 (9,9) 要读 (10,10),数组没有这一格。第二,雷的信息和排查结果混在同一个数组里,打印给玩家看时还要区分"这是雷"还是"这是数字"。

对应的处理是:数组外扩一圈,做成 11×11;再用两个数组,一个存雷,一个存给玩家看的视图。

要解决的问题 方案
雷和排查结果存在哪里 两个 11×11 的字符数组
统计周围雷数时越界 外圈留一圈哨兵,不布雷
雷的信息和视图互相混淆 mine 存雷,show 存排查结果
两套棋盘复用同一套函数 都用 char,用 '0'、'1'、'*' 和数字字符
代码怎么组织 game.h 声明,game.c 实现,test.c 测试

mine 初始全为 '0',布雷的位置改成 '1';show 初始全为 '*',玩家排查后改成雷数对应的字符。两个数组的合法下标都是 0 到 10,真正参与游戏的是 1 到 9,外圈只当挡板。

c 复制代码
#define ROW 9
#define COL 9
#define ROWS (ROW + 2)
#define COLS (COL + 2)

char mine[ROWS][COLS];
char show[ROWS][COLS];

外圈不布雷,值一直是 '0'。统计 (1,1) 这种角落时,周围 8 格里有 5 格落在外圈,读到的都是 0,不需要为边界单独写 if。这类专门留出来防越界的格子叫哨兵(sentinel)。

坐标按"行 列"输入,2 5 表示第 2 行第 5 列,访问 mine[2][5]。数组的 x 是行号、y 是列号,和数学里横纵坐标的习惯相反,输入提示要写清楚。

2. 三个文件各放什么

项目拆成三个文件:

文件 职责 包含关系
game.h 宏、函数声明 被 game.c、test.c 包含
game.c 各功能函数的定义 包含 game.h
test.c main、菜单、一局游戏的流程 包含 game.h

game.h 的内容:

c 复制代码
#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define EASY_COUNT 10

#define ROW 9
#define COL 9

#define ROWS (ROW + 2)
#define COLS (COL + 2)

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
void DisplayBoard(char board[ROWS][COLS], int row, int col);
void SetMine(char board[ROWS][COLS], int row, int col);
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

宏体要加括号。#define ROWS ROW + 2 单独当实参没问题,一旦参与运算就会出错:ROWS * COLS 会展开成 ROW + 2 * COL + 2,得到 29 而不是 121。写成 (ROW + 2) 才安全。

在 VS2026 里新建空项目,把 game.h、game.c、test.c 都添加到项目,配置选 Debug,按 Ctrl + F5 运行。三个文件一起编译链接,头文件不单独编译,只被两个 .c 包含。

3. 初始化与打印

初始化要把整个 11×11 填满,不能只填中间的 9×9:

c 复制代码
// game.c
#include "game.h"

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            board[i][j] = set;
}

InitBoard(mine, ROWS, COLS, '0') 和 InitBoard(show, ROWS, COLS, '*') 都传 11。如果只初始化 9×9,外圈就是未初始化的值,统计边角雷数时会把它们加进去,结果直接错乱。

下面这些函数都写在 game.c 里。打印只显示 1 到 9 的内区,外圈不显示:

c 复制代码
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
    printf("   ");
    for (int j = 1; j <= col; j++)
        printf("%2d", j);
    printf("\n");

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

初始 show 棋盘输出:

text 复制代码
    1 2 3 4 5 6 7 8 9
 1  * * * * * * * * *
 2  * * * * * * * * *
 3  * * * * * * * * *
 4  * * * * * * * * *
 5  * * * * * * * * *
 6  * * * * * * * * *
 7  * * * * * * * * *
 8  * * * * * * * * *
 9  * * * * * * * * *

表头先打印三个空格,再按 %2d 打列号;行号也用 %2d ,列号和格子才能对齐。棋盘扩到 16×16 时,一位数和两位数宽度不同,统一用 %2d 就不会错位。

4. 布雷与统计

布雷在 1 到 9 的范围内随机取坐标,位置已被占用就重试:

c 复制代码
void SetMine(char board[ROWS][COLS], int row, int col)
{
    int count = EASY_COUNT;
    while (count > 0)
    {
        int x = rand() % row + 1;
        int y = rand() % col + 1;

        if (board[x][y] == '0')
        {
            board[x][y] = '1';
            count--;
        }
    }
}

rand() % row 得到 0 到 8,加 1 就是 1 到 9,外圈永远不会被选中。count 只在成功放下一颗雷时减 1,所以不会重复布雷,也不会少放。

随机种子在 main 里设一次:

c 复制代码
srand((unsigned int)time(NULL));

rand 默认种子是 1,不调用 srand 每次运行布局都一样。time(NULL) 精确到秒,如果把 srand 放进每局游戏,同一秒内连开两局会拿到同一个种子,两局布局完全相同。

统计周围 8 格的雷数:

c 复制代码
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
    return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1]
         + mine[x][y - 1]     +                   mine[x][y + 1]
         + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1]
         - 8 * '0';
}

8 个格子里存的是字符 '0' 或 '1'。字符相加得到的是编码值之和,减去 8 * '0' 才得到数字形式的雷数。返回 0 到 8 的整数。

GetMineCount 前加 static,表示只在 game.c 内部使用,其他文件看不到。它依赖外圈全是 '0',所以角落和中间可以共用同一份代码。

5. 排查与胜负

排查是游戏的主循环:

c 复制代码
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int x = 0;
    int y = 0;
    int win = 0;
    int c = 0;

    while (win < row * col - EASY_COUNT)
    {
        printf("请输入要排查的坐标:>");
        if (scanf("%d %d", &x, &y) != 2)
        {
            if (feof(stdin))
            {
                printf("\n输入结束,退出本局\n");
                return;
            }
            while ((c = getchar()) != '\n' && c != EOF)
                ;
            printf("输入无效,请重新输入\n");
            continue;
        }

        if (x < 1 || x > row || y < 1 || y > col)
        {
            printf("坐标非法,重新输入\n");
            continue;
        }

        if (mine[x][y] == '1')
        {
            printf("很遗憾,你被炸死了\n");
            DisplayBoard(mine, row, col);
            return;
        }

        if (show[x][y] != '*')
        {
            printf("该坐标已排查,请重新输入\n");
            continue;
        }

        show[x][y] = (char)(GetMineCount(mine, x, y) + '0');
        DisplayBoard(show, row, col);
        win++;
    }

    printf("恭喜你,排雷成功\n");
    DisplayBoard(mine, row, col);
}

循环条件是 win < row * col - EASY_COUNT,9×9 棋盘、10 个雷,就是 71 个非雷格。win 只在成功揭开一个新格子时加 1:

  • 踩到 '1',打印 mine 棋盘,return 结束本局。
  • 坐标越界,提示后 continue,不计数。
  • show[x][y] 不是 '*',说明之前已经排查过,提示后 continue,不重复计数。
  • 合法且未排查,算出雷数,写入 show,打印,win++。

win 达到 71 时循环结束,打印胜利信息和 mine 棋盘。

输入处理分两层。scanf 返回值不是 2,说明没读到两个整数:如果已经到文件末尾,直接退出本局;否则用 getchar 把这一行剩下的字符读掉,再提示重新输入。只检查坐标范围、不检查 scanf 返回值,输入字母时 x、y 会保持旧值,循环可能一直转。

scanf 在 VS2026 会报 C4996。在 game.h 第一行、任何 #include 之前加 #define _CRT_SECURE_NO_WARNINGS 1,后文代码省略这一行。

test.c 负责菜单和一局游戏的流程:

c 复制代码
#include "game.h"

static void menu(void)
{
    printf("***********************\n");
    printf("*****   1. play   *****\n");
    printf("*****   0. exit   *****\n");
    printf("***********************\n");
}

static void game(void)
{
    char mine[ROWS][COLS];
    char show[ROWS][COLS];

    InitBoard(mine, ROWS, COLS, '0');
    InitBoard(show, ROWS, COLS, '*');

    SetMine(mine, ROW, COL);
    DisplayBoard(show, ROW, COL);
    FindMine(mine, show, ROW, COL);
}

int main(void)
{
    int input = 0;
    int c = 0;

    srand((unsigned int)time(NULL));

    do
    {
        menu();
        printf("请选择:>");
        if (scanf("%d", &input) != 1)
        {
            if (feof(stdin))
                break;
            while ((c = getchar()) != '\n' && c != EOF)
                ;
            printf("输入无效,请重新选择\n");
            input = -1;
            continue;
        }

        switch (input)
        {
        case 1:
            game();
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default:
            printf("选择错误,重新选择\n");
            break;
        }
    } while (input);

    return 0;
}

把随机种子固定成 1,依次输入 1、7 5、8 5、0,输出节选:

text 复制代码
    1 2 3 4 5 6 7 8 9
 1  * * * * * * * * *
 ...
 9  * * * * * * * * *
请输入要排查的坐标:>    1 2 3 4 5 6 7 8 9
 1  * * * * * * * * *
 ...
 7  * * * * 2 * * * *
 ...
请输入要排查的坐标:>很遗憾,你被炸死了
    1 2 3 4 5 6 7 8 9
 1  0 0 0 0 0 0 1 0 0
 2  0 0 0 0 0 0 0 0 0
 3  0 0 0 0 0 0 0 0 0
 4  1 0 0 0 0 0 0 0 0
 5  0 0 0 0 0 0 0 0 0
 6  0 0 0 0 0 0 0 0 1
 7  0 0 0 0 0 0 0 0 0
 8  0 0 1 0 1 1 1 0 1
 9  0 1 1 0 0 0 0 0 0

7 5 周围有 2 颗雷,显示 2;8 5 本身是雷,踩中后打印 mine 棋盘。mine 里的 1 是雷,0 是安全格。

6. 容易漏掉的几处

问题 现象 处理
重复排查 同一个坐标反复输入,胜利进度虚增 写入前判断 show[x][y] != '*'
scanf 失败 输入字母后旧值不变,反复提示或死循环 检查返回值,清掉本行再继续
宏没有括号 ROWS * COLS 算成 29 宏体写成 (ROW + 2)、(COL + 2)
表头宽度 棋盘大于 9 后行列号错位 统一用 %2d
菜单输入失败 输入字母后直接走 case 0 退出 菜单也检查 scanf 返回值

这些问题的共同点是"输入和边界没有单独处理"。写游戏时先把输入校验、坐标范围、重复操作三件事列出来,再写主循环,能少走很多弯路。

7. 扩展:展开一片

现在的版本一次只揭开一个格子。扫雷里常见的效果是:如果揭开的格子周围没有雷,就把周围一片一起展开。这正好是递归:

c 复制代码
static int Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
    if (x < 1 || x > ROW || y < 1 || y > COL)
        return 0;
    if (mine[x][y] == '1')
        return 0;
    if (show[x][y] != '*')
        return 0;

    int count = GetMineCount(mine, x, y);
    show[x][y] = (char)(count + '0');
    int opened = 1;

    if (count == 0)
    {
        for (int i = x - 1; i <= x + 1; i++)
            for (int j = y - 1; j <= y + 1; j++)
                opened += Expand(mine, show, i, j);
    }
    return opened;
}

三个终止条件缺一不可:越出 1 到 9 的内区、踩到雷、格子已经揭开。最后一个条件防止相邻格来回递归。只有当前格雷数为 0 才继续展开 8 个邻居,否则只揭开当前格。

这段按固定 9×9 写;做难度选择时,把 ROW、COL 换成传入的 row、col。

在 FindMine 里把 win++ 换成:

c 复制代码
win += Expand(mine, show, x, y);

Expand 返回本次新揭开的格子数,胜利判定式不用改。递归的细节在《函数递归》一篇展开。

其他扩展方向:难度选择(改 ROW、COL、EASY_COUNT)、标记雷、计时、第一次点击不炸。改难度时注意表头宽度和雷数上限,16×16 棋盘放 40 个雷,30×16 棋盘放 99 个雷。

8. 小结

  • 扫雷用两个字符棋盘:mine 存雷,show 存玩家视图;数组外扩一圈,边角统计不用特判。
  • 字符 '0'、'1' 相加后减 8 * '0' 得到雷数,显示时再 + '0' 转回字符。
  • SetMine 用 rand() % row + 1 取 1 到 9,位置重复就重试;srand 在 main 里只调一次。
  • 排查主循环要处理坐标范围、重复排查、scanf 返回值和 EOF,胜利条件是揭开全部非雷格。
  • 宏体加括号,ROWS * COLS 才不会展开错;表头用 %2d 适配两位数。
  • 展开一片用递归:越界、踩雷、已揭开都返回,雷数为 0 才继续展开 8 个邻居。
  • 项目拆成 game.h、game.c、test.c,声明和实现分开,三个文件一起编译链接。
相关推荐
丘山录1 小时前
本地部署 LongCat-2.5,要 7 台机器我只有 4 台
人工智能·算法
Allen_LVyingbo1 小时前
NVIDIA CUDA Tile 在医疗计算中的算子形式化、算法重构与内核实现
大数据·python·算法·重构·知识图谱·健康医疗·数据库架构
冻柠檬飞冰走茶3 小时前
PTA刷题集之Airline Routes(C语言实现)
c语言·开发语言·数据结构·算法
宗旨飞翔3 小时前
不同的二叉搜索树
算法
736c3 小时前
固件开发岗算法
数据结构·算法
青少儿编程课堂3 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:倍增法求最近公共祖先LCA解析
c++·python·算法·bfs·信息学竞赛
冻柠檬飞冰走茶3 小时前
PTA刷题集之Battle Over Cities - Hard Version(C语言实现)
c语言·开发语言·数据结构·算法
吴长建先生重名了3 小时前
电商订单系统在Docker和Kubernetes中部署的高
算法
郝学胜-神的一滴4 小时前
AI 编程智能体 22:解析器赋能与实战落地全指南
人工智能·程序人生·游戏·unity·游戏引擎