1.游戏说明
一.扫雷的游戏规则
扫雷是一款单人益智游戏,玩家需要通过翻开方块来避免触雷并揭示所有的非雷方块。以下是扫雷的规则:
游戏开始屏幕上(n*n)的方形网格,网格上方块开始均为初始状态。玩家选择一个方块时方块会有三种情况:
1.方块会显示数字(即在以此方块为中心的n*n区域内雷的数目)。
2方块会显示一颗雷(即游戏结束)。
3方块可被玩家标记(如果认为此方块是雷,玩家可以标记下来,方便记忆)。
玩家需要通过选择方块并根据方块上数字排查出所有的雷,而且不触碰雷,如果雷都被排查出来则游戏成功,排雷结束。
此文章为用C语言基础知识来写扫雷,所以只是扫雷的基础实现(用简单的代码实现简单的扫雷)。
2.扫雷网站(有兴趣的可以体验一下)
2.扫雷代码实现时使用内容
1.C语言rand()函数
扫雷过程中雷的出现是随机的我们就需要使用随机数函数来产生随机数。(本文章rand函数不是学习重点,所以只介绍基本用法)。
使用rand()函数需要头文件stdlib.h。time()函数需要头文件time.h
要产生n-m之间的随机数:
rand()%(n-m+1) + m;
比如获取15~43的随机数:
rand()%29+15;
但是rand()函数产生的随机数是伪随机数即每次产生的随机数相同。因为rand()产生随机数不仅受到我们设置条件影响,还被一个隐藏的全局变量seed控制。它实际上是根据我们给出的条件和一个种子按照某个公式推算出来的。
这时候需要我们引出srand()函数。每次系统调用rand()时会先检查是否主动调用srand函数设置种子,如果没有则使用默认种子(默认为1),种子相同每次伪随机数产生也是相同的。
srand函数:void srand( unsigned int seed );
所以我们只需让种子一直改变产生的伪随机数也就是不同的了,计算机中一直发生改变的量之一就是时间,所以我们可以把时间当做种子。可以使用time函数获得时间戳即从1970年1月1日0点到现在时间的秒数,将time函数强转为无符号整数放置到srand()中。这样每次获得的随机数也就是不同的了。即srand((unsigned int)time(NULL));
2.本文章代码运行建立在Visual Studio(使用其他编译器可跳过)
如果大家也是使用该软件,为了代码简洁会使用头文件(Minesweeper.h)和源文件(Minesweeper.c与text.c)。即
头文件当中写我们需要使用的头文件以及函数的声明,Mine.c文件写函数的具体实现,text.c文件写扫雷的主体即实现过程。
3.扫雷过程分析与代码实现
1.游戏主干(界面初始化)
在屏幕上打印出游戏开始与退出游戏的两个选项,玩家可以在键盘上选择是否开始显然可以使用switch函数。而且玩家可能会进行多次游戏。此时使用do-while函数就十分的合适(无论玩家会选择什么总是需要执行一次)。
打印两个选项(菜单menu)使用printf就可以轻松解决
void menu()
{
printf("************************************************\n");
printf("************ 1.start 0.exit ************\n");
printf("************************************************\n");
}
主函数:
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
//我们通过玩家输入1或者0判断游戏开始还是结束,
//这样的好处是选择0时while函数也会刚好结束。
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:game();
break;
case 0:printf("游戏结束\n");
break;
default:printf("非法输入,重新输入\n");
break;
}
} while (input);
return 0;
}
2.扫雷具体实现过程(game())
1.头文件的准备
头文件需要写我们需要的头文件,我们函数声明,以及我们创建的数组大小以及雷的数目是会改变的为了方便更改我们是用#define定义ROW代表横,COL代表列,我们创建9*9的二维数组10个雷为例子。
头文件代码展示:
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
//ROW代表行的大小,COL代表列的大小
#define ROW 9
#define COL 9
//ROWS代表我们实际需要的行的大小
//COLS代表我们实际需要的列的大小
#define ROWS ROW+2
#define COLS COL+2
//MINECOUNT代表雷的数目
#define MINECOUNT 10
//数组的初始化
void BoardInit(char str[ROWS][COLS], int rows, int cols, char set);
//数组的打印
void BoardPrint(char str[ROWS][COLS], int row, int col);
//雷的布置
void MineSet(char str[ROWS][COLS], int row, int col);
//排查雷
void MineFind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
注意,为了防止下面所说的问题,我们上面的函数传入的数组全部使用的是我们实际需要的数组即str[ROWS][COLS],数组传参的过程中我们也需要将我们需要的实际横纵坐标的大小(例如数组打印只需要打印ROW*COL的数组,初始化则需要全部初始化,则需要ROWS,COLS),因为在函数内部获取横纵坐标大小是很费劲的。
2.扫雷我们需要棋盘(即数组),那我们需要创建数组,思考一下第一想法应该是创建一个数组,用两个字符区分是否是雷,那我们还需要第三个字符代表未选择部分。这样可以但是实现的过程难免会十分麻烦而且实现的逻辑会不清晰。
既然一个数组使用起来会有些麻烦,所以我们就可以使用两个数组来帮助解决问题。一个数组是展示给玩家区分选择部分和未选择部分,一个数组放置雷与无雷,这样很好解决了上面所说的问题。
假如我们我们要实现9*9大小十个雷的扫雷游戏,是需要创建9*9大小的棋盘吗?答案是否定的,因为仔细思考一下,扫雷的过程是选择一个坐标检测该坐标是否是雷,是雷游戏结束,不是雷则统计以该坐标为中心的3*3矩阵的雷的个数,如果选择的坐标是9*9矩阵的边界坐标,统计3*3矩阵雷的个数就会发生数组越界的错误,为了防止发生数组越界错误所以我们需要在我们需要的数组大小的基础上增大一圈范围。只需要在原来的数组横坐标纵坐标同时加上二即可。
第一个数组未被选择部分就用‘ * ’表示,选择后没有雷则出现雷的个数。
第二个数组雷用1表示,不是雷的地方用0表示。
所以数组创建用一个函数即可表示,初始化时不用布置雷,所以一个数组都是‘ * ’,一个都是‘ 0 ’。
数组初始化代码展示:
void BoardInit(char str[ROWS][COLS], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
str[i][j] = set;
}
}
}
3.既然初始化雷之后我们就需要将数组打印出来,即二维数组的打印,两层for循环即可,但是为了方便玩家玩的过程,对于某点坐标可以清晰的看出来,我们最好将横坐标纵坐标打印出来,只需要在二维数组打印代码基础上添加几行代码即可实现。
数组打印代码展示:
void BoardPrint(char str[ROWS][COLS], int row, int col)
{
//该循环是打印数组的纵坐标
for (int i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
//打印数组的横坐标
printf("%d ", i);
for (int j = 1; j <= col; j++)
{
printf("%c ", str[i][j]);
}
printf("\n");
}
}
4.下一步就需要在全部为‘ 0 ’的棋盘里布置雷(即为‘ 1 ’)了 ,布置雷的过程也是十分简单,只需要随机产生一个坐标,将此处的‘ 0 ’变为‘ 1 ’即可,这时候就需要我们的随机数函数 rand()来产生两个0-9的随机数代表横坐标x与纵坐标y,为了每次产生随机数不同也需要使用srand()函数。
产生0-9的随机数用:rand() % 9+ 1;(具体规则看上面rand()函数介绍)。
每个程序使用一次srand()函数即可,所以我们在主函数里写srand((unsigned int)time(NULL));即能让每次产生的随机数是不同的。(具体看函数主干里的主函数)。
10个雷,我们用循环雷的数目作为循环停止条件,但是有个细节需要注意,就是在产生坐标是,因为产生的是0-9的随机数,有可能会发生两次布置雷的坐标是相同的,这个时候就不能让雷的数量减少了,如果减少雷的总数目就不足10个了。所以我们每次要判断这地方是否是‘ 0 ’,且雷布置成功才让雷的数目减少。
布置雷的代码展示:
void MineSet(char str[ROWS][COLS], int row, int col)
{
int mine = MINECOUNT;
//雷的数目作为循环条件,雷数目为零是退出循环
while (mine)
{
//产生两个0-9范围内的随机数
int x = rand() % ROW + 1;
int y = rand() % COL + 1;
if (str[x][y] == '0')
{
str[x][y] = '1';
//成功布置雷后才让雷的数目减少,防止二维数组中雷的数目不足10个
mine--;
}
}
}
5.最后一步也是游戏主要的逻辑,排查雷。由玩家通过键盘输入一个坐标,我们的程序需要判断mine(有雷无雷的二维数组即' 0 ' ' 1 '数组)如果mine[x][y]==‘ 1 ’则选择到雷,退出程序,游戏结束,如果选择的坐标在mine[x][y]中不是‘ 1 ’,统计以mine[x][y]为中心,3*3的矩阵中‘ 1 ’的个数,然后在show(区分选择未选择的部分)数组中show[x][y]替换为雷的个数。什么时候游戏胜利呢,即当剩余的坐标都是雷游戏胜利,那么就代表我们选择了ROW*COL-MINECOUNT次坐标,当到达这个次数代表所有雷都排查出来,游戏胜利。显然也是使用循环来控制主逻辑。
排查雷代码展示:
void MineFind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0, y = 0;
//游戏胜利的条件
int win = 0;
//win小与此次数是游戏继续
while (win < ROW * COL - MINECOUNT)
{
printf("请输入你要排查的坐标\n");
scanf("%d%d", &x, &y);
//确保玩家输入的坐标是合法的
if (x >= 1 && x <= 9 && y >= 1 && y <= 9)
{
//选择的坐标是雷则游戏结束,打印雷的棋盘
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了,游戏结束\n");
BoardPrint(mine, ROW, COL);
break;
}
//不是雷统计3*3矩阵中雷的数目,然后在show(x,y)中显示个数,在打印出show棋盘让win++
else
{
//统计雷的个数需要再写一个函数
int count = GetCount(mine, x, y);
show[x][y] = count + '0';
BoardPrint(show, ROW, COL);
win++;
}
}
//不合法则需要重新输入
else
{
printf("非法输入,重新输入\n");
}
}
//当达到该次数时游戏胜利。
if (win == row * col - MINECOUNT)
{
printf("恭喜你,排查掉了所有的雷\n");
}
}
上面提到了需要统计雷的个数,即遍历以选择的坐标为中心的3*3的数组统计一的个数即可,画出底下的图慢慢写就不会出错。因为在数组中是字符‘ 1 ’和字符‘ 0 ’所以需要减去8个字符‘ 0 ’,才是雷的个数。
统计雷的个数代码展示:
int GetCount(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');
}
text代码展示:
有条理的实现扫雷函数的逻辑以及调用上面我们写的函数。我们需要先调试我们的函数看是否能正常使用(正常情况下我们需要写完一个函数调试调用看是否能正常运行)
void menu()
{
printf("************************************************\n");
printf("************ 1.start 0.exit ************\n");
printf("************************************************\n");
}
void game()
{
//创建两个数组
char show[ROWS][COLS] = { 0 };
char mine[ROWS][COLS] = { 0 };
//初始化两个数组
BoardInit(mine, ROWS, COLS, '0');
BoardInit(show, ROWS, COLS, '*');
//布置雷,可以将雷的个数调到ROW*COL-1个
MineSet(mine, ROW, COL);
//将两个数组都打印出来
BoardPrint(show, ROW, COL);
BoardPrint(mine, ROW, COL);
//排查雷,看全部排除能否正常退出,以及排到雷是否结束游戏
MineFind(mine, show, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
//我们通过玩家输入1或者0判断游戏开始还是结束,
//这样的好处是选择0时while函数也会刚好结束。
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:game();
break;
case 0:printf("游戏结束\n");
break;
default:printf("非法输入,重新输入\n");
break;
}
} while (input);
return 0;
}
4.整体代码展示
Mine.h头文件
#pragma once
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINECOUNT 10
void BoardInit(char str[ROWS][COLS], int rows, int cols, char set);
void BoardPrint(char str[ROWS][COLS], int row, int col);
void MineSet(char str[ROWS][COLS], int row, int col);
void MineFind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
Mine.c文件
#include "Mine.h"
void BoardInit(char str[ROWS][COLS], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
str[i][j] = set;
}
}
}
void BoardPrint(char str[ROWS][COLS], int row, int col)
{
for (int i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i);
for (int j = 1; j <= col; j++)
{
printf("%c ", str[i][j]);
}
printf("\n");
}
}
void MineSet(char str[ROWS][COLS], int row, int col)
{
int mine = MINECOUNT;
while (mine)
{
int x = rand() % ROW + 1;
int y = rand() % COL + 1;
if (str[x][y] == '0')
{
str[x][y] = '1';
mine--;
}
}
}
int GetCount(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');
}
void MineFind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0, y = 0;
int win = 0;
while (win < ROW * COL - MINECOUNT)
{
printf("请输入你要排查的坐标\n");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= 9 && y >= 1 && y <= 9)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了,游戏结束\n");
BoardPrint(mine, ROW, COL);
break;
}
else
{
int count = GetCount(mine, x, y);
show[x][y] = count + '0';
BoardPrint(show, ROW, COL);
win++;
}
}
else
{
printf("非法输入,重新输入\n");
}
}
if (win == row * col - MINECOUNT)
{
printf("恭喜你,排查掉了所有的雷\n");
}
}
text.c文件
#include "Mine.h"
void menu()
{
printf("************************************************\n");
printf("************ 1.start 0.exit ************\n");
printf("************************************************\n");
}
void game()
{
char show[ROWS][COLS] = { 0 };
char mine[ROWS][COLS] = { 0 };
BoardInit(mine, ROWS, COLS, '0');
BoardInit(show, ROWS, COLS, '*');
MineSet(mine, ROW, COL);
BoardPrint(show, ROW, COL);
MineFind(mine, show, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
//我们通过玩家输入1或者0判断游戏开始还是结束,
//这样的好处是选择0时while函数也会刚好结束。
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:game();
break;
case 0:printf("游戏结束\n");
break;
default:printf("非法输入,重新输入\n");
break;
}
} while (input);
return 0;
}
5.总结
扫雷的代码量并不是很大,使用的知识点也都是基础,通过扫雷的简单实现可以帮助大家对C语言知识点的使用更加熟练以及写代码的逻辑有所帮助,我们只需分析扫雷步骤然后转化成代码的形式。扫雷代码上大家也可以加入自己的想法,这就需要大家自己动手了。
希望此文章对大家能有所帮助,我会继续发博客跟着大家一起学习的。