扫雷作为一款内置于windows XP系统的游戏,相信大多数人都有游玩过。接下来我将带着各位用C语言来实现这个游戏。
首先,我们来了解扫雷游戏的规则,将这些规则逐步用函数来实现,再经过逻辑的调整即可得到所需的代码。
可以试着先自己玩一把再继续看本文章。
编辑
规则如下:
第一次点击不会是雷。格子里的数字表示它周围有几个雷。游戏目标是找出所有雷。" 触雷" 则输。点击表情重新开始。二选一留到最后, 可任选, 需先清完其他方块。
为了达到在编程中学习的目的,我们降低难度,让初学函数的同学也能写出来、有收获,我们把需求简化如下:
1、打印游戏菜单,让玩家确定是否开始游戏
2、生成特定尺寸的棋盘格子(对应网页上的不同难度)
3、生成地雷
4、玩家输入排雷位置
5、判断此处是否有地雷,如果有雷,游戏失败,如果此处无地雷,就显示这个格子周围的地雷个数。
6、把不是地雷的位置都排查完后游戏胜利。
接下来对需求进行分析:
打印菜单很简单,设计一个menu()函数即可,代码如下:
void menu()
{
printf("*****************\n");
printf("*****1_play******\n");
printf("*****0_exit******\n");
printf("*****************\n");
}
玩家输入数字1来进行游戏,在主函数中用scanf()函数和if()语句即可实现即可实现。
一、创建数组——表示棋盘格和地雷位置
为了方便调整棋盘格的尺寸,我们使用宏定义定义全局变量ROW和CLO用来表示行数和列数。
编辑
如何生成棋盘格呢?很容易想到用二维数组,那数组的大小应该是多少呢?
我们来转到一个现成的棋盘——excel:
编辑
这是一个9*9的棋盘,假设我们用*来表示没有排查的位置,并且为了方便玩家知道几行几列还加入了一个坐标轴,所以变成了一个10*10的棋盘,变为更加通用的情况就是[ROW+1][CLO+1]的情况。
我们先暂定一个字符型数组show[ROW+1][CLO+1]。
接下来,要有一个数组来存放地雷的相关位置,地雷不仅要作为判断是否踩雷的依据,还要知道某一格周围有几个地雷,我们有两种选择,第一种是选择用某个记号来表示有或者没有比如用Y表示有地雷,N表示没有地雷,第二个选择是使用数字来表示,1表示有地雷,0表示没有地雷,要得到周围地雷数的时候只需要加上周围这一圈就可以了。
编辑(此处用黄色来表示选中的位置,绿色表示计数范围)
那么边上这一圈怎么办呢?
不妨在定义数组时再扩大一点,变成下图
编辑
这样就可以在任意实际展示出的9*9格子正确计数了。将地雷数组记为char mine[12][12];
为了统一坐标,我们也将show数组扩大为char show[12][12],为什么要定义成字符型而不定义成整形数组呢?下一小节中我会解释。
为了方便之后修改棋盘格大小,我们使用宏定义 ROWS和CLOS
编辑
这样就完成了数组的创建。
二、初始化数组
我们定义一个Initboard()函数用来初始化数组,之前定义的数组均为字符型,这样用一个函数就可以完成初始化。
void Initboard(char arr0[ROWS][CLOS], char set)
{
int i, j;
for (i = 0; i < ROWS; i++)
{
for (j = 0; j < CLOS; j++)
{
arr0[i][j] = set;
}
}
if (set == '*')
{
for (i = 0; i < ROWS - 1; i++)
arr0[i][0] = i + '0';
for (j = 0; j < CLOS - 1; j++)
arr0[0][j] = j + '0';
}
}
在初始化show数组时在set处传入'*' , mine数组传入'0',在最后一条if语句是为了当初始化show数组时加入坐标轴。这样就用一个函数实现了两个数组的初始化,并且可以自定义初始化符号。
三、打印数组
要将show数组展示给玩家看,就要打印这一数组。定义一个Displayboard()函数用来打印。
void Displayboard(char arr[ROWS][CLOS])
{
int i, j;
for (i = 0; i < ROWS - 1; i++)
{
for (j = 0; j < CLOS - 1; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
printf("___________________________\n");
}
最后的一行打印一个横线是为了帮助玩家区分各次操作。
四、埋雷
随机位置埋雷,容易想到rand()用来指定范围生成随机数作为地雷的坐标,但是,思考一下这其中有没有可能两次埋雷放到了同一位置?如何规避这一现象?我们可以对最终的mine数组每个元素求和,看看是否总共有10个'1',如果不是,就再循环,直到满足条件。需要注意,此前你已经对mine数组进行了修改,要想重新正确生成,需要再次对其初始化。有了大致思路,接下来开始撸代码:
先使用宏定义 EASY_cont 来说明我们要生成多少个地雷,之后可以方便地修改,可以先自己尝试写一写,看看我们的实现思路是否相同,有更好的思路欢迎大佬在评论区交流。
int Set_mine(char mine[ROWS][CLOS])
{
int row[EASY_cont] = { 0 }, clo[EASY_cont] = { 0 }, i = 0, error = 0, cont = 0;
for (error = 0; error < EASY_cont; error++, i++)
{
srand((unsigned int)(time(NULL) + error));
//for (i = 0; i < EASY_cont; i++)
row[i] = 1 + rand() % ROW;
//for (i = 0; i < EASY_cont; i++)
clo[i] = 1 + rand() % CLO;
}
for (i = 0; i < EASY_cont;)
{
mine[row[i]][clo[i]] = '1';
i++;
continue;
}
if(loading)
Displayboard(mine);//调试用的,实际游戏中注释掉即可
cont = 0;
for (i = 0; i < ROWS; i++)
{
for (error = 0; error < CLOS; error++)
{
cont += (int)(mine[i][error] - '0');//这里太坑了,之前排查很久都没排查到
}
}
if (cont == EASY_cont)
{
printf("生成完成,按任意键开始游戏\n");
system("pause");
return 0;
}
else
{
if(loading)
printf("只生成了%d个地雷,发生错误,正在重新生成地雷(doge)\n", cont);
return 1;
}
}
(这里的error可以看成j,程序能跑就不想改了[doge])
为啥有个if(loading)呢?其实这里的loading是一个宏定义的量,作用是为了在自己做测试的时候观察生成时错误的情况。
为啥这里定义成int型呢?在我的主函数里,调用这一函数是这样写的:
menu();
scanf("%d", &test);
deal_with_Easter_egg:
if (test)
{
Initboard(show, '*');
Initboard(mine, '0');
while (Set_mine(mine))
{
goto deal_with_Easter_egg;//异常处理
}
如果地雷有重叠就回到标签的位置初始化数组并且重新生成。
五、根据坐标计算周围的地雷数
只需要参考之前的excel棋盘格就,对玩家输入的坐标一圈的mine()数组求和即可。
int Cont_mine(char mine[ROWS][CLOS], int i, int j)
{
int cont;
cont = (int)(mine[i - 1][j] + mine[i + 1][j] + mine[i][j - 1] + mine[i][j + 1] + mine[i - 1][j - 1] + mine[i - 1][j + 1] + mine[i + 1][j + 1] + mine[i + 1][j - 1] - 8 * '0')-1;
return cont;
}
注意求和的计算方法。
六、关于游戏成功的判定
只要棋盘上已经排除的格子数加上地雷数之和等于展示给玩家的格子总数即可
while (EASY_cont + cont_mine != ROW * CLO)//此处的cont_mine是用来计数已经排查的位置的。
最后,用合适的逻辑串联起来即可。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#define ROW 3//这个数字和下面这个数字都不能小于3,不然会报错
#define CLO 3
#define ROWS ROW + 2
#define CLOS CLO + 2
#define EASY_cont 1 // 表示炸弹数
#define toushi 1 //这个选项用来开关炸弹位置透视
#define loading 0 //这个参数用来开关生成过程输出
int main()
{
char show[ROWS][CLOS] = { 0 }, mine[ROWS][CLOS] = { 0 };
int test, i, j, cont, cont_mine;
// show数组是打印出来展示的,mine数组用来储存地雷的位置
// 展示的部分用*表示没有探索的区域,用数字表示已经开发的区域,如果一个格子周围有雷,就用数字来表示周围一圈的地雷数量
// Initboard用来初始化两个字符型数组
// Displayboard用来打印棋盘
// Set_mine用来随机布置地雷
// Cont_mine用来得出周围的地雷数,在得出地雷数之前还要判断所在位置是否有地雷,有地雷就结束游戏失败,没有地雷就得出周围的地雷数
menu();
scanf("%d", &test);
deal_with_Easter_egg:
if (test)
{
Initboard(show, '*');
Initboard(mine, '0');
while (Set_mine(mine))
{
goto deal_with_Easter_egg;//异常处理
}
if (loading)
Displayboard(show);
cont_mine = 0;//用于统计已经排除的位置
while (EASY_cont + cont_mine != ROW * CLO)
{
Displayboard(show);
if (toushi)
Displayboard(mine);
error_location:
printf("请输入坐标\n");
scanf("%d %d", &i, &j);
if ((i > 0 && i <= ROW) && (j > 0 && j <= CLO))
{
if (mine[i][j] == '0')
{
cont = Cont_mine(mine, i, j);
cont++;
cont_mine++;
show[i][j] = (char)(cont + '0');//相关知识:https://zhidao.baidu.com/question/90654244.html
Displayboard(show);
}
else
{
printf("游戏失败\n");
menu();
scanf("%d", &test);
goto deal_with_Easter_egg;
}
}
else
{
printf("坐标输入错误,请重新输入\n");
goto error_location;
}
}
printf("游戏成功\n");
menu();
scanf("%d", &test);
goto deal_with_Easter_egg;
}
else
printf("感谢游玩,期待下一次相遇\n");
}