从零开始实现扫雷游戏(C 语言控制台版)

扫雷是很多人最早接触的电脑游戏之一。它规则简单,但非常适合练习 C 语言的二维数组、函数封装、多文件编程、随机数、字符处理等知识。

这篇文章会从扫雷的规则和设计思路讲起,再一步一步给出完整代码实现。最终实现一个 9×9 棋盘、默认 10 个雷的控制台扫雷游戏。

一、游戏功能说明

我们要实现的控制台扫雷包含以下功能:

  1. 使用控制台运行经典扫雷游戏;
  2. 游戏菜单可以开始游戏或退出;
  3. 棋盘为 9×9;
  4. 默认随机布置 10 个雷;
  5. 玩家输入坐标排查雷:
    • 如果该位置是雷,游戏结束;
    • 如果该位置不是雷,显示周围 8 个格子中有几个雷;
  6. 当所有非雷位置都被排查出来,游戏胜利;
  7. 可以重复游玩。

二、整体设计思路

1. 多文件编程

为了让代码结构清晰,我们把程序拆成三个文件:

文件 作用
test.c 游戏整体逻辑、菜单循环、main 函数
game.c 游戏具体函数的实现
game.h 宏定义和函数声明

这样做的好处是:main 函数只负责整体流程,具体功能封装成函数,代码更容易阅读和维护。

2. 为什么需要两个棋盘?

扫雷中有一个很关键的问题:

如果某个位置不是雷,我们要显示它周围有几个雷。但是"雷"本身也用数字表示时,容易和"周围雷数"冲突。

例如:

  • 用 1 表示雷;
  • 排查后发现周围有 1 个雷,也要显示 1;
  • 这样就分不清这个 1 到底是雷,还是提示数字。

因此我们使用两个二维数组:

  • mine 数组:存放雷的布置信息;
  • show 数组:存放展示给玩家的信息。

为了统一处理,两个数组都使用字符数组:

  • mine 数组初始化为 '0';
  • 布置雷时把对应位置改成 '1';
  • show 数组初始化为 '*',表示未排查;
  • 排查后把周围雷数转换成字符填入 show。

3. 为什么棋盘要变成 11×11?

排查一个坐标周围有几个雷时,需要访问它周围 8 个格子:

text

复制代码
左上  上  右上
左    当前 右
左下  下  右下

如果玩家排查的是边上的格子,直接访问周围 8 个格子就可能越界。

解决方法很简单:

在 9×9 棋盘外面再加一圈"边界",实际使用 11×11 的数组。

真正给玩家看的还是中间的 9×9 区域,外面一圈只用于防止越界访问 宏定义如下:

c 复制代码
#define row 9
#define line 9

#define rows (row + 2)
#define lines (line + 2)

4. 字符与数字的转换

mine 数组中存放的是字符 '0' 和 '1'。

统计周围雷数时,如果把 8 个字符相加,得到的是 ASCII 码值之和。例如:

c 复制代码
'1' + '0' + '1'

并不是数字 2。

字符转数字的方法是:

c 复制代码
'0' - '0' = 0
'1' - '0' = 1
'2' - '0' = 2

所以统计 8 个格子时,可以先全部相加,再减去 8 * '0':

c 复制代码
arr[r - 1][l - 1] + arr[r - 1][l] + ... - 8 * '0'

得到数字后,如果要存回字符数组,再加回 '0':

c 复制代码
arr2[i][j] = count + '0';

三、代码实现

1. game.h

c 复制代码
#pragma once

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

#define mine_count 10

#define row 9
#define line 9

#define rows (row + 2)
#define lines (line + 2)

// 菜单
void menu(void);

// 初始化棋盘
void set_board(char arr[rows][lines], int x, int y, char z);

// 展示棋盘
void show_board(char arr[rows][lines], int x, int y);

// 布置雷
void set_mine(char arr[rows][lines], int x, int y);

// 统计周围雷数
int add_mine(char arr[rows][lines], int r, int l);

// 排查雷
void find_mine(char arr1[rows][lines], int x, int y, char arr2[rows][lines]);

注意:#define 定义宏时,末尾不要加分号。例如:

c 复制代码
#define mine_count 10

不要写成:

c 复制代码
#define mine_count 10;

否则宏展开时可能带来隐藏问题。


2. game.c

c 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include "game.h"

void menu(void)
{
    printf("-------扫雷游戏-------\n");
    printf("--------1.play--------\n");
    printf("--------0.exit--------\n");
    printf("----------------------\n");
    printf("输入1开始游戏,输入0退出\n");
    printf("请选择:");
}

void set_board(char arr[rows][lines], int x, int y, char z)
{
    int i = 0;
    for (; i < x; i++)
    {
        int j = 0;
        for (; j < y; j++)
        {
            arr[i][j] = z;
        }
    }
}

void show_board(char arr[rows][lines], int x, int y)
{
    int i = 0;
    int j = 0;

    // 打印列号
    printf("  ");
    for (j = 1; j <= y - 2; j++)
    {
        printf("%d ", j);
    }
    printf("\n");

    // 打印行号和棋盘
    for (i = 1; i <= x - 2; i++)
    {
        printf("%d ", i);
        for (j = 1; j <= y - 2; j++)
        {
            printf("%c ", arr[i][j]);
        }
        printf("\n");
    }
}

void set_mine(char arr[rows][lines], int x, int y)
{
    int count = mine_count;

    while (count != 0)
    {
        int r = rand() % (x - 2) + 1;
        int l = rand() % (y - 2) + 1;

        if (arr[r][l] == '0')
        {
            arr[r][l] = '1';
            count--;
        }
    }
}

int add_mine(char arr[rows][lines], int r, int l)
{
    return arr[r - 1][l - 1] + arr[r - 1][l]
        + arr[r - 1][l + 1] + arr[r][l - 1]
        + arr[r][l + 1] + arr[r + 1][l - 1]
        + arr[r + 1][l] + arr[r + 1][l + 1]
        - 8 * '0';
}

void find_mine(char arr1[rows][lines], int x, int y, char arr2[rows][lines])
{
    int i = 0;
    int j = 0;
    int remain = (x - 2) * (y - 2) - mine_count;

    while (remain != 0)
    {
        show_board(arr2, x, y);
        printf("请输入要排查的坐标(行 列):");
        scanf("%d%d", &i, &j);

        if (i >= 1 && i <= x - 2 && j >= 1 && j <= y - 2)
        {
            if (arr2[i][j] == '*')
            {
                if (arr1[i][j] == '1')
                {
                    printf("很遗憾,你踩到雷了,游戏结束!\n");
                    show_board(arr1, x, y);
                    break;
                }
                else
                {
                    int c = add_mine(arr1, i, j);
                    arr2[i][j] = c + '0';
                    remain--;
                }
            }
            else
            {
                printf("该坐标已经排查过,请重新输入\n");
            }
        }
        else
        {
            printf("坐标输入错误,请输入 1~9 之间的坐标\n");
        }
    }

    if (remain == 0)
    {
        printf("恭喜你,排雷成功!\n");
    }
}

3. test.c

c 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include "game.h"

void game(void)
{
    char mine[rows][lines];
    char show[rows][lines];

    // 初始化棋盘
    set_board(mine, rows, lines, '0');
    set_board(show, rows, lines, '*');

    // 布置雷
    set_mine(mine, rows, lines);

    // 排查雷
    find_mine(mine, rows, lines, show);
}

int main(void)
{
    int input = 0;

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

    do
    {
        menu();
        scanf("%d", &input);

        switch (input)
        {
        case 1:
            game();
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default:
            printf("输入错误,请输入 0 或 1\n");
        }
    } while (input);

    return 0;
}

四、核心函数讲解

1. set_board:初始化棋盘

c 复制代码
set_board(mine, rows, lines, '0');
set_board(show, rows, lines, '*');

mine 数组全部初始化为 '0',表示暂时没有雷。

show 数组全部初始化为 '*',表示玩家还没有排查。


2. set_mine:随机布雷

c 复制代码
int r = rand() % (x - 2) + 1;
int l = rand() % (y - 2) + 1;

因为数组是 11×11,真正棋盘范围是 19,所以随机坐标范围是 19。

如果该位置已经是 '1',就跳过,避免重复布雷。


3. show_board:展示棋盘

只打印中间的 9×9 区域,并在四周打印行号和列号,方便玩家输入坐标。


4. add_mine:统计周围雷数

c 复制代码
return arr[r - 1][l - 1] + arr[r - 1][l]
    + arr[r - 1][l + 1] + arr[r][l - 1]
    + arr[r][l + 1] + arr[r + 1][l - 1]
    + arr[r + 1][l] + arr[r + 1][l + 1]
    - 8 * '0';

它把周围 8 个字符相加,再减去 8 * '0',得到数字形式的雷数。


5. find_mine:排查雷

排查逻辑:

  1. 展示当前 show 棋盘;
  2. 输入坐标;
  3. 判断坐标是否合法;
  4. 判断该位置是否已经排查过;
  5. 如果踩雷,游戏结束,并展示 mine 棋盘;
  6. 如果不是雷,统计周围雷数,写入 show 数组;
  7. 每成功排查一个非雷位置,remain--;
  8. 当 remain == 0,说明所有非雷位置都排查完,游戏胜利。

五、运行效果示意

菜单:

markdown 复制代码
-------扫雷游戏-------
--------1.play--------
--------0.exit--------
----------------------
输入1开始游戏,输入0退出
请选择:

游戏棋盘:

text

复制代码
  1 2 3 4 5 6 7 8 9 
1 * * * * * * * * * 
2 * * * * * * * * * 
3 * * * * * * * * * 
4 * * * * * * * * * 
5 * * * * * * * * * 
6 * * * * * * * * * 
7 * * * * * * * * * 
8 * * * * * * * * * 
9 * * * * * * * * * 
请输入要排查的坐标(行 列):

七、可以继续扩展的功能

这个版本是基础版扫雷,后续可以继续扩展:

  1. 标记功能:输入坐标后选择"排查"或"插旗";
  2. 自动展开:如果周围雷数为 0,自动展开周围区域;
  3. 难度选择:初级 9×9、中级 16×16、高级 16×30;
  4. 计时功能;
  5. 显示剩余雷数;
  6. 防止第一次踩雷:第一次排查的位置一定不是雷。

总结

通过这个扫雷小游戏,我们练习了:

  • 二维数组的使用;
  • 多文件编程;
  • 函数封装;
  • 随机数生成;
  • 字符与数字的 ASCII 转换;
  • 边界处理;
  • 游戏循环与状态判断。

扫雷虽然规则简单,但非常适合作为 C 语言阶段性练习项目。建议你先照着代码运行一遍,再尝试自己加入"插旗"和"自动展开"功能,这样对数组和函数封装的理解会更深入。

相关推荐
瑶池酒剑仙44 分钟前
C++变量初始化五法完全指南:默认/拷贝/直接/列表/值初始化与窄化转换陷阱(青云之志)
c语言·开发语言·数据结构·c++·算法·visual studio
国科安芯1 小时前
星上计算的第一道防线:低轨遥感卫星载荷处理链中的AS32S601型抗辐射MCU—— 面向高分辨率成像与在轨智能处理的场景化综述
大数据·网络·人工智能·单片机·算法·低轨卫星·抗辐射
thusloop1 小时前
零门槛接入 GPT-6-astra 与 claude-opus-5-5,三大场景实战指南
python·gpt·算法·agent·claude·codex
瓦瓦gogo2 小时前
学回溯算法时,我先弄明白了怎么“退回来”
算法
Lzg_na2 小时前
堆和堆排序
算法
TK泰妞2 小时前
TikTok 带货视频如何做多语言本地化?从翻译脚本到重做场景的实用方法
算法·音视频
房开民2 小时前
DDPM 完整原理 + 全套公式串联
算法
lunar_coder2 小时前
哈希扩展 -- 位图思想
c++·算法·哈希算法
秋天的怀念_David2 小时前
听故事学语言 · 数据结构篇 · 序章一 算法之问
数据结构·程序人生·算法