1.前序
扫雷游戏,这算是一个家喻户晓的游戏了。没完过的话也可以试着玩一玩,大致规则就是在最短时间把所有的所有的雷排查出来即为胜利,如果在期间踩到雷则游戏失败(结束)。那么这篇博客就来介绍如何C语言实现扫雷游戏的具体步骤。
扫雷游戏网页版链接👉minesweeper
2.扫雷游戏分析与设计
2.1 扫雷游戏功能说明
- 游戏可以通过菜单实现继续玩或者退出游戏
- 扫雷的棋盘是9*9的格子
- 默认随机布置10个雷
- 可以排查雷
- 如果不是雷,就显示周围有几个雷
- 如果是雷,就炸死游戏结束
- 把除10个雷之外的所有非雷都找出来,排雷成功
2.2 游戏数据结构分析
下面是我们游戏实现的大致数据结构的分析
- mine数组:棋盘雷区信息
定义:使用一个11x11的数组来存储棋盘信息,其中实际使用的是9x9的范围。
初始化:所有元素初始化为0,表示无雷;布置雷的位置设置为1。
目的:避免边缘排查时的越界问题,同时清晰区分雷区和非雷区。
为什么要使用11x11而不是9x9呢,因为当我们要排查的坐标处于数组的边缘时,计算周围雷的数量就可能产生越界和出错,但后续实际只使用到了9x9的范围。
- show数组:对外展示界面
定义:用于展示给玩家的棋盘界面,初始状态为’*',表示未排查。
更新:当玩家排查一个坐标时,根据周围雷的数量更新该坐标的显示。
数据存储的逻辑
避免混淆:将雷区信息和排查结果分开存储,防止信息混淆。
信息记录:排查非雷区域时,记录周围雷的数量,为玩家提供重要参考。
2.3 游戏文件结构设计
在此项目中我们采用多文件的形式以方便调用和编写:
1.test.c (写文件的测试逻辑)
2.game.c(写游戏中函数的实现)
3.game.h(写游戏中所需要的数据类型和函数声明)
3.扫雷游戏代码实现
3.1 菜单页面
我们这里调用game.h文件,不要忘了还有最基本的stdio.h文件哟,菜单界面可以根据自己的喜好进一步的优化设计,这里主要实现了一个基本架构,输入1开始游戏,输入0停止,输入其他则会报错。
#include"game.h"
void menu()
{
printf("*****************************\n");
printf("*********1.play**************\n");
printf("*********0.exit**************\n");
printf("*****************************\n");
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
return 0;
}
接下来我们开始进行游戏中涉及到的各个函数的编写。
3.2 棋盘初始化
我们通过两个for循环对数组中的每个位置进行赋值,完成棋盘数组的初始化
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
参数的说明
- char board[ROWS][COLS]:当函数的参数是一个数组时,传递的是数组的地址。这意味着在函数内部对数组的修改会反映到函数外部的原始数组上。对于二维数组作为参数的情况,在声明和定义时虽可省略行数,但其是必要的,它决定了数组的内存布局
在函数调用时,作为参数的二维数组可以通过数组名来传递,会传过来首元素的地址 - int rows,int cols:用于指定创建的数组的行数和列数,这有助于在函数内部初始化数组。
- char set:是一个字符类型的变量,用于初始化数组的每个元素。通过传递一个字符类型的参数,一个函数可以处理多种初始化字符,从而减少代码冗余并提高效率。如果我们是直接使用字"*"或"0"这样就需要我们重复去编写功能大致相同的两个函数。
3.3 棋盘打印
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("-------扫雷游戏-------\n");
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= 9; i++)
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= 9; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
参数的说明
第一个参数与上方函数的功能说明是一致的不再赘述,需要注意的是我们这里的第二个和第三个参数与初始化函数中的是不一致的,在上面函数中,我们对所有数组范围都进行了初始化,即便我们后期有一部分用不到,但是在打印的函数中我们只用到了9x9的范围,同时for循环也要注意不要超出这个9x9的范围。
为了让我们在编写代码的过程中不会遗漏和混淆,我们现在把写好的函数部分在game.h、test.c文件中分别定义调用。
void game()
{
char mine[ROWS][COLS];//存放布置雷的信息
char show[ROWS][COLS];//存放排查雷的信息
//初始化棋盘
//1,mine数组最开始全是‘0’
//2,show数组最开始全是‘*’
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
我们在test.c文件中编写void game()函数进行调用。
#pragma once
#include<stdio.h>
#include<stdlib.h>
#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);//打印棋盘
在game.h头文件中对函数,变量进行定义。
我们来看看现在的实现效果,我们这里已经对代码进行了优化有一行”-------扫雷游戏-------“将界面分割开,有对应的行列号,加强用户的游玩体验。
接下来我们继续game.c文件中游戏函数的实现
3.4 布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
//布置10个雷
//随机生成十个随机坐标,布置雷
int count = COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
现在我们对上述代码进行解释:
- 为满足游戏要求,我们用一个整型变量–count来记录雷的数量(这里是十个,如果棋盘的规格更改,雷的数量可以相应增加)
- 生成随机数,需要在main函数加上一句srand((unsigned int)time(NULL)); 并在头文件包含time.h 和stdlib.h 头文件(这个在猜数字游戏的博客中都有介绍)
- 我们通过一个while循环来布置雷,x,y两个整型变量用来接收随机值,当成功布置了一个雷,count–,知道count为0跳出循环。
- 在if语句中判断该坐标点是否被设置为雷,如果没有就将当前坐标点标记为雷。
随机数生成的实现在 猜数字游戏 中有详细讲解,不再赘述。
同样和前面一样我们将SetMine在game.h文件中完成声明
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define 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);//布置雷
在test.c文件中对SetMine函数进行调用,我们再次调用一下DisplayBoard来检查一下我们雷的布置情况。
void game()
{
char mine[ROWS][COLS];//存放布置雷的信息
char show[ROWS][COLS];//存放排查雷的信息
//初始化棋盘
//1,mine数组最开始全是‘0’
//2,show数组最开始全是‘*’
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL);
让我们来测试几次看看效果!!!
现在我们就已经完成了我们雷盘的基本设定啦,接下来就是非常重要的一步排查雷。
3.5 排查雷
我们来梳理一下大致的思路:
- 如果位置不是雷,该坐标就会显示周围的雷的数量
- 如果位置是雷,就炸死游戏结束
- 把除10个雷之外的所有非雷坐标都找出来,排雷成功,游戏结束
- 我们希望能我们的游戏能像我们平时玩得扫雷一样显现出爆炸式展开的效果
- 标记出用户认为的雷的坐标
为了实现我们上面的要求我们现在要写四个函数,分别为:
- GetMineCount(统计周围雷的数量)
- ExplodeBoard(实现游戏的爆炸式展开)
- Signmine(标记雷的位置)
- FindMine(排查雷)
首先我们先来实现GetMineCount函数,让我们来思考一下,想要统计周围雷的数量,是不是只要统计排查雷的坐标周围的3x3的范围,因为前期我们将雷设置为 ‘1’,非雷位置为’0’,那么现在我们只要将3x3的范围中雷的数量加起来即可。
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y] - '0' + mine[x - 1][y - 1] - '0' + mine[x][y - 1] - '0' + mine[x + 1][y - 1] - '0'
+ mine[x + 1][y] - '0' + mine[x + 1][y + 1] - '0' + mine[x][y + 1] - '0' + mine[x - 1][y + 1] - '0');
}
函数首先计算了 (x, y) 周围八个格子的地雷数量。这八个格子分别是:(x-1, y)、(x-1, y-1)、(x, y-1)、(x+1, y-1)、(x+1, y)、(x+1, y+1)、(x, y+1)、(x-1, y+1)。
对于每个格子,函数通过 mine[x - 1][y] - ‘0’ 这样的表达式将字符转换为相应的整数值。这里 ‘0’ 是字符 ‘0’ 的 ASCII 值,通过减去它,可以将字符 ‘1’ 转换为 1,字符 ‘2’ 转换为 2,以此类推。如果字符是 ‘0’,结果就是 0,然后相加。
注意事项:
- 这个函数假设 x 和 y 的值在数组的边界内,即 1 <= x <= ROWS 和 1 <= y <= COLS。如果 x 或 y的值超出这个范围,访问 mine[x][y] 将会导致数组越界错误。
- 函数没有显式地检查边界条件,而是依赖于调用者确保传入的 x 和 y 是有效的索引。
我们来看一下效果!
好的我们继续来编写ExplodeBoard函数,我们平时玩的扫雷是不是像下面的图片一样有些地方一点击周围就会展开一大块,而我们目前的代码很显然还没有实现这样的功能。
如何实现这个功能呢?话不多说,先上代码!!!
//爆炸展开
void ExplodeBoard(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* p)
{
int temp = *p;
//限制条件
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//计算该位置周围雷的个数
int count = GetMineCount(mine, x, y);
if (count == 0)
{
//把该位置变成空格
show[x][y] = ' ';
int i = 0;
//向周围进行递归遍历
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
//限制对重复递归调用的条件,避免死递归
if (show[i][j] == '*')
{
ExplodeBoard(mine, show, row, col, i, j, &temp);
temp++;
}
}
}
}
else
{
show[x][y] = count + '0';
}
}
}
函数实现思路-----
- 函数通过 *p 获取当前的计数器值,并将其存储在局部变量 temp 中。
- 函数检查当前格子 (x, y) 是否在雷区的边界内。
- 如果格子在边界内,函数调用 GetMineCount 来计算当前格子周围地雷的数量。
- 如果count(地雷数量)为0,表示当前格子周围没有地雷,函数将 show[x][y] 设置为 ’ '(空格),表示该格子已展开。
- 接下来,函数使用两层嵌套循环遍历当前格子周围的8个格子,并检查每个格子是否已经被标记为 ‘*’(未展开)。
- 如果某个格子未展开(show[i][j] == ‘*’),函数递归调用 ExplodeBoard 来展开这个格子周围的区域,并更新。
- temp 的值。 如果 count 不为0,表示当前格子周围有地雷,函数将 show[x][y] 设置为表示地雷数量的字符(例如’1’、‘2’ 等)。
- int* p: 指向整数的指针,用于在递归调用中传递一个计数器。
函数中的 if (show[i][j] == ‘*’) 条件用于避免对已经展开的格子进行重复递归调用,这是防止死递归的关键。
我们来看看效果吧,怎么样还不错吧!
好的再接再厉我们来通过编写Signmine函数,来实现标记地雷的效果。如果仔细看的话,会发现在上图中有一个坐标位置是以!的形式呈现的,那么我们是怎么实现这样的功能的呢。
void Signmine(char board[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要标记的坐标:");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (board[x][y] == '*')
{
board[x][y] = '!';
break;
}
else
{
printf("该位置不能被标记,请重新输入:\n");
}
}
else
{
printf("坐标非法,请重新输入:\n");
}
}
}
函数实现思路-----
- 函数使用一个无限循环 while (1) 来不断请求用户输入要标记的坐标。
- 使用 printf 函数提示用户输入坐标,并使用 scanf 函数读取用户的输入。
- 函数首先检查用户输入的坐标 (x, y) 是否在雷区的边界内,即 1 <= x <= row 且 1 <= y <= col。
- 如果坐标在边界内,函数接着检查 board[x][y] 是否已经被标记为地雷(用 ‘*’ 表示)。
- 如果该位置已经被标记为地雷,函数将该位置的标记更改为 ‘!’,表示用户想要更改标记。
- 如果该位置没有被标记为地雷,函数输出提示信息,要求用户重新输入。
- 如果坐标不在边界内,函数输出提示信息,要求用户重新输入。
- 当用户输入了一个有效的坐标,并且该位置已经被标记为地雷时,函数将标记更改为 ‘!’ 并退出循环。
好啦,接下来就只需要将我们这些函数全部在FindMine函数中调用,让我们来编写FindMine函数吧。
在该函数中我们还需要实现的功能就是让用户进行排查雷,如果十个雷全部避开,则提示排雷成功;
如果刚坐标为雷,则提示被炸死啦。
如果输入错误,则提示非法输入重新输入。
//排查雷的函数
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
char ch = 0;
while (win < ROW * COL - COUNT)
{
printf("输入所要排查的目标:\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//检查坐标是否合法
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了。\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
//爆炸展开
win++;
ExplodeBoard(mine, show, row, col, x, y, &win);
//打印棋盘
DisplayBoard(show, ROW, COL);
printf("需要标注地雷输入:Y,不需要则输入:N\n");
//清空缓冲区
while ((ch = getchar()) != '\n');
scanf("%c", &ch);
if (ch == 'Y')
{
//标记雷的位置
Signmine(show, ROW, COL);
}
}
}
else
{
printf("坐标非法,重新输入:\n");
}
//打印棋盘
DisplayBoard(show, ROW, COL);
}
if (win == ROW * COL - COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
函数实现思路-----
- 函数使用一个 while 循环,直到用户成功排查的格子数量等于雷区中非地雷格子的总数(即 ROW * COL - COUNT)。
- 循环中,首先提示用户输入要排查的坐标。
- 检查用户输入的坐标是否合法,即是否在雷区的边界内。 如果坐标合法,检查 mine[x][y]是否为 ‘1’,
- 如果是,表示用户触雷,游戏结束,打印雷区布局并退出循环。
- 如果没有触雷,增加 win 的计数,并调用ExplodeBoard 函数展开周围没有地雷的区域。
- 之后,提示用户是否需要在当前位置标记地雷,并根据用户输入调用 Signmine函数。
- 循环中的每次迭代后,都会调用 DisplayBoard 函数打印当前的棋盘状态。
好啦我们游戏的所涉及的所有功能函数都编写完成啦,不要忘记在test.c和game.h文件中调用和声明哟。
void game()
{
char mine[ROWS][COLS];//存放布置雷的信息
char show[ROWS][COLS];//存放排查雷的信息
//初始化棋盘
//1,mine数组最开始全是‘0’
//2,show数组最开始全是‘*’
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL);
//排查雷
FindMine(mine, show, ROW, COL);
}
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define 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);//排查雷
所有的代码都编写完啦,不要忘记把之前我们布置雷是DisplayBoard函数注释掉,要不然就会将布置的雷都展示出来啦。
我技艺不精,完整的代码我贴在下面啦。大家来玩玩看吧!
完整代码呈现
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//初始化数组
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("-------扫雷游戏-------\n");
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= 9; i++)
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= 9; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
//布置10个雷
//随机生成十个随机坐标,布置雷
int count = COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y] - '0' + mine[x - 1][y - 1] - '0' + mine[x][y - 1] - '0' + mine[x + 1][y - 1] - '0'
+ mine[x + 1][y] - '0' + mine[x + 1][y + 1] - '0' + mine[x][y + 1] - '0' + mine[x - 1][y + 1] - '0');
}
//爆炸展开
void ExplodeBoard(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* p)
{
int temp = *p;
//限制条件
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//计算该位置周围雷的个数
int count = GetMineCount(mine, x, y);
if (count == 0)
{
//把该位置变成空格
show[x][y] = ' ';
int i = 0;
//向周围进行递归遍历
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
//限制对重复递归调用的条件,避免死递归
if (show[i][j] == '*')
{
ExplodeBoard(mine, show, row, col, i, j, &temp);
temp++;
}
}
}
}
else
{
show[x][y] = count + '0';
}
}
}
//标记雷的函数
void Signmine(char board[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要标记的坐标:");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (board[x][y] == '*')
{
board[x][y] = '!';
break;
}
else
{
printf("该位置不能被标记,请重新输入:\n");
}
}
else
{
printf("坐标非法,请重新输入:\n");
}
}
}
//排查雷的函数
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
char ch = 0;
while (win < ROW * COL - COUNT)
{
printf("输入所要排查的目标:\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//检查坐标是否合法
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了。\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
//爆炸展开
win++;
ExplodeBoard(mine, show, row, col, x, y, &win);
//打印棋盘
DisplayBoard(show, ROW, COL);
printf("需要标注地雷输入:Y,不需要则输入:N\n");
//清空缓冲区
while ((ch = getchar()) != '\n');
scanf("%c", &ch);
if (ch == 'Y')
{
//标记雷的位置
Signmine(show, ROW, COL);
}
}
}
else
{
printf("坐标非法,重新输入:\n");
}
//打印棋盘
DisplayBoard(show, ROW, COL);
}
if (win == ROW * COL - COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void menu()
{
printf("*****************************\n");
printf("*********1.play**************\n");
printf("*********0.exit**************\n");
printf("*****************************\n");
}
void game()
{
char mine[ROWS][COLS];//存放布置雷的信息
char show[ROWS][COLS];//存放排查雷的信息
//初始化棋盘
//1,mine数组最开始全是‘0’
//2,show数组最开始全是‘*’
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//排查雷
FindMine(mine, show, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
return 0;
}
game.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define 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);//排查雷
这么详细求个小心心不过分吧。!!!!!!!