文章目录
- 前言
- 一、问题描述
- 二、基本框架构思
- 三、具体实现
- 1.扫雷接口实现
- 2.地图初始化
- 3.设置雷
- 4.显示界面
- 5.开始扫雷
- 6.计算周围雷的数量
- 7.排查雷
- 8.空白展开
- 9.标记雷
- 10.取消标记
- 四、结果演示
- 五、完整代码
- 总结
前言
扫雷,是一个十分经典的小游戏,相信大家小时候都玩过,在实现过程中你将会有很大的成就感,现在就让我们一起来实现它吧,。
一、问题描述
实现除界面外扫雷游戏的所有功能。包括
实现一个简单的界面
实现排查雷的功能
实现标记雷的功能
实现显示周围雷的数量
实现第一次排查不会遇雷
实现如果周围没有雷则展开一片
二、基本框架构思
在开始编写代码之前,我们必须去玩一玩扫雷,熟悉一下它的各种规则和机制,有助于我们形成更加清晰的代码逻辑。同时在写完一个功能后,进行测试并与真实的扫雷功能做对比。为了编写代码,我玩了十余次。
在试玩过后,我们首先想到的便是界面如何展示,雷如何设置。对于二维平面,我们最常用的就是数组,在数组里面存放不同的数据,来表示有无雷。
- 假设我们用一个二维数组,用数据0表示没有雷,1表示有雷,但是当我们排查一个点之后需要显示周围雷的数量,假设也为1,那就会产生冲突,同时也不方便统计周围雷的数量。
- 还有一个问题数组容易出界,没次访问都需要判断,比较麻烦,容易出错。
解决办法:
-
我们使用两个数组,一个表示有无雷,一个展示给用户看,未排查用‘ *
’表示,已排查用周围雷的数量表示。为了统一,我们使用字符数组,遇到整数时将其转化为字符存放。 -
我们可以只使用设定数组的内圈部分,即最外圈不再使用,用于判断。
如下图:
接下来让我们来实现主函数,简单菜单
#define _CRT_SECURE_NO_WARNINGS 1
void 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<stdio.h>
#include<time.h>
#include<stdlib.h>
#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<stdio.h>
#include<time.h>
#include<stdlib.h>
#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");
}
}
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;
}