如果聊起有什么让大家印象深刻的童年游戏,那么《 扫雷 》的地位应该是不可撼动的。这个搭载在 Windows 系统菜单里的小游戏在当时可谓大红大紫,算的上是我们实打实的童年回忆了。
或许我们在一遍又一遍玩扫雷的同时,都会回忆起小学夏天的那节电脑课上,穿着鞋套,在硕大的白色电脑上,瞒着老师偷偷玩一把扫雷的简单快乐吧~
如今我们用经典的C语言复刻扫雷,还能否勾起你对童年的回忆呢?
扫雷游戏规则:玩家需要尽快找出雷区中的所有不是地雷的方块,而不许踩到地雷。
众所周知。点开其中一个小方格之后,数字是几,就说明它周围的八个方位就有几个雷
比如下图中就表示红框内存在2个雷
在介绍过三子棋的实现后,要实现扫雷是不是就会感觉就简单一些了呢?其实二者有很多相似的地方,扫雷游戏的实现主要是对下面两个模块的实现:
一、游戏交互界面
- 创建游戏菜单
- 创建游戏逻辑主体
二、扫雷游戏实现
- 创建扫雷棋盘
- 初始化扫雷棋盘
- 布置雷
- 打印扫雷棋盘
- 用户排查雷
- 判断输赢
这里准备了一张图,便大家更直观的理清游戏实现原理与游戏实现的逻辑 :
代码展示:
void menu()
{
printf("* * * * * * * * * * * * * * * * * *\n");
printf("* * * * * * 1.扫雷游戏 * * * * * *\n");
printf("* * * * * * 0.退出游戏 * * * * * *\n");
printf("* * * * * * * * * * * * * * * * * *\n");
}
代码展示:
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
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;
}
1、
srand((unsigned int)time(NULL))
表示随机数生成函数通过时间戳生成随机数,为下面的布置雷做准备。2、
do-while()
因为游戏至少可以进入一次,至少让玩家输入一次玩或不玩,所以用do-while()
循环–循环代码至少执行一次。
观察上面这段代码,其中我们使用了一个game()
函数来实现扫雷游戏,但是这个函数还未定义,下面将实现对game()
函数的定义。
代码展示
void game()
{
//创建雷盘:
char mine[ROWS][COLS] = { 0 };//存放布置好的雷的信息
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
//初始化雷盘:
InitBoard(mine, ROWS, COLS,'0');
InitBoard(show, ROWS, COLS,'*');
//布置雷:
SetMine(mine, ROW, COL);
//向玩家打印扫雷棋盘:
DisPlayBoard(show, ROW, COL);
//排查雷并判断输赢
FindMine(mine,show,ROW,COL);
}
代码展示:
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
char mine[ROWS][COLS] = { 0 };//存放布置好的雷的信息
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
问题1:这里为什么创建两个棋盘?
假设这里只创建一个棋盘,那么这一个棋盘上就要放置3种信息。分别为雷、非雷、排查出周围雷的信息。虽然这种方法也没太大的问题,但是一个棋盘上放置3种信息会给之后打印棋盘时造成麻烦,不易打印。
所以我们创建两个二维数组,一个数组存放布置好雷的信息;另一个数组存放排查出的雷的信息。这样创建不仅方便之后的打印,也可以将问题分解从而让问题层次更加分明,解题思路更加清晰。
问题2:为什么使用宏常量?
其实这个问题在前面的三子棋游戏中也有介绍。在这里使用宏常量主要是为了方便程序的修改,增加了程序的可塑性。
就像我这里通过宏定义将行列通设定为9
,也就是9*9
的雷盘。假如后面我想要玩12*12
的雷盘,我只需要将宏定义中的9
改为12
即可,这样就省去了在程序中大量修改的精力,使代码可塑性更高。
问题3:为什么二位数组的行列选择ROWS、COLS
而不是ROW、COL
?
通过扫雷游戏规则我们已知,如果一个位置不是雷,我们要排查它周围8个坐标是不是雷,对于
9*9
的棋盘,当在排查边界时可能会出现越界的情况。因此我们创建11*11
的棋盘这样就很好的解决了这个问题。
那么怎么让用户看到9*9
的棋盘呀?其实用户看到的只是程序员想让你看到的,后面我们只需要打印11*11
中的9*9
就ok了。
代码展示:
void InitBoard(char board[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++)
{
board[i][j] = set;
}
}
}
这里就是简单使用两个for循环遍历整个棋盘从而实现棋盘初始化,这里就不在过多赘述。
补充:这里是将mine[ROWS][COLS]
中的内容初始化为‘0’
,show[ROWS][COLS]
中内容初始化为‘*’
代码展示:
//打印棋盘
DisPlayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 1;
int j = 1;
printf("------扫雷游戏------\n");
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
}
简单观察不难发现,上述棋盘使用户查找坐标时变得困难,不易于用户输入排雷坐标 ,因此我们可以通过下面简单优化,为棋盘标号:
优化后代码:
//打印棋盘
DisPlayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 1;
int j = 1;
printf("------扫雷游戏------\n");
//打印列号
for (i = 0; i <= col; i++)
{
printf("%d ",i);
}
printf("\n");
for (i = 1; i <= row; i++)
{//打印行号
printf("%d ",i);
for (j = 1; j <= col; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
}
优化后效果展示:
这样以来雷盘坐标是不是就变得更加直观清楚了 !
代码展示:
#define EASY_COUNT 10
void SetMine(char mine[ROWS][COLS], int row, int col)
{
//布置10个雷
int count = EASY_COUNT;
while (count)
{
//生产随机的下标
int x = rand()%row+1;
int y = rand()%col+1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
1、这里使用宏定义布置雷的个数,便于修改,增加代码的可塑性。
2、rand()
函数与srand((unsigned int)time(NULL))
函数配合生成随机数,rand()%row+1
与rand()%col+1
保证了生成了横纵坐标在合理范围之内。
补充:这里规定雷为'1'
1.排雷原理
在扫雷的时候我们还需要定义一个函数,用来返回排查坐标周围地雷的个数。
下面用一张图来解析排查原理:
假设排查坐标为(
x
,y
),我们可以如右图,依次返回其周围8
个坐标下对应的值,由于我们上面规定,雷为‘1’
,非雷为‘0’
,则字符相加减对应ASCLL码值相加减。
例如:(x
,y
)周围有1个雷,则7*'0'+'1'-8*'0'
即表示‘1’的ASCLL码值减‘0’的ASCLL码值,返回整数1
,即周围有1个雷
方案一:逐个遍历
代码展示:
//返回排查坐标周围雷的数量
int get_mine_count(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';
}
方案二:循环遍历
代码展示:
int get_mine_count2(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int count = 0;
for (i = -1; i <= 1; i++)
{
int j = 0;
for (j = -1; j <= 1; j++)
{
if (mine[x + i][y + j] == '1')
count++;
}
}
return count;
}
2.扫雷流程+判断输赢
设计思路:
1.输入排查的坐标
2.判断坐标合法性
(1)合法 -程序执行下一步
(2)不合法 -重新输入
3.检查坐标处是不是雷
(1)是雷 - 很遗憾炸死了 - 游戏结束
(2)不是雷 - 统计坐标周围有几个雷 - 存储排查雷的信息到show数组,游戏继续
程序执行过程如下图:
代码展示:
注意:
row*col- EASY_COUNT
表示所有非雷数量,所有非雷坐标全部排查则游戏正常结束,玩家胜利。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{ //思路:
//1. 输入排查的坐标
//2、判断坐标合法性
//3. 检查坐标处是不是雷
// (1) 是雷 - 很遗憾炸死了 - 游戏结束
// (2) 不是雷 - 统计坐标周围有几个雷 - 存储排查雷的信息到show数组,游戏继续
int x = 0;
int y = 0;
int win = 0;
while (win<row*col- EASY_COUNT)
{
printf("请输入要排查的坐标:>");
scanf("%d%d", &x, &y);//x--(1,9) y--(1,9)
//判断坐标的合法性
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, row, col);
break;
}
else
{
//不是雷情况下,统计x,y坐标周围有几个雷
int count = get_mine_count(mine, x, y);
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);//向玩家展示一下布雷棋盘
}
}
到这里初级扫雷游戏就搭建完成了,我们来试玩一把:
1.上述代码每次排雷只展开一个坐标,排雷效率较低.
2.没有标记功能,缺少趣味性.
显然,初级扫雷虽然能实现扫雷游戏的基本功能,但是缺少展开功能和标记功能,扫雷仿佛没了灵魂,那么我们应该如何画龙点睛,让扫雷如虎添翼,更上一层呢?下面我们就针对游戏缺陷进一步改进!
如果没有思路,我们可以先观察一下游戏中的展开过程:
聪明的你们一定会发现:当排雷坐标周围处地雷数量为
0
时,棋盘会向周围展开,并且将周围雷的数量显示到屏幕上,直到坐标周围地雷数不为0
。
显然,这是一个递归问题,当排雷坐标周围地雷数为0
,棋盘会继续遍历周围个坐标,直到坐标周围出现雷,递归停止。
代码展示:
static void Digit_boom(char show[ROWS][COLS], char mine[ROWS][COLS], int x, int y, int row, int col, int* win)
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标合法性
{
int ret = Around_num2(mine, x, y);//接受坐标周围雷的数量
if (ret == 0)//递归条件--周围雷数为0
{
(*win)++;//每排查一个坐标,排查次数加1,为判断输赢做准备
show[x][y] = '0';//显示周围雷数
int i = 0;
int j = 0;
//用两个循环遍历周围8个坐标
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (show[x + i][y + j] == '*')//递归的坐标必须是未排查过的坐标,
//防止死递归
{
Digit_boom(show, mine, x + i, y + j, row, col,win);
}
}
}
}
else
{ //条件不满足退出递归
(*win)++;//排查坐标,次数加1
show[x][y] = ret + '0';//显示周围雷数
}
}
}
1、递归参数
win
这里为传址调用,作用为计数器,统计排雷次数,方便之后判断输赢。
2、注意此递归有两个条件:
条件一:if (ret == 0)
为大前提,只有周围坐标为0才可能进入递归
条件二:if(show[x+i][y+j]=='*')
,为小前提,进入递归的坐标必须是未排查过的坐标,否则可能重复排查坐标,出现死递归
效果展示:
标记功能,作用是把玩家确定的雷坐标标记出来,或者是把不确定的雷坐标标记出来。它的作用只是做记号,相比于递归展开,标记功能显然更容易实现。
由于是做标记,我们需要再引入一个标志符号,为了不与雷盘符号*
冲突,易于玩家区分,这里使用#
作为标记符号。
代码展示:(标记雷)
//标记雷
static void flag(char show[ROWS][COLS], int row, int col)
{
while (1)
{
int x = 0;
int y = 0;
printf("请输入标记坐标(输入:0 0退出):>");
scanf("%d %d",&x,&y);
if (x >= 1 && x <= row && y >= 0 && y <= col)
{
if (show[x][y] != '#')
{
show[x][y] = '#';
break;
}
else
{
printf("该坐标已经标记,请重新选择\n");
}
}
else
{
printf("坐标越界,请重新输入\n");
}
if (x == y && x == 0)
break;
}
}
代码展示:(取消标记)
static void cancel_flag(char show[ROWS][COLS], int row, int col)
{
while (1)
{
int x = 0;
int y = 0;
printf("请输入取消标记坐标(输入:0 0退出):>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 0 && y <= col)
{
if (show[x][y] == '#')//比较用双等号!!!
{
show[x][y] = '*';
break;
}
else
{
printf("该坐标还未标记,请重新选择\n");
}
}
else
{
printf("坐标越界,请重新输入\n");
}
if (x == y && x == 0)
break;
}
}
当我们添加了这些功能后,为了能够有更好的游戏体验我们可以为这些功能添加一个菜单,如下图:
代码展示:
static void menu2()
{
printf("┌-----------------------------┐\n");
printf("├**********1.排雷*************┤\n");
printf("├**********2.标记*************┤\n");
printf("├**********3.取消标记*********┤\n");
printf("└-----------------------------┘\n");
}
在实现了上述功能后,我们可以重新封装排雷函数FindMine()
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int choice = -1;
int x = 0;
int y = 0;
int win = 0;
while (win < col * row - Easy_Count)
{
menu2();
printf("请选择:>");
scanf("%d", &choice);
//标记
if (2 == choice)
{
flag(show, ROW, COL);
DisplayBoard(show, ROW, COL);
}
//取消标记
else if (3 == choice)
{
cancel_flag(show, ROW, COL);
DisplayBoard(show, ROW, COL);
}
//排雷
else if(1==choice)
{
printf("请输入排查坐标:>");
scanf("%d%d", &x, &y);
if (x >= 0 && x <= row && y >= 0 && y <= col)
{
if (mine[x][y] == '1')
{
DisplayBoard(mine, ROW, COL);
printf("很遗憾,你被炸死了!\n");
break;
}
else if(show[x][y]=='*')
{
Digit_boom(show, mine, x, y, row, col, &win);
DisplayBoard(show, row, col);
}
else
{
printf("该坐标已排查,请重新选择\n");
}
}
else
{
printf("输入坐标错误,请重新输入\n");
}
}
else
{
printf("选择错误,请重新选择\n");
}
}
if (win == col * row - Easy_Count)//非雷坐标全部排查完毕
{
DisplayBoard(mine, ROW, COL);
printf("恭喜你,你赢了咯!\n");
}
}
最终效果展示:
为了让扫雷界面更简洁,在每次打印雷盘前增加了清屏操作:
system("cls");
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//游戏菜单
menu()
{
printf("* * * * * * * * * * * * * * * * * *\n");
printf("* * * * * * 1.扫雷游戏 * * * * * *\n");
printf("* * * * * * 0.退出游戏 * * * * * *\n");
printf("* * * * * * * * * * * * * * * * * *\n");
}
//扫雷游戏
void game()
{
//创建扫雷棋盘
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化扫雷棋盘
Init(mine, ROWS, COLS, '0');
Init(show, ROWS, COLS, '*');
//布置雷
SetMine(mine, ROW, COL);
//打印扫雷棋盘
DisplayBoard(show, ROW, COL);
//开始扫雷并判断输赢
FindMine(mine, show, ROW, COL);
}
//交互界面
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
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;
}
#pragma once
//包含头文件
#include
#include
#include
#include
#include
//宏定义
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define Easy_Count 10
//初始化雷盘
void Init(char board[ROWS][COLS],int rows,int cols,char x);
//打印雷盘
void DisplayBoard(char board[ROWS][COLS],int row,int col);
//布置雷
void SetMine(char mine[ROWS][COLS],int row,int col);
//排雷并判断输赢
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//初始化雷盘
void Init(char board[ROWS][COLS], int rows, int cols, char x)
{
int i = 0;
for (i=0;i<rows;i++)
{
int j = 0;
for (j=0;j<cols;j++)
{
board[i][j] = x;
}
}
}
//打印雷盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
system("cls");
printf("-----扫雷游戏-----\n");
int i = 0;
int j = 0;
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 ",board[i][j]);
}
printf("\n");
}
}
//布置雷
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = Easy_Count;
while (count)
{
int i = rand() % row + 1;
int j = rand() % col + 1;
if (mine[i][j] == '0')
{
mine[i][j] = '1';
count--;
}
}
}
//显示周围雷数
//方案一
static int Around_num1(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y] + mine[x - 1][y - 1] +
mine[x - 1][y + 1] + mine[x][y - 1] +
mine[x][y + 1] + mine[x + 1][y] +
mine[x + 1][y - 1] + mine[x + 1][y + 1]-8*'0';
}
//方案二
static int Around_num2(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int count = 0;
for (i = -1; i <= 1; i++)
{
int j = 0;
for (j = -1; j <= 1; j++)
{
if (mine[x + i][y + j] == '1')
count++;
}
}
return count;
}
//标记雷
static void flag(char show[ROWS][COLS], int row, int col)
{
while (1)
{
int x = 0;
int y = 0;
printf("请输入标记坐标(输入:0 0退出):>");
scanf("%d %d",&x,&y);
if (x >= 1 && x <= row && y >= 0 && y <= col)
{
if (show[x][y] != '#')
{
show[x][y] = '#';
break;
}
else
{
printf("该坐标已经标记,请重新选择\n");
}
}
else
{
printf("坐标越界,请重新输入\n");
}
if (x == y && x == 0)
break;
}
}
//取消标记
static void cancel_flag(char show[ROWS][COLS], int row, int col)
{
while (1)
{
int x = 0;
int y = 0;
printf("请输入取消标记坐标(输入:0 0退出):>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 0 && y <= col)
{
if (show[x][y] == '#')//比较用双等号!!!
{
show[x][y] = '*';
break;
}
else
{
printf("该坐标还未标记,请重新选择\n");
}
}
else
{
printf("坐标越界,请重新输入\n");
}
if (x == y && x == 0)
break;
}
}
//如果排雷坐标周围雷数为零,递归展开
static void Digit_boom(char show[ROWS][COLS], char mine[ROWS][COLS], int x, int y, int row, int col, int* win)
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标合法性
{
int ret = Around_num2(mine, x, y);//接受坐标周围雷的数量
if (ret == 0)//递归条件--周围雷数为0
{
(*win)++;//每排查一个坐标,排查次数加1,为判断输赢做准备
show[x][y] = '0';
int i = 0;
int j = 0;
//用两个循环遍历周围8个坐标
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (show[x + i][y + j] == '*')//递归的坐标必须是未排查过的坐标,防止死递归
{
Digit_boom(show, mine, x + i, y + j, row, col,win);
}
}
}
}
else
{
//条件不满足退出递归
(*win)++;//排查坐标,次数加1
show[x][y] = ret + '0';
}
}
}
//扫雷菜单
static void menu2()
{
printf("┌-----------------------------┐\n");
printf("├**********1.排雷*************┤\n");
printf("├**********2.标记*************┤\n");
printf("├**********3.取消标记*********┤\n");
printf("└-----------------------------┘\n");
}
//扫雷并判断输赢
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int choice = -1;
int x = 0;
int y = 0;
int win = 0;
while (win < col * row - Easy_Count)
{
menu2();
printf("请选择:>");
scanf("%d", &choice);
if (2 == choice)
{
flag(show, ROW, COL);
DisplayBoard(show, ROW, COL);
}
else if (3 == choice)
{
cancel_flag(show, ROW, COL);
DisplayBoard(show, ROW, COL);
}
else if(1==choice)
{
printf("请输入排查坐标:>");
scanf("%d%d", &x, &y);
if (x >= 0 && x <= row && y >= 0 && y <= col)
{
if (mine[x][y] == '1')
{
DisplayBoard(mine, ROW, COL);
printf("很遗憾,你被炸死了!\n");
break;
}
else if(show[x][y]=='*')
{
Digit_boom(show, mine, x, y, row, col, &win);
DisplayBoard(show, row, col);
}
else
{
printf("该坐标已排查,请重新选择\n");
}
}
else
{
printf("输入坐标错误,请重新输入\n");
}
}
else
{
printf("选择错误,请重新选择\n");
}
}
if (win == col * row - Easy_Count)
{
DisplayBoard(mine, ROW, COL);
printf("恭喜你,你赢了咯!\n");
}
}
由于作者水平有限,如笔下有误,敬请留言。
如果本文对您有所帮助,请给博主点赞关注哦,笔者会持续更新干货教程,期待与君共勉!