目录
一、前言
《扫雷》是一款大众类的益智小游戏,游戏目标是在尽可能短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷就全盘皆输。
提到扫雷大家都不陌生,扫雷自Windows 3.1系统以来就存在我们的电脑中,扫雷的难度等级分为初级(9 * 9个方块,一共10个雷)、中级(16 * 16个方块,一共40个雷)、高级(16 * 30个方块,一共99个雷)等模式。扫雷可以说是必玩的一个游戏,不知道大家扫雷的成绩如何呢?就让我们深入探讨扫雷游戏的内核,一共来进行扫雷游戏各种功能代码的书写。
二、扫雷游戏的实现思路
2.1分析扫雷游戏和需要解决的问题
我们要思考如何写出和上面图片中一样的效果,我们可以发了9*9的格子被一层银白色的格子覆盖,当点开格子的时候可以发现以下的几个问题
☆1 现在的扫雷游戏得到了优化,第一下点不会被雷炸死
☆2 当点到的格子九宫格内有雷时,点到的格式会返回周围一圈雷的个数
☆3 当点到的格子没有雷时,会将周围都展开直到有格子周围有雷时停止
☆4 我们可以对确定是雷的位置进行标记
然后不断地点开格子,直到把所有的雷的格子全部点开后,就代表我们把游戏里的雷全部找到,获得胜利。我们上面提到有一层银白格子将将雷给覆盖,那么此时我们就需要两个二维数组,一个mine数组对雷的坐标进行存储,但不能让玩家看见(看见了就不叫扫雷了,而就叫鼠标练习啦),一个show数组存放点开格子周围九宫格内雷的个数,也就是我们看到的银白色格子。
那我们还要思考的一个问题是我们的二维数组要设多大才合适?
我们可以从上面图片可以看到,边缘格子的九宫格是绿色框框,如果我们只创建9 * 9的二维数组,我们在逐个排查点开格子周围时,可能宝越界访问,我们只需把行列各加一行变为11 * 11的二维数组就不会发生越界访问了。
2.2游戏主体的流程图
三、扫雷游戏的各个功能的函数实现
3.1扫雷游戏的菜单
游戏菜单的打印
#include"game.h"
void menu()
{
printf("*****************************************\n");
printf("********* 1.开始游戏 *********\n");
printf("********* 0.退出游戏 *********\n");
printf("*****************************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };//存放布置好的雷的信息
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');//'0'
InitBoard(show, ROWS, COLS, '*');//'*'
//打印一下棋盘
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");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
运行的效果图
3.2棋盘的初始化
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;
}
}
}
运行的效果图
3.3雷坐标的设置
使用srand和rand函数来产生随机数从而随机设置雷的坐标,我们将’1’作为雷,'0’作为非雷。
//设置地雷
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
运行的效果图
3.4棋盘的打印
大家可以改变ROW、COL和MineCount的个数来改变游戏的难度
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("***************扫雷游戏***************\n");
for (i = 0; i <= row; i++)
{
printf(" %d ", i);
}
printf("\n");
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf(" %d ", i);
for (j = 1; j <= col; j++)
{
printf(" %c ", board[i][j]);
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
if (i <= row - 1)
{
printf(" ");
for (j = 1; j <= col; j++)
{
printf("———");
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
}
}
printf("***************扫雷游戏***************\n");
}
3.5避免第一次中雷
俗话说人一倒霉了,喝凉水都塞牙缝。才开始我没准备写防第一次就中雷的,但我连续好几次第一次就炸死了。那么我们这个函数的逻辑是什么呢?就是说当我们第一次遇到雷时,我们把这个坐标改为 ’ ',然后再为这个雷生成一个随机坐标。
void FirstSafe(char mine[ROWS][COLS], int x, int y)//防止玩家第一次被炸死。
{
int m = 0;
int n = 0;
do
{
m = rand() % 9 + 1;
n = rand() % 9 + 1;//如果玩家踩到雷,就重新随机一个没有雷的地方,将炸弹移动到那里。
if (mine[m][n] == '0')
{
mine[x][y] = ' ';//将该区域设置为空格。
mine[m][n] = '1';
break;
}
} while (1);
}
运行的效果图
从上图我们可以看到第一次我们选择了雷但没有炸。
3.6对雷的坐标进行标记与取消标记
void SignMine(char show[ROWS][COLS], int row, int col,int n)
{
int x = 0;
int y = 0;
while (1)
{
if (1 == n)//标记地雷位置
{
printf("请输入要标记的坐标:>");
scanf("%d %d", &y, &x);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
show[x][y] = '!';
break;
}
else
{
printf("该位置不能被标记,请重新输入:>\n");
}
}
else
{
printf("输入坐标非法,请重新输入:\n");
}
}
else //取消标记地雷位置
{
printf("请输入要取消标记的坐标:>");
scanf("%d %d", &y, &x);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '!')
{
show[x][y] = '*';
break;
}
else
{
printf("该位置不能被取消标记,请重新输入:>\n");
}
}
else
{
printf("输入坐标非法,请重新输入:\n");
}
}
}
system("cls");
DisplayBoard(show, row, col);
}
运行的效果图
3.7统计雷的个数
我们可以将我们点开的格子当作上图的x, y,那么周围的8个格子就是我们需要排查的雷,从而将数字放在x,y上,那么我们现在有两种思路。
☆1 我们可以对周围8个格子逐一排查,若每遇到一个雷就增加一个来进行计数,再返回回去。
☆2 我们可以先把周围8个格子的字符型数据减去’0’转化为整型数据进行相加,最后把和加上’0’返回回去。
//统计九宫格内存在地雷的个数
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x][y - 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] +
mine[x][y + 1] +
mine[x - 1][y + 1] - 8 * '0';
}
3.8扫雷
对各个函数功能的一个串联,从而排查出地雷的位置。
//扫雷
void FindMind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;//记录排查出不是雷的个数
int n = 0;
while (win < row * col - COUNT)
{
printf("请输入要排查的位置下标:>");
scanf("%d %d", &y, &x);
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入下标是否有效
{
if (mine[x][y] == '1')//排查到了地雷
{
if (!win)//防止第一次被炸死
{
FirstSafe(mine, x, y);
win++;
continue;
}
DisplayBoard(show, row, col);
break;
}
//此时没有排查到地雷
else
{
win++;
//空白展开
ExplodeSpread(mine, show, row, col, x, y);
system("cls");
//打印棋盘
DisplayBoard(show, row, col);
printf("需要标注或取消标注地雷输入:>1,不需要标注地雷则输:>0\n");
scanf("%d", &n);
switch (n)
{
case 1:
printf("需标注地雷请输入:>1,需取消标注地雷请输入:>2\n");
scanf("%d", &n);
SignMine(show, row, col,n);//标记雷的位置
break;
default:
break;
}
}
}
else
{
printf("输入下标非法,请重新输入:>\n");
}
}
//把所有mine中地雷显示到show上
ShowAllMine(mine, show, row, col);
system("cls");
//打印棋盘
DisplayBoard(show, row, col);
//判断是否排查成功
if (win == row * col - COUNT)
{
printf("所有雷均已被排查出,恭喜你,获得胜利\n");
}
else
{
printf("你被炸死了,排查失败\n");
}
}
运行的效果图
3.9展示雷的位置
void ShowAllMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (mine[i][j] == '1')
{
show[i][j] = '#';//地雷用字符‘#’在玩家界面的上进行展示
}
}
}
}
3.10扫雷的空白展开
这个展开函数也就伍用递归的思想来解决,递归的本质也就是大事化小,这个函数就是从你点开的不是雷时,一直向你周围的8个格子排查,直到遇到有格子周围的8个格子有雷时停下。
递归最怕的也就是死递归,所以我们要明确我们的问题,从而添加限制条件,防止死递归。
第一个问题是如果我们从mine数组进行排查一直扩张,但同一个格子并没有改变所以我们的函数一直会对这个格子进行排查,从而陷入死递归。
☆其实我们可以从show数组进行排查,排查后将show数组中的’*‘改变为’ ',从而就解决函数会对同一个格子进行排查的问题,那么这个死递归问题就解决了。
第二个问题就是我们没有对函数排查的坐标进行限制,从而导致越界访问
☆我们只需对函数的坐标排查范围进行限制只在棋盘上排查,这个问题也就解决了。
//空白展开函数
void ExplodeSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y,char* win)
{
//防止非法坐标的展开
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//计算该位置附近四周地雷的个数
int count = GetMineCount(mine, x, y);
//若四周没有一个地雷,则需要向该位置的四周展开,直到展开到某个位置附近存在地雷为止
if (count == 0)
{
//把附近没有地雷的位置变成字符 ' '
show[x][y] = ' ';
(*win)++;
int i = 0;
//向周围共8个位置递归调用
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
//限制对点位置的重复展开调用,使得每一个位置只能向四周展开一次
if (show[i][j] == '*')
{
ExplodeSpread(mine, show, row, col, i, j,&win);
}
}
}
}
//若周围存在地雷则应该在这个位置上标注上地雷的个数
else
{
show[x][y] = count + '0';
}
}
}
四、完整代码
game.h
game.h —— 包含了程序所需的各种头文件并且包换对实现游戏功能的各种函数的声明
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define COUNT 10
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//设置地雷
void SetMine(char mine[ROWS][COLS], int row, int col);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//统计九宫格内存在地雷的个数
int GetMineCount(char mine[ROWS][COLS], int x, int y);
//被炸死后展示地雷
void ShowAllMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//扫雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//空白展开函数
void ExplodeSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y);
//防止玩家第一次被炸死。
void FirstSafe(char mine[ROWS][COLS], int x, int y);
//标记地雷
void SignMine(char show[ROWS][COLS], int row, int col, int n);
game.c
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 SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("***************扫雷游戏***************\n");
for (i = 0; i <= row; i++)
{
printf(" %d ", i);
}
printf("\n");
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf(" %d ", i);
for (j = 1; j <= col; j++)
{
printf(" %c ", board[i][j]);
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
if (i <= row - 1)
{
printf(" ");
for (j = 1; j <= col; j++)
{
printf("———");
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
}
}
printf("***************扫雷游戏***************\n");
}
//统计九宫格内存在地雷的个数
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x][y - 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] +
mine[x][y + 1] +
mine[x - 1][y + 1] - 8 * '0';
}
//把所有mine中地雷全部显示到show上
void ShowAllMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (mine[i][j] == '1')
{
show[i][j] = '#';//地雷用字符‘#’在玩家界面的上进行展示
}
}
}
}
//空白展开函数
void ExplodeSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
//防止非法坐标的展开
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//计算该位置附近四周地雷的个数
int count = GetMineCount(mine, x, y);
//若四周没有一个地雷,则需要向该位置的四周展开,直到展开到某个位置附近存在地雷为止
if (count == 0)
{
//把附近没有地雷的位置变成字符 ' '
show[x][y] = ' ';
int i = 0;
//向周围共8个位置递归调用
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
//限制对点位置的重复展开调用,使得每一个位置只能向四周展开一次
if (show[i][j] == '*')
{
ExplodeSpread(mine, show, row, col, i, j);
}
}
}
}
//若周围存在地雷则应该在这个位置上标注上地雷的个数
else
{
show[x][y] = count + '0';
}
}
}
//防止玩家第一次被炸死。
void FirstSafe(char mine[ROWS][COLS], int x, int y)
{
int m = 0;
int n = 0;
do
{
m = rand() % row + 1;
n = rand() % col + 1;//如果玩家踩到雷,就重新随机一个没有雷的地方,将炸弹移动到那里。
if (mine[m][n] == '0')
{
mine[x][y] = ' ';//将该区域设置为空格。
mine[m][n] = '1';
break;
}
} while (1);
}
//标记地雷
void SignMine(char show[ROWS][COLS], int row, int col, int n)
{
int x = 0;
int y = 0;
while (1)
{
if (1 == n)//标记地雷位置
{
printf("请输入要标记的坐标:>");
scanf("%d %d", &y, &x);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
show[x][y] = '!';
break;
}
else
{
printf("该位置不能被标记,请重新输入:>\n");
}
}
else
{
printf("输入坐标非法,请重新输入:\n");
}
}
else //取消标记地雷位置
{
printf("请输入要取消标记的坐标:>");
scanf("%d%d", &y, &x);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '!')
{
show[x][y] = '*';
break;
}
else
{
printf("该位置不能被取消标记,请重新输入:>\n");
}
}
else
{
printf("输入坐标非法,请重新输入:\n");
}
}
}
system("cls");
DisplayBoard(show, row, col);
}
//扫雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;//记录排查出不是雷的个数
int n = 0;
while (win < row * col - COUNT)
{
printf("请输入要排查的位置下标:>");
scanf("%d %d", &y, &x);
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入下标是否有效
{
if (mine[x][y] == '1')//排查到了地雷
{
if (!win)//防止第一次被炸死
{
FirstSafe(mine, x, y);
win++;
continue;
}
DisplayBoard(show, row, col);
break;
}
//此时没有排查到地雷
else
{
//空白展开
ExplodeSpread(mine, show, row, col, x, y);
system("cls");
//打印棋盘
DisplayBoard(show, row, col);
printf("需要标注或取消标注地雷输入:>1,不需要标注地雷则输:>0\n");
scanf("%d", &n);
switch (n)
{
case 1:
printf("需标注地雷请输入:>1,需取消标注地雷请输入:>2\n");
scanf("%d", &n);
SignMine(show, row, col, n);//标记雷的位置
break;
default:
break;
}
}
}
else
{
printf("输入下标非法,请重新输入:>\n");
}
}
//把所有mine中地雷显示到show上
ShowAllMine(mine, show, row, col);
system("cls");
//打印棋盘
DisplayBoard(show, row, col);
//判断是否排查成功
if (win >= row * col - COUNT)
{
printf("所有雷均已被排查出,恭喜你,获得胜利\n");
}
else
{
printf("你被炸死了,排查失败\n");
}
}
test.c
test.c —— 对游戏需要的函数进行引用
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu()
{
printf("*****************************************\n");
printf("********* 1.开始游戏 *********\n");
printf("********* 0.退出游戏 *********\n");
printf("*****************************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };//存放布置好的雷的信息
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');//'0'
InitBoard(show, ROWS, COLS, '*');//'*'
//打印一下棋盘
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");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
五、结语
以上是我对扫雷游戏的实现,如果时间充足还是要从前到尾看一遍,肯定会有收获的,尽量不要直接跳转到完整代码直接复制!!!如果有大佬感兴趣可以进一步优化。
因本人能力还有待提高,如有错误可以及时指出。感谢大家的观看和支持。