C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章目录
- 前言
- 一、问题描述
- 二、基本框架构思
- 三、具体实现
-
- 1.扫雷接口实现
- 2.地图初始化
- 3.设置雷
- 4.显示界面
- 5.开始扫雷
- 6.计算周围雷的数量
- 7.排查雷
- 8.空白展开
- 9.标记雷
- 10.取消标记
- 四、结果演示
- 五、完整代码
- 总结
前言 扫雷,是一个十分经典的小游戏,相信大家小时候都玩过,在实现过程中你将会有很大的成就感,现在就让我们一起来实现它吧,。
一、问题描述 实现除界面外扫雷游戏的所有功能。包括
实现一个简单的界面
实现排查雷的功能
实现标记雷的功能
实现显示周围雷的数量
实现第一次排查不会遇雷
实现如果周围没有雷则展开一片
文章图片
二、基本框架构思 在开始编写代码之前,我们必须去玩一玩扫雷,熟悉一下它的各种规则和机制,有助于我们形成更加清晰的代码逻辑。同时在写完一个功能后,进行测试并与真实的扫雷功能做对比。为了编写代码,我玩了十余次。
在试玩过后,我们首先想到的便是界面如何展示,雷如何设置。对于二维平面,我们最常用的就是数组,在数组里面存放不同的数据,来表示有无雷。
- 假设我们用一个二维数组,用数据0表示没有雷,1表示有雷,但是当我们排查一个点之后需要显示周围雷的数量,假设也为1,那就会产生冲突,同时也不方便统计周围雷的数量。
- 还有一个问题数组容易出界,没次访问都需要判断,比较麻烦,容易出错。
- 我们使用两个数组,一个表示有无雷,一个展示给用户看,未排查用‘ *
’表示,已排查用周围雷的数量表示。为了统一,我们使用字符数组,遇到整数时将其转化为字符存放。
- 我们可以只使用设定数组的内圈部分,即最外圈不再使用,用于判断。
文章图片
接下来让我们来实现主函数,简单菜单
#define _CRT_SECURE_NO_WARNINGS 1void Menu()
{ printf("********************************\n");
printf("*********1. play********\n");
printf("*********0. exit********\n");
printf("********************************\n");
}
int main()
{ int input = 0;
int count = 0;
srand((unsigned int)time(NULL));
do
{Menu();
//清除缓冲区,第一次不用
if(count!=0)
{char ch;
while ((ch = getchar()) != EOF && ch != '\n')
{;
}
}
count++;
printf("请选择:>");
scanf("%d", &input);
switch (input)
{case 1:
//将标记正确的数量重置为0
mark_count = 0;
MineSweeper();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
} while (input);
return 0;
}
设置清除缓冲区代码的目的:
防止输入一个有效数字但带有空格,后面又输入一个数字存放在缓冲区,导致下一次直接取缓冲区的数字,不符合本意。
如图:
文章图片
加上之后:
文章图片
三、具体实现 1.扫雷接口实现 先定义相关宏定义,方面后面修改
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include
#include
#include#define ROW 9//扫雷地图的数组的行
#define COL 9//扫雷地图的数组的行#define ROWS 11//真实数组的行,为了方便查找雷的数量,这样的话不用判断访问数组是否越界
#define COLS 11//真实数组的行,多加一圈#define MINE_COUNT 9 //设置雷的数量//设置全局变量
int find_count;
//用于判断是否为第一次排雷,防止第一次被诈
int mark_count;
//标记正确的数量
游戏接口,调用各函数实现扫雷。
void MineSweeper()
{ //为了统一符号,使用字符数组 char mine[ROWS][COLS] = {
0 };
//用于存放雷的数组,0表示没有雷,1表示有雷
char show[ROWS][COLS] = {
0 };
//用于存放打印给用户看的该点周围雷的数量的数组,
//默认为*,输入坐标后其内容为周围雷的数量
// !为标记点
//数组初始化
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//设置雷
SetMine(mine, ROW, COL);
system("cls");
//调试用,可以查看雷的位置
//DisplayBoard(mine, ROW, COL);
//显示数组版面
DisplayBoard(show, ROW, COL);
//游戏入口
PlayGame(mine, show, ROW, COL);
}
2.地图初始化 因为两个数组最初都是只存放一种字符,’ 0 ‘和’ * ',所有可以直接把字符当作参数传入,这样就可以通用一个函数了。
//数组初始化
void InitBoard(char board[][COLS], int row, int col, char ch)
{ for (int i = 0;
i < row;
i++)
{for (int j = 0;
j < col;
j++)
{board[i][j] = ch;
}
}
}
3.设置雷 使用随机函数设置雷,即将mine数组的部分随机设置成’ 1 ',需要注意的是,我们只针对数组内部真正有效的部分,最外一圈不管,所以遍历是从1开始。
//设置雷
void SetMine(char board[][COLS], int row, int col)
{ int count = MINE_COUNT;
while (count)
{//随机获得雷的坐标
int x = rand() % row + 1;
//从1到row-1
int y = rand() % col + 1;
//判断是否已经是雷
if (board[x][y] != '1')
{//不是雷,就放
board[x][y] = '1';
count--;
}
}
}
4.显示界面 打印传入的数组,并打印行号,注意只打印数组内部,遍历从1开始。
如果做了标记用’ ! '表示。
//显示数组版面
void DisplayBoard(char board[][COLS], int row, int col)
{ printf("------------------------\n");
printf("");
for (int i = 1;
i <= col;
i++)
{printf("%2d", i);
}
printf("\n");
for (int i = 1;
i <= row;
i++)
{printf("%2d", i);
for (int j = 1;
j <= col;
j++)
{printf(" %c", board[i][j]);
}
printf("\n");
}
printf("------------------------\n");
}
5.开始扫雷 排查过的数量等于不是雷的数量 或者 标记正确雷的数量等于设置雷的数量结束,排雷成功。
每次选择执行完后打印显示(show)数组。
//游戏入口,选择排查或者标记
void PlayGame(char mine[][COLS], char show[][COLS], int row, int col)
{ int win_count = 0;
//排查过的数量 //排查过的数量等于‘0’的数量的时候 或者 标记正确雷的数量等于设置的雷数量结束,排雷成功
while (win_count < (row * col - MINE_COUNT) && mark_count < MINE_COUNT)
{printf("################################\n");
printf("#########1. 排查雷########\n");
printf("#########2. 标记雷########\n");
printf("#########3. 取消标记 ########\n");
printf("################################\n");
int choice;
printf("请选择:>");
//清除缓冲区
char ch;
while ((ch = getchar()) != EOF && ch != '\n')
{;
}
scanf("%d", &choice);
if (choice != 1 && choice != 2 && choice != 3)
{printf("输入错误,请重新输入\n");
//跳过本次循环
continue;
}
if (choice == 1)
{//排查雷
int judge = FindMine(mine, show, row, col, &win_count);
if (!judge)
{//被雷炸死,打印藏雷的数组,并结束
DisplayBoard(mine, row, col);
return;
}
}
else
{if (choice == 2)
{//标记雷
MarkMine(mine, show, row, col);
system("cls");
DisplayBoard(show, row, col);
}
else
{//取消标记雷
CancelMark(mine, show, row, col);
system("cls");
DisplayBoard(show, row, col);
}
} }
if (win_count == row * col - MINE_COUNT||mark_count==MINE_COUNT)
{system("cls");
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
return;
}
}
6.计算周围雷的数量 统计八个方向,上、下、左、右、左上,左下,右上,右下为雷的数量,因为存放的是字符,所有相加后得减去’ 0 ',即可得到整数。
//查找周围雷的数量
int GetMineCount(char mine[][COLS], int x, int y)
{ return (mine[x - 1][y] +
mine[x + 1][y] +
mine[x][y - 1] +
mine[x][y + 1] +
mine[x - 1][y - 1] +
mine[x + 1][y - 1] +
mine[x - 1][y + 1] +
mine[x + 1][y + 1] - 8 * '0');
}
7.排查雷 排查雷需要设置第一次不被炸死,同时如果被标记则不能再排查。当被炸死或者输入坐标错误则返回。
//排查雷
//返回0代表结束,返回1代表继续
int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin)
{ int x, y;
printf("请输入想要排查的坐标:>");
scanf("%d %d", &x, &y);
//如果是第一次,重新设置后如果还是雷则继续循环
while (find_count == 0)
{if (mine[x][y] == '1')//是雷
{//现将mine数组置空,即初始化
InitBoard(mine, row, col, '0');
//重新布雷
SetMine(mine, row, col);
}
else
{break;
}
}
if (x >= 1 && x <= row && y >= 1 && y <= col)
{if (mine[x][y] == '0')//不是雷
{//如果已经标记,则不能排查
if (show[x][y] == '!')
{printf("该点已经被标记,请重新输入\n");
return 1;
}
system("cls");
SpreadBlank(mine, show, x, y, pwin);
DisplayBoard(show, row, col);
}
else
{printf("很遗憾,你被炸死了!\n");
return 0;
}
}
else
{printf("输入坐标错误,请重新输入\n");
return 1;
}
}
标记后不能再被排查:
文章图片
第一次不会被炸死:
文章图片
选择之后:
文章图片
8.空白展开 目的:如果周围没有雷则继续展开(递归),遇到雷停止
这是较难实现的一个函数,需要使用递归实现,而使用递归就需要确定递归的终止条件,这里有三个
- 对最外层一圈不做计算,直接返回,这就是多设置一圈的好处
- 如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出。
- 如果周围有雷就停止。
//如果周围没有雷,则全部展开
//展开空白区域
void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin)
{ //对最外层一圈不做计算,直接返回,这就是多设置一圈的好处
if (x==0||y==0||x==ROWS-1||y==COLS-1)
return;
//如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出
if (show[x][y] != '*')
return;
int count = GetMineCount(mine, x, y);
if (count > 0)
{show[x][y] = count + '0';
//增加排查数量
(*pwin)++;
return ;
}
else
{//八个方向,上、下、左、右、左上,左下,右上,右下show[x][y] = '0';
//增加排查数量
(*pwin)++;
SpreadBlank(mine, show, x - 1, y, pwin);
SpreadBlank(mine, show, x + 1, y, pwin);
SpreadBlank(mine, show, x, y - 1, pwin);
SpreadBlank(mine, show, x, y + 1, pwin);
SpreadBlank(mine, show, x - 1, y - 1, pwin);
SpreadBlank(mine, show, x + 1, y - 1, pwin);
SpreadBlank(mine, show, x - 1, y + 1, pwin);
SpreadBlank(mine, show, x + 1, y + 1, pwin);
}
}
9.标记雷 用’ ! '为标记符号。如果正确标记雷点,则标记正确数+1
//标记雷点
void MarkMine(char mine[][COLS], char show[][COLS], int row, int col)
{ int x;
int y;
printf("请输入想要标记的坐标:>");
scanf("%d%d", &x, &y);
//该点需未被探查,即在show数组为‘*’的点
if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='*')
{if (mine[x][y] == '1')
{//正确标记雷点
mark_count++;
}
show[x][y] = '!';
}
else
{printf("输入坐标错误,请重新输入\n");
}}
10.取消标记 与标记类似,只需把标记点改为’ * '即可。如果该点本是正确标记雷点,则标记正确数-1
//取消标记雷点
void CancelMark(char mine[][COLS], char show[][COLS], int row, int col)
{ int x;
int y;
printf("请输入想要取消标记的坐标:>");
scanf("%d%d", &x, &y);
//该点需已被标记
if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='!')
{if (mine[x][y] == '1')
{//如果是正确标记雷点,数量减一
mark_count--;
}
show[x][y] = '*';
}
else
{printf("输入坐标错误或者不是被标记点,请重新输入\n");
}}
四、结果演示 我们将雷的数量设置为1,同时展示mine数组,让我们知道雷的位置,方便测试。
#define MINE_COUNT 1 //设置雷的数量
DisplayBoard(mine, ROW, COL);
文章图片
文章图片
展开演示:
文章图片
全部展开,排雷成功。
文章图片
当然也有下面种情况。
文章图片
五、完整代码 MineSweeper.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include
#include
#include#define ROW 9//扫雷地图的数组的行
#define COL 9//扫雷地图的数组的行#define ROWS 11//真实数组的行,为了方便查找类的数量,这样的话不用判断访问数组是否越界
#define COLS 11//真实数组的行,多加一圈#define MINE_COUNT 1 //设置雷的数量int find_count;
//用于判断是否为第一次排雷,防止第一次被诈
int mark_count;
//标记正确的数量//数组初始化
void InitBoard(char board[][COLS], int row, int col, char ch);
//显示数组版面
void DisplayBoard(char board[][COLS], int row, int col);
//设置雷
void SetMine(char board[][COLS], int row, int col);
//游戏入口,选择排查或者标记
void PlayGame(char mine[][COLS], char show[][COLS], int row, int col);
//排查雷
//返回0代表结束,返回1代表继续
int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin);
//查找周围雷的数量
int GetMineCount(char mine[][COLS], int x, int y);
//如果周围没有雷,则全部展开
//展开空白区域
void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin);
//标记雷点
void MarkMine(char mine[][COLS], char show[][COLS], int row, int col);
//取消标记雷点
void CancelMark(char mine[][COLS], char show[][COLS], int row, int col);
MineSweeper.c
#pragma once#include"MineSweeper.h"//数组初始化
void InitBoard(char board[][COLS], int row, int col, char ch)
{ for (int i = 0;
i < row;
i++)
{for (int j = 0;
j < col;
j++)
{board[i][j] = ch;
}
}
}//显示数组版面
void DisplayBoard(char board[][COLS], int row, int col)
{ printf("------------------------\n");
printf("");
for (int i = 1;
i <= col;
i++)
{printf("%2d", i);
}
printf("\n");
for (int i = 1;
i <= row;
i++)
{printf("%2d", i);
for (int j = 1;
j <= col;
j++)
{printf(" %c", board[i][j]);
}
printf("\n");
}
printf("------------------------\n");
}//设置雷
void SetMine(char board[][COLS], int row, int col)
{ int count = MINE_COUNT;
while (count)
{//随机获得雷的坐标
int x = rand() % row + 1;
int y = rand() % col + 1;
//判断是否已经是雷
if (board[x][y] != '1')
{//不是雷,就放
board[x][y] = '1';
count--;
}
}
}//游戏入口,选择排查或者标记
void PlayGame(char mine[][COLS], char show[][COLS], int row, int col)
{ int win_count = 0;
//排查过的数量 //排查过的数量等于‘0’的数量的时候 或者 标记正确雷的数量等于设置的雷数量结束,排雷成功
while (win_count < (row * col - MINE_COUNT) && mark_count < MINE_COUNT)
{printf("################################\n");
printf("#########1. 排查雷########\n");
printf("#########2. 标记雷########\n");
printf("#########3. 取消标记 ########\n");
printf("################################\n");
int choice;
printf("请选择:>");
//清除缓冲区
char ch;
while ((ch = getchar()) != EOF && ch != '\n')
{;
}
scanf("%d", &choice);
if (choice != 1 && choice != 2 && choice != 3)
{printf("输入错误,请重新输入\n");
//跳过本次循环
continue;
}
if (choice == 1)
{//排查雷
int judge = FindMine(mine, show, row, col, &win_count);
if (!judge)
{//被雷炸死,打印藏雷的数组,并结束
DisplayBoard(mine, row, col);
return;
}
}
else
{if (choice == 2)
{//标记雷
MarkMine(mine, show, row, col);
system("cls");
DisplayBoard(show, row, col);
}
else
{//取消标记雷
CancelMark(mine, show, row, col);
system("cls");
DisplayBoard(show, row, col);
}
} }
if (win_count == row * col - MINE_COUNT||mark_count==MINE_COUNT)
{system("cls");
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
return;
}
}//排查雷
//返回0代表结束,返回1代表继续
int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin)
{ int x, y;
printf("请输入想要排查的坐标:>");
scanf("%d %d", &x, &y);
//如果是第一次
while (find_count == 0)
{if (mine[x][y] == '1')//是雷
{//现将mine数组置空,即初始化
InitBoard(mine, row, col, '0');
//重新布雷
SetMine(mine, row, col);
}
else
{break;
}
}
if (x >= 1 && x <= row && y >= 1 && y <= col)
{if (mine[x][y] == '0')//不是雷
{//如果已经标记,则不能排查
if (show[x][y] == '!')
{printf("该点已经被标记,请重新输入\n");
return 1;
}//int count = GetMineCount(mine, x, y);
//show[x][y] = count + '0';
system("cls");
SpreadBlank(mine, show, x, y, pwin);
DisplayBoard(show, row, col);
//win++;
}
else
{printf("很遗憾,你被炸死了!\n");
return 0;
}
}
else
{printf("输入坐标错误,请重新输入\n");
return 1;
}
}//查找周围雷的数量
int GetMineCount(char mine[][COLS], int x, int y)
{ return (mine[x - 1][y] +
mine[x + 1][y] +
mine[x][y - 1] +
mine[x][y + 1] +
mine[x - 1][y - 1] +
mine[x + 1][y - 1] +
mine[x - 1][y + 1] +
mine[x + 1][y + 1] - 8 * '0');
}//如果周围没有雷,则全部展开
//展开空白区域
void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin)
{ //对最外层一圈不做计算
if (x==0||y==0||x==ROWS-1||y==COLS-1)
return;
//如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出
if (show[x][y] != '*')
return;
int count = GetMineCount(mine, x, y);
if (count > 0)
{show[x][y] = count + '0';
//增加排查数量
(*pwin)++;
return ;
}
else
{//mine[x][y] = '2';
//八个方向,上、下、左、右、左上,左下,右上,右下show[x][y] = '0';
//增加排查数量
(*pwin)++;
SpreadBlank(mine, show, x - 1, y, pwin);
SpreadBlank(mine, show, x + 1, y, pwin);
SpreadBlank(mine, show, x, y - 1, pwin);
SpreadBlank(mine, show, x, y + 1, pwin);
SpreadBlank(mine, show, x - 1, y - 1, pwin);
SpreadBlank(mine, show, x + 1, y - 1, pwin);
SpreadBlank(mine, show, x - 1, y + 1, pwin);
SpreadBlank(mine, show, x + 1, y + 1, pwin);
}
}//标记雷点
void MarkMine(char mine[][COLS], char show[][COLS], int row, int col)
{ int x;
int y;
printf("请输入想要标记的坐标:>");
scanf("%d%d", &x, &y);
//该点需未被探查,即在show数组为‘*’的点
if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='*')
{if (mine[x][y] == '1')
{//正确标记雷点
mark_count++;
}
show[x][y] = '!';
}
else
{printf("输入坐标错误,请重新输入\n");
}}//取消标记雷点
void CancelMark(char mine[][COLS], char show[][COLS], int row, int col)
{ int x;
int y;
printf("请输入想要取消标记的坐标:>");
scanf("%d%d", &x, &y);
//该点需已被标记
if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='!')
{if (mine[x][y] == '1')
{//如果是正确标记雷点,数量减一
mark_count--;
}
show[x][y] = '*';
}
else
{printf("输入坐标错误或者不是被标记点,请重新输入\n");
}}
【C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析】MineSweeperTest.c
#define _CRT_SECURE_NO_WARNINGS 1#include"MineSweeper.h"void Menu()
{ printf("********************************\n");
printf("*********1. play********\n");
printf("*********0. exit********\n");
printf("********************************\n");
}void MineSweeper()
{ //为了统一符号,使用字符数组 char mine[ROWS][COLS] = {
0 };
//用于存放雷的数组,0表示没有雷,1表示有雷
char show[ROWS][COLS] = {
0 };
//用于存放打印给用户看的该点周围雷的数量的数组,
//默认为*,输入坐标后其内容为周围雷的数量
// !为标记点
//数组初始化
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//设置雷
SetMine(mine, ROW, COL);
system("cls");
//调试用
DisplayBoard(mine, ROW, COL);
//显示数组版面
DisplayBoard(show, ROW, COL);
//游戏入口
PlayGame(mine, show, ROW, COL);
}int main()
{ int input = 0;
int count = 0;
srand((unsigned int)time(NULL));
do
{Menu();
//清除缓冲区,第一次不用
if(count!=0)
{char ch;
while ((ch = getchar()) != EOF && ch != '\n')
{;
}
}
count++;
printf("请选择:>");
scanf("%d", &input);
switch (input)
{case 1:
//将标记正确的数量重置为0
mark_count = 0;
MineSweeper();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
} while (input);
return 0;
}
总结 对扫雷的完善,将其除界面外全部功能实现,其中最难的一点是展开空白区域,因为涉及递归,并且在数组受限条件较多,需要多次调试。如有错误,欢迎大佬指正。
推荐阅读
- 7.9号工作总结~司硕
- 【生信技能树】R语言练习题|【生信技能树】R语言练习题 - 中级
- 小学英语必考的10个知识点归纳,复习必备!
- 一起来学习C语言的字符串转换函数
- C语言字符函数中的isalnum()和iscntrl()你都知道吗
- C语言浮点函数中的modf和fmod详解
- C语言中的时间函数clock()和time()你都了解吗
- 最有效的时间管理工具(赢效率手册和总结笔记)
- C语言学习|第十一届蓝桥杯省赛 大学B组 C/C++ 第一场
- 数据库总结语句