1. 游戏介绍
扫雷是一款益智的小游戏,游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
我们进入游戏,首先会有如下的一个棋盘:
我们通过点击棋盘格,如果这个格子上没有雷,在相应的格子上就会出现相应的数字,表示这个格子的左上、右上、左下、右下、上、下、左、右相邻的8个格子中雷的个数。如果这个格子上有雷,则棋盘会将所有雷的位置显示,游戏失败。如果剩下没有点击的格子均有雷,游戏成功。
话不多说,接下来让我们用C语言来实现扫雷小游戏吧!
2. 设计思路
首先说明本人使用的编译器是Visual Studio 2022。我准备将游戏的主体代码用一个头文件game.h和两个源文件game.c和test.c实现。其中game.h包含游戏函数的声明,game.c是游戏函数的实现,而test.c用来测试游戏函数。
我的设计思路如下:
(1)首先设计一个游戏菜单,用户可以通过游戏菜单的说明进行相应的游戏操作。
(2)扫雷游戏是在一个平面进行,所以我们需要使用二维数组。这里我打算创建2个二维数组,均为字符数组,一个mine数组存放布置好的雷,一个show数组存放排查出的雷的信息(也就是显示这个格子周围8个格子有几个雷)。
(3)初始化棋盘并打印棋盘。
(4)用随机数函数在mine数组中布置雷,然后排查雷,同时统计雷的个数。最后通过相应的判断来显示游戏的输赢,判断完游戏输赢后,显示棋盘中所有雷的位置。
3. 具体实现
3.1 设计游戏菜单
void menu()
{
printf("************************\n");
printf("***** 1. play ******\n");
printf("***** 0. exit ******\n");
printf("************************\n");
}
我们在test.c文件中写一个menu()函数,使用printf()函数打印菜单,玩家输入1时开始游戏,输入0时退出游戏。
因为玩家玩好一局游戏后,可能还想继续玩。所以这里我采用了do-while结构,同时用了switch语句来实现用户做出相应的选择后执行相应的功能。
又因为我们后面布置雷时是随机布置的,所以会使用随机数函数rand(),srand()函数和time()函数。
这三个函数的用法在我之前写的三子棋小游戏中有所介绍:https://blog.csdn.net/m0_62531913/article/details/131906875?spm=1001.2014.3001.5501
main()函数的实现:
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("开始游戏\n"); //即game()函数部分,我们之后用game()函数替换这行代码
break; //这里是为了方便测试
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择!\n");
}
} while (input);
return 0;
}
这里的printf("开始游戏\n") ;这行代码只是为了方便测试,我们之后用game()函数实现游戏的功能,用game();替换这行代码
3.2 设计棋盘
3.2.1 设计棋盘大小
这里我们用二维数组创建一个棋盘,棋盘的大小(行和列)用宏定义实现。使用宏定义能改变棋盘的大小,增强程序的可移植性。
我们这里将棋盘的行ROW和列COL宏定义为9。又因为接下来在排查雷时,我们需要对我们选择的棋盘格的左上、右上、左下、右下、上、下、左、右相邻的8个格子进行相应判断操作,这时候容易出现统计坐标周围的雷的个数时越界的问题(即下图框出来的部分):
为了解决这个问题,我们只要在给棋盘上面添加一行,下面添加一行,左边添加一列,右边添加一列即可解决:
因此我们在这里宏定义了ROWS为ROW+2,而COLS为COL+2。
在game.h中进行宏定义:
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
在test.c中写一个game()函数,在函数中创建2个二维数组mine数组和show数组:
void game()
{
char mine[ROWS][COLS] = {
0 }; //存放布置好的雷
char show[ROWS][COLS] = {
0 }; //存放排查出的雷的信息
}
3.2.2 初始化棋盘
我们将mine数组初始化为'0',将show数组初始化为'*'。
在game.h文件中进行InitBoard()的函数声明:
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
在game.c文件中进行InitBoard()的函数实现:
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;
}
同时在test.c文件的game()函数中增加相应内容:
void game()
{
char mine[ROWS][COLS] = {
0 }; //存放布置好的雷
char show[ROWS][COLS] = {
0 }; //存放排查出的雷的信息
//初始化棋盘
//1. mine数组最开始为全'0'
//2. show数组最开始为全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
}
3.2.3 打印棋盘
我们为了方便和美观,需要将棋盘的行号和列号打印出来。
即达到下图效果:
首先我们在game.h文件中进行DisplayBoard()的函数声明:
void DisplayBoard(char board[ROWS][COLS], int row, int col);
在game.c文件中进行DisplayBoard()的函数实现:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("--------扫雷游戏--------\n");
for (int i = 0; i <= col; i++) //打印列号
{
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i); //打印行号
for (int j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
我们同时在test.c文件中的game()函数中增加相应内容:
void game()
{
char mine[ROWS][COLS] = {
0 }; //存放布置好的雷
char show[ROWS][COLS] = {
0 }; //存放排查出的雷的信息
//初始化棋盘
//1. mine数组最开始为全'0'
//2. show数组最开始为全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
}
我们测试打印后的棋盘经过初始化后如下:
3.3 布置雷
我们一开始将mine数组初始化为全'0',我们可以用随机数函数rand()生成相应的有雷的坐标,这里我布置10个雷。
定义x,y两个变量分别为雷的横、纵坐标,因为雷要落在棋盘上,且不能在有雷的位置上放雷。
所以我们使用rand() % row + 1让x的范围为1~row,使用rand() % col + 1让y的范围为1~col。并且我们要进行if语句判断,避免重复布置雷。
定义一个count变量记录雷的个数,因为我们要生成10个雷的坐标,所以我们使用while循环,每进行一次循环(布置1个雷),就让count--。
在game.h文件中进行SetMine()的函数声明:
void SetMine(char board[ROWS][COLS], int row, int col);
为了增强代码的可移植性,我们在game.h中增加宏定义:
#define EASY_COUNT 10
在game.c文件中进行SetMine()函数的实现:
void SetMine(char board[ROWS][COLS], int row, int col)
{
//布置10个雷
//随机生成雷的坐标,布置雷
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1; //1~row
int y = rand() % col + 1; //1~col
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
在test.c中我们调用SetMine()函数,并DisplayBoard()函数进行测试:
void game()
{
char mine[ROWS][COLS] = {
0 }; //存放布置好的雷
char show[ROWS][COLS] = {
0 }; //存放排查出的雷的信息
//初始化棋盘
//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);
}
我们可以看到棋盘格共有10个'0'被标记成了'1',即这些位置是有雷的。
3.4 排查雷
在game.h中进行FindMine()的函数声明:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
3.4.1 GetMineCount()——统计坐标周围雷的个数
我们自定义了一个GetMineCount()函数,用来得到某个坐标的左上、右上、左下、右下、上、下、左、右相邻的8个格子中雷的个数。因为我们一开始将mine数组初始化为全'0',将布置了雷的坐标标记为'1'。
因此我们可以将某个坐标的左上、右上、左下、右下、上、下、左、右相邻的8个格子相加,利用数字和字符间ASCII码的关系,再减去8个'0',就能得到某坐标周围8个相邻坐标的雷的个数。
某个坐标x,y的相邻8个格子的坐标如下:
在game.c中实现GetMineCount()函数:
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');
}
3.4.2 DfsMine()——递归展开非雷区域
扫雷小游戏中,我们可以通过点一个坐标,如果这个坐标不是雷。就会相应展开一片不是雷的区域,我们可以考虑用递归实现,这里我使用了深度优先搜索(DFS)。
我们搜索某个坐标,当这个坐标不是雷,这个坐标周围没有雷,且这个坐标没有被排查过,我们继续往下通过递归进行深度优先搜索。
当一个坐标周围有雷时,我们使用return;
这里我使用了数组dx[ ]和dy[ ]分别存储了某个坐标相邻的左上,上,右上,左,右,左下,下,右下的8个格子的坐标,通过for循环同时进行递归。
我们将计数器win的地址传参给函数的形参,每次当有一个坐标满足条件可以搜索,我们就在搜索这个坐标前前让计数器win加1(因为win在这里是指针,要想让win值+1,就要要进行指针解引用的操作)。
//递归,展开一片不是雷的区域
void DfsMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* win)
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
if (count != 0)
return;
int dx[] = {
-1,-1,-1,0,0,1,1,1 };
int dy[] = {
-1,0,1,-1,1,-1,0,1 };
for (int i = 0; i < 8; i++)
{
int a = x + dx[i];
int b = y + dy[i];
if (a >= 1 && a <= row && b >= 1 && b <= col && mine[a][b] == '0' && show[a][b] == '*')
{
*win += 1;
DfsMine(mine, show, row, col, a, b, win);
}
}
}
3.4.3 FindMine()
我们定义x,y表示要排查的横、纵坐标。定义win来计数,即记录游戏进行的次数。
棋盘中雷的个数为EASY_COUNT,总共有rowcol个格子,所以没有雷的格子为rowcol-EASY_COUNT 个。
我们可以用一个while循环来进行排查雷,每排查一次,如果满足相应条件,我们就让win++。
当我们输入排查的坐标非法会提示"坐标非法,重新输入!"。
坐标合法且为雷时,就会提示"很遗憾,你被炸死了!",并且展示雷在棋盘的分布。
坐标合法但不为雷时,就会统计坐标周围8个坐标的雷的个数。如果坐标周围没有雷,就会展开一片不是雷的区域。
最后我们判断,当win的值等于row*col-EASY_COUNT(即棋盘剩下的没有被排查的格子均为雷)时,游戏获胜,提示"恭喜你,排雷成功!",并且展示雷在棋盘的分布。
在game.c中进行FindMine()的函数实现:
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (win < row * col - EASY_COUNT)
{
printf("请输入要排查的坐标:>");
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
{
//该位置不是雷,就统计这个坐标周围有几个雷
int count = GetMineCount(mine, x, y);
if (count == 0)
{
//递归,展开一片没有雷的区域
DfsMine(mine, show, row, col, x, y, &win); //这里需要传win的地址
}
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
printf("坐标非法,重新输入!\n");
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功!\n");
DisplayBoard(mine, ROW, COL);
}
}
我们先测试一下递归展开一片区域:
为了方便测试我们这里将EASY_COUNT先改为80:
4.完整代码
因为最后以游戏的形式给用户玩,所以测试时有些打印棋盘的代码,我们需要删除掉或者注释掉。
4.1 game.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//初始化棋盘
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);
4.2 game.c
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("--------扫雷游戏--------\n");
for (int i = 0; i <= col; i++) //打印列号
{
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i); //打印行号
for (int j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
//布置10个雷
//随机生成雷的坐标,布置雷
int count = EASY_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 - 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');
}
//递归,展开一片不是雷的区域
void DfsMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* win)
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
if (count != 0)
return;
int dx[] = {
-1,-1,-1,0,0,1,1,1 };
int dy[] = {
-1,0,1,-1,1,-1,0,1 };
for (int i = 0; i < 8; i++)
{
int a = x + dx[i];
int b = y + dy[i];
if (a >= 1 && a <= row && b >= 1 && b <= col && mine[a][b] == '0' && show[a][b] == '*')
{
*win += 1;
DfsMine(mine, show, row, col, a, b, win);
}
}
}
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (win < row * col - EASY_COUNT)
{
printf("请输入要排查的坐标:>");
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
{
//该位置不是雷,就统计这个坐标周围有几个雷
int count = GetMineCount(mine, x, y);
if (count == 0)
{
//递归,展开一片没有雷的区域
DfsMine(mine, show, row, col, x, y, &win);
}
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
printf("坐标非法,重新输入!\n");
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功!\n");
DisplayBoard(mine, ROW, COL);
}
}
4.3 test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu()
{
printf("************************\n");
printf("***** 1. play ******\n");
printf("***** 0. exit ******\n");
printf("************************\n");
}
void game()
{
char mine[ROWS][COLS] = {
0 }; //存放布置好的雷
char show[ROWS][COLS] = {
0 }; //存放排查出的雷的信息
//初始化棋盘
//1. mine数组最开始为全'0'
//2. show数组最开始为全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(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");
break;
default:
printf("选择错误,重新选择!\n");
}
} while (input);
return 0;
}
5.总结
到这里,我们的扫雷小游戏的设计就结束了。通过设计该游戏,我加深了对二维数组和循环结构的理解,学习使用了递归进行深度优先搜索,也感悟了指针传地址给函数形参的奥妙。能比较熟练使用随机数函数。在数组的使用中也注意到了考虑越界的问题。
希望通过后续学习,不断提高代码能力和分析解决问题的能力。由于本人是大一学生,知识水平有限,本文如果有错误和不足之处,还望各位大佬多多指出并提出建议!