一、扫雷
扫雷,故名思意,即预先制成棋盘,并与其中生成MINE,玩家将其中MINE全部识别出,即获得游戏胜利
二、代码实现
test.c 测试游戏的逻辑
game.c 游戏功能的实现
game.h 游戏代码的申明(函数声明、符号申明)
注:本篇已实现扫雷时一扫一片的功能,实现插旗和拔旗,相较于鹏哥的课堂已有进步,万般部族,还望指点。
三、具体实现方法和相关想法
1、实现思路:
首先我们需要9*9的棋盘以实现对游戏基本盘的实现对游戏内容的基本实现。这里需要二维数组作为工具,而因为扫雷涉及到排雷和布置雷,所以我们需要两个二维数组。
然后,我们需要对期盼内容进行初始化,布置雷,然后让玩家进行“扫雷”。再进行判断,如果“触碰”到雷,判玩家游戏失败,如果所有的雷均被查出,玩家游戏胜利。
2、主函数逻辑实现:
主函数区域主要需要需要对游戏最初开始以及选取模式等负责,这里使用DO WHILE 加SWITCH语句进行设置,相对容易实现,此处设计初始面板MENU以方便玩家选择和游玩。
3、 游戏主体函数实现:
首先,我们需要建立最基本的信息,即两个二维数组,因为后续符号设置中,采用了“#”和“*”,此处为字符二维数组,这里设置行列为ROWS和COLS,这两个值这里是11,后续会及进行解释。
然后,进行初始化内容,MINE数组因为要放置'雷',所以这里初始化的时候,给他的宣称为“ ”(space),而SHOW数组因为为了给予玩家进行游戏体验,为了保持神秘感,我们给他初始化为“*”
再然后,进行雷的生成,这里运用到随机数的方法,随机生成两个1~9的数进行随机填充,以实现对游戏基本盘的实现,即9*9的棋盘,10个MINE。
这里为例,为了方便调试,设置了一个打印函数(显示板),后续也有用,但这里存在可以用来调试BUG
最后,进行合理的判断即可。
void game()
{
//难度选择
//DifficultySelect();
//建立炸弹盘 //建立排查盘
char mine[ROWS][COLS] = { '0' };//放置雷的信息
char show[ROWS][COLS] = { '0' };//排查雷的信息
//棋盘初始化
Initboard(mine, ROWS, COLS,' ');
Initboard(show, ROWS, COLS,'*');
//生成MINE
SetMINE(mine, ROW, COL);
//棋盘打印
//Displayboard(mine, ROW, COL);
/*Displayboard(show,ROW,COL);*/
//扫雷(包含一次展开一片)
Minesweeper(mine, show, ROW, COL);
4、棋盘初始化建设:
这里设置了两组函数进行初始化和打印,Initboard用于实现数组初始化,二维数组可以通过两组FOR循环来对每一个数据进行填充,这里又巧妙的设置了替换元素的变量,以实现可以为不同数组进行初始化。
棋盘的打印,为了美观和方便玩家进行游玩,可以进行横纵坐标的打印以及边界线的打印,这里运用新的变量z进行第一组横坐标的填充,而纵坐标其实只需要根据变量i的值进行合理打印即可,最后就可以制造出一个美观且直接的棋盘。(下图有实际操作建设出的)
这里展示手动示范eg:这里仅供参考,实际图可看下方附件一。
0 | 1 | 2 | 3
1 | --- | --- | ---
2 | %c | %c | %c
3 | --- | --- | ---
4 | %c | %c | %c
附件一:
5、实现对地雷的埋放:
这里为了实现功能,构建了一个SetMINE函数以实现对雷的安放,使用TIME函数构建时间戳,利用RAN生成随机数,再%上row或者对用col实现产生 0~8的随机数,+1产生1~9的随机值,这样任意组合即可随机生成地雷,利用count判断数量是否达标,最后只要判断此时加入的X Y两个值是否合法即可。
void SetMINE(char board[ROWS][COLS], int row, int col)
{
int x = 1;
int y = 1;
int count = 0;
while (count < MINE)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (board[x][y] == ' ' && board[x][y] != '#')
{
board[x][y] = '#';
count++;
}
}
}
6、实现玩家游玩以及判断输赢
这里搭建一个Minesweeper的函数实现对扫雷过程的整体框架,首先让玩家输入对应坐标,判断这些坐标是否合法,如果合法。即可进行判断是否是炸弹,若为炸弹,则游戏失败,即游戏结束。如果不是炸弹,则需要进行判断此坐标周边八个左边是否是雷,并统计个数。
此处为了游戏方便,我们可以进一步拓展
例如:若这八个坐标均不是雷,可以以这八个坐标分别为中心,再次向外拓展,直至碰到雷并标记。以此方法可以实现一扫一片,极快的加速的游戏进程。而这里为了不使数组数据访问出界,我们设置数组时即让ROWS和COLS为11,使得81个游戏方格内任何一个方格都可有8个外围,且不越界访问。
然后使对胜利的判断,我们发现,只需要满足棋盘中的空白方格数量等于row * col - MINE(初始雷数量),即完成了扫雷游戏,所以,我们需要一个变量(win)进行数据判断。
void Minesweeper(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;//确认棋盘中非‘#’‘*’位置的SPACE空间数量以判断输赢
while (win < row * col - MINE)
{
printf("请玩家输入坐标:>");
scanf("%d %d", &x, &y);
if (x > 0 && x < 10 && y>0 && y < 10 && show[x][y] == '*' || show[x][y] == '#')
{
if (mine[x][y] == '#')//如果是雷
{
printf("BOOM!!很遗憾,你被炸死了,下图为MINE示意图\n");
printf("------------------------------------------------------\n");
Displayboard(mine, ROW, COL);
printf("------------------------------------------------------\n");
break;
}
else//如果不是雷
{
MinesweeperII(mine, show, row, col, x, y); //先进行展开判断
Displayboard(show, ROW, COL);
FLAG(mine, show); //标志判断
CFLAG(mine, show); //去除标志判断
printf("------------------------------------------------------\n");
int i = 0;
int j = 0;
for (win = 0, i = 1; i <= ROW; i++)//实现每一次改变棋盘后计算win以判断是否胜利
{
for (j = 1; j <= COL; j++)
{
if (show[i][j] != '*' && show[i][j] != '#')
{
win++;
}
}
}
}
}
else
{
printf("非法坐标,请重新输入\n");
}
}
if (win == row * col - MINE)
{
printf("------------------------------------------------------\n");
printf("BINGO!! 恭喜你排雷成功!!游戏胜利\n");
printf("------------------------------------------------------\n");
}
}
7、实现判断函数
此处构建 CheckMine 和 MinesweeperII函数,进行判断。
CheckMine函数主要直接实现单个坐标周边8个位置是否含有雷,且含有数量。
MinesweeperII函数目的为了进行循环和展开判断,实现上述拓展中的做法,首先对需要判断的X和Y进行限制,不可让其越界访问产生错误或死递归。其次为了减少计算量,重复判断的坐标因被更改为“ ”(space),所以可以不用再次判断。最后满足条件的,我们逐一对这个八个坐标及进行MinesweeperII函数调用,以递归的方式实现一次排查一大片,同时使得无雷空间变为“ ”(space),极大方便我们游玩
int CheckMine(char mine[ROWS][COLS],int x,int y)
{
int i = 0;
int j = 0;
int count = 0;
for (i = -1; i <= 1; i++)
{
for(j = -1;j <= 1; j++)
{
if (mine[x + i][y + j] == '#')
{
count++;
}
}
}
return count;
}
void MinesweeperII(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (x<1 || x>row || y<1 || y>col)//坐标非法=
{
return;
}
if (show[x][y] != '*')//已被排查
{
return;
}
int count = CheckMine(mine, x, y);
if (count != 0)//周围有雷,显示雷数量
{
show[x][y] = count + '0';
return;
}
else if (count == 0)//周围没有雷
{
show[x][y] = ' ';//先变换为' ',美观
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
MinesweeperII(mine, show, ROW, COL, i, j);
}
}
}
}
8、拓展:对于插旗标志和撤旗标志的操作
此处实现相对容易,仅需要WHILE和SWITCH的合理分配即可。且逻辑相似。
void FLAG(char mine[ROWS][COLS], char show[ROWS][COLS])
{
int i = 0;
int x = 0;
int y = 0;
int a = 0;//跳出循环标志符
while (1)
{
printf("是否进行插旗操作(1/0):>");
scanf("%d", &i);
switch (i)
{
case 1:
printf("请输入要插旗的坐标:> ");
scanf("%d %d", &x, &y);
getchar();
if (show[x][y] == '*')
{
show[x][y] = '#';
Displayboard(show, ROW, COL);
a++;
break;
}
else
{
printf("坐标已被填充,请重新填写\n");
break;
}
case 0:
return;
default:
printf("非法坐标,请重新输入\n");
break;
}
if (a == 1)
{
break;
}
}
}
void CFLAG(char mine[ROWS][COLS], char show[ROWS][COLS])
{
int i = 0;
int x = 0;
int y = 0;
int a = 0;//跳出循环标志符
while (1)
{
printf("是否进行拔旗操作(1/0):>");
scanf("%d", &i);
switch (i)
{
case 1:
printf("请输入要插旗的坐标:> ");
scanf("%d %d", &x, &y);
getchar();
if (show[x][y] == '#')
{
show[x][y] = '*';
Displayboard(show, ROW, COL);
a++;
break;
}
else
{
printf("坐标已被填充,请重新填写\n");
break;
}
case 0:
return;
default:
printf("非法坐标,请重新输入\n");
break;
}
if (a == 1)
{
break;
}
}
}
四、完整代码:
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE 10
//棋盘初始化
void Initboard(char board[ROWS][COLS], int row, int col, char set);
//棋盘打印
void Displayboard(char board[ROWS][COLS], int row, int col);
//埋雷
void SetMINE(char board[ROWS][COLS], int row, int col);
//扫雷
void Minesweeper(char mine[ROWS][COLS],char show[ROWS][COLS], int row, int col);
void MinesweeperII(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y);
//插旗
void FLAG(char mine[ROWS][COLS], char show[ROWS][COLS]);
//拔旗
void CFLAG(char mine[ROWS][COLS], char show[ROWS][COLS]);
//头文件.h
//---------------------------------------------------------------------------------------------
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void Initboard(char board[ROWS][COLS], int row, int col, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = set;
}
}
}
void Displayboard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
int z = 0;
for (i = 0; i < row; i++)
{
//打印横坐标
if (z != 10)
{
for (z = 0; z <= col; z++)
{
printf(" %d ", z);
if (z < col)
{
printf("|");
}
}
printf("\n");
}
//打印分割线
if (i < row )
{
for (j = 0; j <= col; j++)
{
printf("---");
if (j < col)
{
printf("|");
}
}
printf("\n");
}
//打印纵坐标
printf(" %d ", i + 1);
printf("|");
//打印数组
for (j = 1; j <= col; j++)
{
printf(" %C ",board[i+1][j]);
if (j < col)
{
printf("|");
}
}
printf("\n");
}
}
void SetMINE(char board[ROWS][COLS], int row, int col)
{
int x = 1;
int y = 1;
int count = 0;
while (count < MINE)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (board[x][y] == ' ' && board[x][y] != '#')
{
board[x][y] = '#';
count++;
}
}
}
int CheckMine(char mine[ROWS][COLS],int x,int y)
{
int i = 0;
int j = 0;
int count = 0;
for (i = -1; i <= 1; i++)
{
for(j = -1;j <= 1; j++)
{
if (mine[x + i][y + j] == '#')
{
count++;
}
}
}
return count;
}
void FLAG(char mine[ROWS][COLS], char show[ROWS][COLS])
{
int i = 0;
int x = 0;
int y = 0;
int a = 0;//跳出循环标志符
while (1)
{
printf("是否进行插旗操作(1/0):>");
scanf("%d", &i);
switch (i)
{
case 1:
printf("请输入要插旗的坐标:> ");
scanf("%d %d", &x, &y);
getchar();
if (show[x][y] == '*')
{
show[x][y] = '#';
Displayboard(show, ROW, COL);
a++;
break;
}
else
{
printf("坐标已被填充,请重新填写\n");
break;
}
case 0:
return;
default:
printf("非法坐标,请重新输入\n");
break;
}
if (a == 1)
{
break;
}
}
}
void CFLAG(char mine[ROWS][COLS], char show[ROWS][COLS])
{
int i = 0;
int x = 0;
int y = 0;
int a = 0;//跳出循环标志符
while (1)
{
printf("是否进行拔旗操作(1/0):>");
scanf("%d", &i);
switch (i)
{
case 1:
printf("请输入要插旗的坐标:> ");
scanf("%d %d", &x, &y);
getchar();
if (show[x][y] == '#')
{
show[x][y] = '*';
Displayboard(show, ROW, COL);
a++;
break;
}
else
{
printf("坐标已被填充,请重新填写\n");
break;
}
case 0:
return;
default:
printf("非法坐标,请重新输入\n");
break;
}
if (a == 1)
{
break;
}
}
}
void MinesweeperII(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (x<1 || x>row || y<1 || y>col)//坐标非法
{
return;
}
if (show[x][y] != '*')//已被排查
{
return;
}
int count = CheckMine(mine, x, y);
if (count != 0)//周围有雷
{
show[x][y] = count + '0';
return;
}
else if (count == 0)//周围没有雷
{
show[x][y] = ' ';//填充为space
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
MinesweeperII(mine, show, ROW, COL, i, j);//递归实现外展
}
}
}
}
void Minesweeper(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;//确认棋盘中非‘#’‘*’位置的SPACE空间数量以判断输赢
while (win < row * col - MINE)
{
printf("请玩家输入坐标:>");
scanf("%d %d", &x, &y);
if (x > 0 && x < 10 && y>0 && y < 10 && show[x][y] == '*' || show[x][y] == '#')
{
if (mine[x][y] == '#')//如果是雷
{
printf("BOOM!!很遗憾,你被炸死了,下图为MINE示意图\n");
printf("------------------------------------------------------\n");
Displayboard(mine, ROW, COL);
printf("------------------------------------------------------\n");
break;
}
else//如果不是雷
{
MinesweeperII(mine, show, row, col, x, y); //先进行展开判断
Displayboard(show, ROW, COL);
FLAG(mine, show); //标志判断
CFLAG(mine, show); //去除标志判断
printf("------------------------------------------------------\n");
int i = 0;
int j = 0;
for (win = 0, i = 1; i <= ROW; i++)//实现每一次改变棋盘后计算win以判断是否胜利
{
for (j = 1; j <= COL; j++)
{
if (show[i][j] != '*' && show[i][j] != '#')
{
win++;
}
}
}
}
}
else
{
printf("非法坐标,请重新输入\n");
}
}
if (win == row * col - MINE)
{
printf("------------------------------------------------------\n");
printf("BINGO!! 恭喜你排雷成功!!游戏胜利\n");
printf("------------------------------------------------------\n");
}
}
//game.c
//---------------------------------------------------------------------------------------------
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
//项目:扫雷
//构建开始游戏体系 建立难度选择机制
//建立标志雷体系
//建立排查雷体系
void game()
{
//难度选择
//DifficultySelect();
//建立炸弹盘 //建立排查盘
char mine[ROWS][COLS] = { '0' };//放置雷的信息
char show[ROWS][COLS] = { '0' };//排查雷的信息
//棋盘初始化
Initboard(mine, ROWS, COLS,' ');
Initboard(show, ROWS, COLS,'*');
//生成MINE
SetMINE(mine, ROW, COL);
//棋盘打印
Displayboard(mine, ROW, COL);
/*Displayboard(show,ROW,COL);*/
//扫雷(包含一次展开一片)
Minesweeper(mine, show, ROW, COL);
}
void menu()
{
printf("-----------------------------------\n");
printf("-------- 扫 雷 游 戏 -------\n");
printf("-------- 1. 开 始 游 戏 -------\n");
printf("-------- 0. 退 出 游 戏 -------\n");
printf("-----------------------------------\n");
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入数字以决定是否开始游戏:>(输入1/0)");
scanf("%d", &input);
switch (input)
{
case 1:
printf("好的,开始游戏\n");
game();
Sleep(4000);
system("cls");
break;
case 0:
printf("好的,退出程序\n");
break;
default:
printf("输入非法,请重新输入\n");
break;
}
} while (input);
}
//test.c
//---------------------------------------------------------------------------------------------
五、总结
其实我感觉写这个还好的,对于初学者而言,实现基本思路其实是相对重要的,跟随鹏哥学习的过程中能激发很多思考,比如这里对于扫雷的两个拓展。同时也遇到了很多很多困难,毕竟我的知识量并不算多。所幸的是我坚持下来了,并完成了除了难度选择的其余拓展(因为最初选择宏定义的方式进行书写,而宏定义在我的认知中是不能修改的,这里想到了最蠢的办法,多次定义宏,EG:ROW ROW1然后重复多次书写已有代码,但是感觉相对愚笨,故不再尝试,待有朝一日学成归来,定将此处遗漏的点补上),我自知上述材料还有很多不足,欢迎大家批评指正。
最后,希望此篇不辜负那个初出茅庐的自己叭。