目录
一、基本功能
二、附加功能
1、标记
2、通过递归实现展开
三、源码
扫雷作为一款经典游戏,其主要玩法就是在一片固定的区域(n*n)中,通过所获得的一个格子周围的8个格子中的雷的数量,来避开地雷,最终使棋盘上只剩下地雷。
利用C语言实现扫雷,用到的主要知识有:二维数组、循环语句、选择语句、常量的定义、头文件的使用、随机数生成
环境:Visual Studio 2022
首先,扫雷游戏需要一个大小为n*n的棋盘,这里以9*9大小的棋盘为例
创建名为game.h的头文件,用于引入头文件、常量的定义、函数的声明。在头文件中分别定义棋盘大小行(ROW)和列(COL),在头文件中定义常量方便后期修改
#define ROW 9
#define COL 9
因为游戏时,方格中显示的数字为该方格周围8个方格所有的雷的数量,若数组的大小为9*9,在计算边缘方格时存在越界访问的问题,因此,我们在定义数组时应该在棋盘的上下左右各加一行(列),此时再定义数组的行和列
#define ROWS ROW+2
#define COLS COL+2
此时,我们就可以再创建一个名为test.c的源文件,并开始游戏的主函数
作为游戏的开头,一个开始菜单必不可少。开始菜单应该同时满足开始和退出游戏、在完成一次游戏后再次开始的功能。那么可以通过switch语句对玩家的输入做出选择。
void menu()
{
printf("**************************\n");
printf("***** 1.start *****\n");
printf("***** 0.exit *****\n");
printf("**************************\n");
printf("请选择=>");
}
为防止玩家输入的选项超出我们给的范围,我们通过循环提醒玩家。以下为开始菜单的写法
do//程序先执行一次
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
//printf("game\n");
game();
break;
case 0:
printf("exit\n");
break;
default:
printf("error\n");
break;
}
} while (input);//当输入非法时,再次输入
此时便可以开始编写游戏的主要函数game()
首先需要创建游戏所用的数组,若只有一个数组,将难以同时完成布置雷区和显示游玩情况等目标,因此,两个或以上的数组会是更好的选择,此处我们创建两个数组,分别用于游戏进程的显示(show)和雷区的布置(mine)
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
在创建二维数组后,需要对其进行初始化,在show数组中,用‘ * ’表示未排查的方块,在mine中,用‘ 0 ’表示非雷区,用‘ 1 ’表示雷区。我们可以写一个通用函数,利用循环对两个数组进行初始化。为方便管理,我们将调用的函数放置于名为game.c的源文件中
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
因为两个数组所需初始化的内容不同,此处通过向函数传递内容来确定初始化内容(char set)。
//初始化函数的调用
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
此处再写一个打印棋盘的函数,并在棋盘左边和上边设置坐标,方便查看,用于在测试时确认棋盘被初始化,和在游玩时显示进度
void DisplayBoard(char arr[ROWS][COLS], int row, int col)//此时打印的棋盘大小为9*9
{
int i = 0;
int j = 0;
printf("---mine clearance---\n");
for (j = 0; j <= col; j++)//打印列的编号,方便观察
{
printf("%d ", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);//打印行的编号
for (j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
printf("---mine clearance---\n");
}
在创建并初始化数组后,就可以开始写布雷和扫雷的程序了
FindMine(show, mine, ROW, COL)
由于使用随机数生成雷区,因此我们需要在玩家第一次选择之后再生成雷区,此处为第一次输入
//第一次输入坐标
printf("请输入坐标=>");
while (1)//判断输入
{
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
break;
}
else
{
printf("输入非法,请重新输入=>");
}
}
这是就可以避开玩家的选择来生成雷区:在mine数组中用1代表雷区,通过随机数写入
srand((unsigned int)time(NULL));//利用时间戳设置随机数生成的种子
//放在main函数中
//需要头文件time.h
//和stdlib.h
在头文件中定义雷的数量
#define EAZY_COUNT 10
//写一个函数用于生成雷区
void SetMine(char arr[ROWS][COLS], int row, int col, int a, int b)
{
int i = 0;
int j = 0;
int count = EAZY_COUNT;
while (count)
{
i = rand() % row + 1;
j = rand() % col + 1;
if (i != a && j != b)
{
if (arr[i][j] != '1')
{
arr[i][j] = '1';
count--;
}
}
}
}
此时就可以开始写玩家的输入了,此处需要先检验玩家的输入是否合法,再判断是否踩雷,因此我们,需要一个循环来实现,此处我选择用死循环,在输入合法坐标时跳出循环。此处同样需要打印棋盘来显示游戏进度。
在玩家输入后,程序需要校验玩家是否踩雷,若没踩雷,则需要校验周围的8格内存在几个雷,并打印出来。
while (1)//判断输入
{
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
break;
}
else
{
printf("输入非法,请重新输入=>");
}
}
if (mine[x][y] == '1')//判断有没有踩雷
{
printf("爆了!!!!!!\n");
DisplayBoard(mine, ROW, COL);
goto END;
}
else
{
if (show[x][y] != '*')
{
printf("该坐标已被排查,请重新输入\n");
}
else
{
show[x][y] = MineCount(mine, x, y) + '0';//校验雷的数量
DisplayBoard(show, ROW, COL);
}
break;
}
//通过循环检验雷的数量
//因为数组类型为cahr,所以其中的1和0都是以ASCII值储存
//字符数字在转换为整形数字时需要-48,也就是0对应的值
//或者写成-‘0’
int MineCount(char mine[ROWS][COLS], int x, int y)//计算输入坐标周围的雷数
{
int i = -1;
int j = -1;
int count = 0;
for (; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
count += mine[x + i][y + j];
}
}
count -= ('0' * 9);//将字符转换为整形
return count;
}
最后就是输赢的判断,输的判断很简单,就是判断是否踩雷,也就是玩家的选择坐标对应的mine数组的值是否为1。赢的判断有两种,这里先讲第一种,下文会讲第二种。
我们可以通过计算成功操作,也就是输入合法的次数来计算。设置一个变量count,每成功输入一次,count++,当count的值为棋盘格数减去雷的个数时,玩家胜利。
count == ROW * COL - EAZY_COUNT;
这就是扫雷基本功能的实现。
我们都知道,在Windows中的扫雷中,玩家可以通过右键进行标记,我们通过判断也可以在自己写的简易扫雷中实现这个功能。我们可以通过直接修改show数组中对应的ASCII值对显示字符进行修改。
scanf("%d %d", &x, &y);
show[x][y] = 4;//标记
DisplayBoard(show, ROW, COL);
再添加一个判断,让玩家选择进行什么操作,并将其与排查功能结合。
printf("请选择模式(1、排查 2、标记)\n");//实现标记功能
while (1)
{
scanf("%d", &chose);
if(chose == 1)//选择排查
{
printf("请输入坐标=>");
while (1)//判断输入
{
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
break;
}
else
{
printf("输入非法,请重新输入=>");
}
}
if (mine[x][y] == '1')//判断有没有踩雷
{
printf("爆了!!!!!!\n");
DisplayBoard(mine, ROW, COL);
goto END;
}
else
{
if (show[x][y] != '*'&& show[x][y] != 4)
{
printf("该坐标已被排查,请重新输入\n");
}
else
{
show[x][y] = MineCount(mine, x, y) + '0';
DisplayBoard(show, ROW, COL);
count = Iswin(show, ROW, COL);
}
break;
}
}
else if (chose == 2)//选择标记
{
printf("请输入坐标=>");
scanf("%d %d", &x, &y);
show[x][y] = 4;//标记
DisplayBoard(show, ROW, COL);
break;
}
else
{
printf("选择错误,请重新选择=>");
}
}
在扫雷中,玩家第一个选择的方格一定不是雷,并且程序会把玩家选择的格子周围不存在雷的区域全部显示,在c语言中,我们可以通过递归来实现这个功能。
通过调用计算雷数的函数,从玩家第一次选择的格子开始,逐渐向四周判断:当一个格子应该显示0时,则在show中将其改为空格,即不显示,再以这个格子为起始点向周围判断,当一个格子的显示不为0时,终止该格向四周扩展的过程,并显示该格对应数字。
在运用递归时,应注意递归的条件,防止出现死递归。
void AutoExc(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y)
{
int i = -1;
int j = -1;
show[x][y] = ' ';//将当前格子替换为空格
for (; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (mine[x + i][y + j] != '1')
{
int ret = 0;
ret = MineCount(mine, x + i, y + j);//判断下一格是否为0
if (ret == 0)
{
if (show[x + i][y + j] == '*')//防止判断过的格子被再次判断,造成死递归
{
AutoExc(show, mine, ROW, COL, x + i, y + j);
}
show[x + i][y + j] = ' ';
}
else
{
show[x + i][y + j] = ret + '0';
}
}
}
}
}
此时我们需要运用第二种判断胜利的方法:计算没有被玩家排查或者标记格格子数
int Iswin(char show[ROWS][COLS],int row,int col)
{
int i = 1;
int j = 1;
int count = 0;
for (; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
if (show[i][j] == '*'|| show[i][j] == 4)
{
count++;
}
}
}
return count;
}
game.h
#pragma once
#include
#include
#include
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EAZY_COUNT 10
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set);//初始化棋盘
void DisplayBoard(char arr[ROWS][COLS], int rows, int cols);//打印棋盘
void SetMine(char arr[ROWS][COLS], int row, int col, int a, int b);//设置雷区
void FindMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int cow);//扫雷
int MineCount(char mine[ROWS][COLS], int x, int y);//计算雷数
void AutoExc(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y);//自动排除
test.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void menu()
{
printf("**************************\n");
printf("***** 1.start *****\n");
printf("***** 0.exit *****\n");
printf("**************************\n");
printf("请选择=>");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
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(show, mine, ROW, COL);//开始扫雷
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));//设置随机数生成的种子
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
//printf("game\n");
game();
break;
case 0:
printf("exit\n");
break;
default:
printf("error\n");
break;
}
} while (input);
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
void DisplayBoard(char arr[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf("---mine clearance---\n");
for (j = 0; j <= col; j++)//打印列的编号,方便观察
{
printf("%d ", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);//打印行的编号
for (j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
printf("---mine clearance---\n");
}
void SetMine(char arr[ROWS][COLS], int row, int col, int a, int b)
{
int i = 0;
int j = 0;
int count = EAZY_COUNT;
while (count)
{
i = rand() % row + 1;
j = rand() % col + 1;
if (i != a && j != b)
{
if (arr[i][j] != '1')
{
arr[i][j] = '1';
count--;
}
}
}
}
void FindMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = 0;
int chose = 0;
//第一次输入坐标
printf("请输入坐标=>");
while (1)//判断输入
{
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
break;
}
else
{
printf("输入非法,请重新输入=>");
}
}
SetMine(mine, ROW, COL, x, y);//在第一次输入后设置雷区,防止上来就失败了
show[x][y] = MineCount(mine, x, y) + '0';
DisplayBoard(show, ROW, COL);
//DisplayBoard(mine, ROW, COL);
AutoExc(show, mine, ROW, COL, x, y);//将第一个坐标周围没有雷的地方自动排除
DisplayBoard(show, ROW, COL);
while (count < ROW * COL - EAZY_COUNT)
{
printf("请选择模式(1、排查 2、标记)\n");//实现标记功能
while (1)
{
scanf("%d", &chose);
if(chose == 1)//选择排查
{
printf("请输入坐标=>");
while (1)//判断输入
{
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
break;
}
else
{
printf("输入非法,请重新输入=>");
}
}
if (mine[x][y] == '1')//判断有没有踩雷
{
printf("爆了!!!!!!\n");
DisplayBoard(mine, ROW, COL);
goto END;
}
else
{
if (show[x][y] != '*'&& show[x][y] != 4)
{
printf("该坐标已被排查,请重新输入\n");
}
else
{
show[x][y] = MineCount(mine, x, y) + '0';
DisplayBoard(show, ROW, COL);
count = Iswin(show, ROW, COL);
}
break;
}
}
else if (chose == 2)//选择标记
{
printf("请输入坐标=>");
scanf("%d %d", &x, &y);
show[x][y] = 4;//标记
count = Iswin(show, ROW, COL);
DisplayBoard(show, ROW, COL);
break;
}
else
{
printf("选择错误,请重新选择=>");
}
}
if (count == EAZY_COUNT)
{
printf("YOU WIN!\n");
DisplayBoard(mine, ROW, COL);
break;
}
}
END:
;
}
int MineCount(char mine[ROWS][COLS], int x, int y)//计算输入坐标周围的雷数
{
int i = -1;
int j = -1;
int count = 0;
for (; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
count += mine[x + i][y + j];
}
}
count -= ('0' * 9);//将字符转换为整形
return count;
}
void AutoExc(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y)
{
int i = -1;
int j = -1;
show[x][y] = ' ';
for (; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (mine[x + i][y + j] != '1')
{
int ret = 0;
ret = MineCount(mine, x + i, y + j);
if (ret == 0)
{
if (show[x + i][y + j] == '*')
{
AutoExc(show, mine, ROW, COL, x + i, y + j);
}
show[x + i][y + j] = ' ';
}
else
{
show[x + i][y + j] = ret + '0';
}
}
}
}
}
int Iswin(char show[ROWS][COLS],int row,int col)
{
int i = 1;
int j = 1;
int count = 0;
for (; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
if (show[i][j] == '*'|| show[i][j] == 4)
{
count++;
}
}
}
return count;
}