扫雷是很多人最早接触的电脑游戏之一。它规则简单,但非常适合练习 C 语言的二维数组、函数封装、多文件编程、随机数、字符处理等知识。
这篇文章会从扫雷的规则和设计思路讲起,再一步一步给出完整代码实现。最终实现一个 9×9 棋盘、默认 10 个雷的控制台扫雷游戏。
一、游戏功能说明
我们要实现的控制台扫雷包含以下功能:
- 使用控制台运行经典扫雷游戏;
- 游戏菜单可以开始游戏或退出;
- 棋盘为 9×9;
- 默认随机布置 10 个雷;
- 玩家输入坐标排查雷:
- 如果该位置是雷,游戏结束;
- 如果该位置不是雷,显示周围 8 个格子中有几个雷;
- 当所有非雷位置都被排查出来,游戏胜利;
- 可以重复游玩。
二、整体设计思路
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:排查雷
排查逻辑:
- 展示当前
show棋盘; - 输入坐标;
- 判断坐标是否合法;
- 判断该位置是否已经排查过;
- 如果踩雷,游戏结束,并展示
mine棋盘; - 如果不是雷,统计周围雷数,写入
show数组; - 每成功排查一个非雷位置,
remain--; - 当
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 * * * * * * * * *
请输入要排查的坐标(行 列):
七、可以继续扩展的功能
这个版本是基础版扫雷,后续可以继续扩展:
- 标记功能:输入坐标后选择"排查"或"插旗";
- 自动展开:如果周围雷数为 0,自动展开周围区域;
- 难度选择:初级 9×9、中级 16×16、高级 16×30;
- 计时功能;
- 显示剩余雷数;
- 防止第一次踩雷:第一次排查的位置一定不是雷。
总结
通过这个扫雷小游戏,我们练习了:
- 二维数组的使用;
- 多文件编程;
- 函数封装;
- 随机数生成;
- 字符与数字的 ASCII 转换;
- 边界处理;
- 游戏循环与状态判断。
扫雷虽然规则简单,但非常适合作为 C 语言阶段性练习项目。建议你先照着代码运行一遍,再尝试自己加入"插旗"和"自动展开"功能,这样对数组和函数封装的理解会更深入。