目录
1.扫雷游戏的分析和设计
(1)扫雷游戏的实现的功能和说明
游戏总逻辑部分:
程序运行一次,在游戏刚开始前,玩家可以选择玩或不玩,如果玩,每次游戏结束玩家也可以选择玩或不玩
游戏设计和逻辑部分:
a.游戏为一个9 * 9的游戏棋盘,玩家可以输入范围内的坐标来进行扫雷
b.玩家输入的坐标如果藏雷,则游戏直接结束,如果不藏雷,则在该位置显示四周的雷数,并在终端显示游戏开始到当前的扫雷的棋盘结果
c.如果成功扫完所有无雷区,则游戏结束
2.代码实现
(1)游戏总逻辑部分:
程序运行起来,玩家刚开始选择玩或不玩,因为玩家每玩一次结束,又可以重新选择玩或不玩,所
以在这里可以设置一个循环条件使其能玩多次,所以这里建议用 do while 循环结构,因为其结构
特点就是一进入循环,先做完一次再判断,然后玩家输入的值为1就表示Play(玩),2就表示
Quit(退出),但难免玩家输入的值不是这两个数,所以输入其他值的话,就提示让玩家重新输入,
所以还可以在每次输入前在屏幕上打印提示信息
(2)游戏设计和逻辑部分:
因为为一个平面棋盘,所以我们考虑可以用二维数组来展示,,在创建之前,我们应该考虑我们的棋盘既要用来在屏幕上面打印出来每次扫完雷的棋盘,又要用来存储含雷区和不含雷区信息的棋盘,所以如果用一个二维数组容易混淆起来,不利于我们分析和排雷,所以我们创建两个数组,一个用来打印,一个用来存储,之后考虑用字符 ' 1 ' 和 ' 0 '分别表示有雷和无雷也方便统计四周的雷数
然后假设我们创建的时9 * 9的二维数组我们思考一下当我们扫下去的坐标无雷,要统计四周含雷数目,这是就会有两种情况
由该图就可知当扫到的无雷区在边界的时候,对它四周进行访问就会发现有一部分位置不是我们没有权限进行访问,如果硬要访问就造成数组越界访问的行为,所以可以考虑将二维数组在扩大一圈,创建两个11 * 11的二维数组。
ok,分析到这里就可以开始写代码了
创建两个二维数组
char Set_Mine[11][11];//用来存储有雷和无雷信息的棋盘
char Show_mine[11][11];//用来打印每次排查雷区后的棋盘
文件结构
文件结构我们采用多文件结构
新建两个源文件和一个头文件
regular-practice.c//⽂件中写游戏的测试逻辑
game.c//⽂件中写游戏中函数的实现等
game.h//⽂件中写游戏需要的数据类型和函数声明等
宏定义和函数声明放到game.h头文件中
//利用宏定义define将定义的内容进行了替换
//这样的话避免以后要修改行数或列数等就不用整个程序中出现的行数或列数值就行修改,只要将define定义的内容再进行了替换就行
#define ROWS 11//表示二维数组的行数
#define COLS 11//表示二维数组的边数
#define COL COLS-2//表示的棋盘的行数
#define ROW ROWS-2//表示的棋盘的边数
#define Count 11//表示的地雷数
函数定义的内容放到game.c源文件中
因为game.c源文件中要实现各类函数的功能,免不了
在regular-practice.c源文件如果要用该函数直接引用即可
二维数组初始化
自定义一个函数 Init 用来专门初始化二维数组的函数
这样的话只要二维数组都可以进行初始化,节省代码量
//第一个参数为要传进来的二位数组
//第二个参数为操作的行数
//第三个参数为操作的列数
//第四个参数为要初始化的字符
void Init(char Set_Mine[ROWS][COLS], int rows, int cols, char op)
{
int i = 0;
for(i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
Set_Mine[i][j] = op;
}
}
}
打印棋盘
为了让玩家方便观察和分析,每次扫完雷的棋盘都将它打印出来
自定义一个函数 print 用来专门打印棋盘
//第一个参数为要打印的数组
//第二个参数为操作的行数
//第三个参数为操作的列数
void print(char Mine[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%2c", Mine[i][j]);
}
printf("\n");
}
}
为了让玩家更好的分析和观察棋盘和坐标,可以对打印专门的函数进行优化,在每行和每列前面显
示其对应的行数或列数
//第一个参数为要打印的数组
//第二个参数为操作的行数
//第三个参数为操作的列数
void print(char Mine[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("%d", i);
for (j = 1; j <= col; j++)
{
printf("%2c", Mine[i][j]);
}
printf("\n");
}
}
布置地雷
自定义一个函数 Set 用来专门布置地雷
因为要随机布置地雷,所以我们要用到生成随机数的相关函数 rand ,srand,time
//第一个参数为设置地雷的二位数组
//第二个参数为操作的行数
//第三个参数为操作的列数
//第四个参数为布置雷的个数
void Set(char Set_Mine[ROWS][COLS], int rows, int cols, int count)
{
while (count)
{
int x = rand() % rows + 1;//生成随机数1 —— 9
int y = rand() % cols + 1;//生成随机数1 —— 9
if (Set_Mine[x][y] == '0')//为了防止当前生成的随机数所对应的位置已经布置雷
{
Set_Mine[x][y] = '1';
count--;
}
}
}
排查地雷
自定义一个函数 Cheak_Mine 用来排查地雷
int Cheak_Mine(char Set_Mine[ROWS][COLS], char Show_Mine[ROWS][COLS], int row, int col)
{
int count = 0;//用来计算标记地雷的个数
while (1)//成功排雷次数
{
count = 0;
int i = 0;
int j = 0;
int x = 0;
int y = 0;
printf("请输入一个坐标 -> ");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//限定范围
{
if (Set_Mine[x][y] == '1')
{
printf("很抱歉,您刚好扫到地雷了\n");
break;
}
else
{
if (Show_Mine[x][y] == '*')//避免对该位置二次排查
{
Recursion(Set_Mine, Show_Mine, x, y);//递归展开
Aut(Set_Mine, Show_Mine);//标记地雷
print(Show_Mine, ROW, COL);//打印数组
for (i = 1; i <= ROW; i++)//遍历数组,统计标记个数
{
for (j = 1; j <= COL; j++)
{
if (Show_Mine[i][j] == '!')
{
count++;
}
}
}
int cnt = 0;
for (i = 1; i <= ROW; i++)//统计字符 ‘*’ 的个数
{
for (j = 1; j <= COL; j++)
{
if (Show_Mine[i][j] == '*')
{
cnt++;
}
}
}
if ((count == Count) && (cnt == 0))两个条件满足,扫雷成功,退出循环
{
break;
}
}
else
{
printf("该位置已经扫过,请从新输入新的坐标\n");
}
}
}
else
{
printf("输入的坐标超出范围,请从新输入\n");
}
}
return count;
}
递归展开一片
自定义一个函数 Recursion 用来实现递归
递归思路:
首先先讲一下它递归展开一片的原理:
当我们确定要排某个坐标时且该坐标不含地雷时,点下去之后,我们以该位置为中心构成的3 * 3正方形里的每个坐标为基准,计算该范围里每一个坐标其四周含地雷数目,1:如果一个地雷都没有,我们把该位置赋值为空格,并继续调用 Recursion 函数继续这个坐标重复上面的步骤,实现递归,2:只要其四周含地雷则我们把该位置赋值为其四周含地雷数目的字符形式
大家可以看以下这两张图对比一下前后的不同,方便大家理解
红圈里面有1的位置为我第一次点的位置,之后就递归展开成这样,其他位置大家可以自行脑补一下
第二次点的位置就是红色炸弹的位置,游戏结束
根据以上我们就可以知道该
递归函数的三要素:
1.递归函数功能:
传入一个坐标,判断其3 * 3范围内每个坐标四周含有的地雷数,根据地雷数对其相应在Show_Mine数组的位置进行赋值;
2.递归的结束条件:含某个位置四周含地雷
3.函数的等价关系:通过不断的对其棋盘上的位置进行判断,进而扩大判断范围,靠近含地雷的位置
代码实现递归:
void Recursion(char Set_Mine[ROWS][COLS],char Show_Mine[ROWS][COLS],int x, int y)
{
//遍历该位置3 * 3范围内的每个位置
for (int i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (int j = y - 1; j <= y + 1;j++)
{
if ((i < 1) || (i > ROW) || (j < 1) || (j > COL) || (Show_Mine[i][j] != '*')||(Set_Mine[i][j] == '1'))//防止数组越界或已经排查过了或排的位置为地雷
{
continue;//跳过循环剩下的部分,进入下一次循环
}
int num = NUM(Set_Mine, i, j,'1');//统计含地雷个数
if (num == 0)
{
Show_Mine[i][j] = ' ';
Recursion(Set_Mine, Show_Mine, i, j);//调用函数本身,实现递归
}
else
{
Show_Mine[i][j] = num + '0';
}
}
}
}
自定义一个函数 NUM 用来计算某个位置四周的地雷数
//该函数计算四周含字符' 1 '的个数即含地雷个数
NUM(char Mine[ROWS][COLS], int x, int y, char op)//op为要计算的字符
{
int num = 0;
int i = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
if ((i < 1) || (i > ROW) || (j < 1) || (j > COL))
{
continue;
}
if (Mine[i][j] == op)
{
num++;
}
}
}
return num;
}
标记地雷
标记思路:
当我们每一次递归后,遍历一遍Show_Mine数组,如果遍历过程中有元素为0 - 8整数的字符形式,则我们遍历该位置四周的每个元素,如果其四周的字符 ' * ' 的个数再加上已经标记过的地雷数目 即字符' ! ' 的个数总和 加上 ' 0 ' 等于该位置的ASCLL值,说明' * 的位置为地雷,则遍历其四周3 * 3范围内每个元素,只要是有 ' * ' 的位置赋值为字符 ' ! ' ,作为标记
大家可以参考以下图片理解
代码实现标记:
自定义一个函数 Aut 用来实现标记
void Aut(char Set_Mine[ROWS][COLS], char Show_Mine[ROWS][COLS])
{
for (int i = 1; i <= ROW; i++)
{
int j = 0;
for (j = 1; j <= COL; j++)
{
if ((Show_Mine[i][j] < 49) || (Show_Mine[i][j] > 56))
{
continue;
}
int cnt = NUM(Show_Mine, i, j, '*');
cnt += NUM(Show_Mine, i, j, '!');
if (cnt + '0' == Show_Mine[i][j])
{
for (int k = i - 1; k <= i + 1; k++)
{
for (int l = j - 1; l <= j + 1; l++)
{
if (Show_Mine[k][l] == '*' && Set_Mine[k][l] == '1')
{
Show_Mine[k][l] = '!';
}
}
}
}
}
}
}
完整源代码
game.h头文件代码
#pragma once
#include <stdio.h>
#include <stdlib.h>//srand.rand函数
#include <time.h>//time函数
#define ROWS 11//表示二维数组的行数
#define COLS 11//表示二维数组的边数
#define COL COLS-2//表示的棋盘的行数
#define ROW ROWS-2//表示的棋盘的边数
#define Count 10//表示设置的地雷数量
void Init(char Set_Mine[ROWS][COLS], int rows, int cols, char op);
void print(char Mine[ROWS][COLS], int row, int col);
void Set(char Set_Mine[ROWS][COLS], int rows, int cols, int count);
int Cheak_Mine(char Set_Mine[ROWS][COLS], char Show_Mine[ROWS][COLS], int row, int col);
regular-practice源文件代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
printf("************************************\n");
printf("********1 ------- Play**************\n");
printf("********0 ------- Quit**************\n");
printf("************************************\n");
}
void game()
{
char Set_Mine[ROWS][COLS];//用来存储有雷和无雷信息
char Show_Mine[ROWS][COLS];//用来打印每次排查雷区后的棋盘
//创建数组
Init(Set_Mine, ROWS, COLS, '0');
Init(Show_Mine, ROWS, COLS, '*');
//初始化数组print(Show_Mine, ROW, COL);
Set(Set_Mine, ROW, COL, Count);
//布置雷
print(Show_Mine, ROW, COL);
//打印棋盘
int count = Cheak_Mine(Set_Mine, Show_Mine, ROW, COL);
//排查地雷
if (count == Count)
{
printf("恭喜您,扫雷成功\n");
}
print(Show_Mine, ROW, COL);
//打印棋盘
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择玩或不玩 ");
printf("\n");
scanf("%d", &input);
switch (input)
{
case 1:
printf("----Game Start-----\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
game.c源文件代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void Init(char Set_Mine[ROWS][COLS], int rows, int cols, char op)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
Set_Mine[i][j] = op;
}
}
}
void print(char Mine[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("%d", i);
for (j = 1; j <= col; j++)
{
printf("%2c", Mine[i][j]);
}
printf("\n");
}
}
void Set(char Set_Mine[ROWS][COLS], int rows, int cols, int count)
{
while (count)
{
int x = rand() % rows + 1;//生成随机数1 —— 9
int y = rand() % cols + 1;//生成随机数1 —— 9
if (Set_Mine[x][y] == '0')
{
Set_Mine[x][y] = '1';
count--;
}
}
}
static int NUM(char Mine[ROWS][COLS], int x, int y, char op)
{
int num = 0;
int i = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
if ((i < 1) || (i > ROW) || (j < 1) || (j > COL))
{
continue;
}
if (Mine[i][j] == op)
{
num++;
}
}
}
return num;
}
static void Recursion(char Set_Mine[ROWS][COLS],char Show_Mine[ROWS][COLS],int x, int y)
{
int num = NUM(Set_Mine, x, y, '1');
if (num == 0)
{
for (int i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (int j = y - 1; j <= y + 1; j++)
{
if ((i < 1) || (i > ROW) || (j < 1) || (j > COL) || (Show_Mine[i][j] != '*') || (Set_Mine[i][j] == '1'))
{
continue;
}
Show_Mine[i][j] = ' ';
Recursion(Set_Mine, Show_Mine, i, j);
}
}
}
else
{
Show_Mine[x][y] = num + '0';
}
}
void Aut(char Set_Mine[ROWS][COLS], char Show_Mine[ROWS][COLS])
{
for (int i = 1; i <= ROW; i++)
{
int j = 0;
for (j = 1; j <= COL; j++)
{
if ((Show_Mine[i][j] < 49) || (Show_Mine[i][j] > 56))
{
continue;
}
int cnt = NUM(Show_Mine, i, j, '*');
cnt += NUM(Show_Mine, i, j, '!');
if (cnt + '0' == Show_Mine[i][j])
{
for (int k = i - 1; k <= i + 1; k++)
{
for (int l = j - 1; l <= j + 1; l++)
{
if (Show_Mine[k][l] == '*' && Set_Mine[k][l] == '1')
{
Show_Mine[k][l] = '!';
}
}
}
}
}
}
}
int Cheak_Mine(char Set_Mine[ROWS][COLS], char Show_Mine[ROWS][COLS], int row, int col)
{
int count = 0;
while (1)//成功排雷次数
{
count = 0;
int i = 0;
int j = 0;
int x = 0;
int y = 0;
printf("请输入一个坐标 -> ");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//限定范围
{
if (Set_Mine[x][y] == '1')
{
printf("很抱歉,您刚好扫到地雷了\n");
break;
}
else
{
if (Show_Mine[x][y] == '*')
{
Recursion(Set_Mine, Show_Mine, x, y);
Aut(Set_Mine, Show_Mine);
print(Show_Mine, ROW, COL);
for (i = 1; i <= ROW; i++)
{
for (j = 1; j <= COL; j++)
{
if (Show_Mine[i][j] == '!')
{
count++;
}
}
}
int cnt = 0;
for (i = 1; i <= ROW; i++)
{
for (j = 1; j <= COL; j++)
{
if (Show_Mine[i][j] == '*')
{
cnt++;
}
}
}
if ((count == Count) && (cnt == 0))
{
break;
}
}
else
{
printf("该位置已经扫过,请从新输入新的坐标\n");
}
}
}
else
{
printf("输入的坐标超出范围,请从新输入\n");
}
}
return count;
}
运行结果
扫雷游戏(递归+标记)运行结果
ok,到这就结束了,以上就是关于扫雷游戏的简单设计,如有不足之处,可以在评论区留言,小编看到后就会及时纠正,有兴趣的同学可以试着去学一下哦。
最后谢谢诸位读者的阅读