目录
1 系统概述
1.1设计目的
扫雷是一种经典的电脑单人游戏,是一种以逻辑为基础的益智游戏。通过输入坐标来揭示数字或地雷的位置,找到所有没有地雷的格子,最终获得胜利。扫雷游戏具有良好的交互性和可玩性,可以帮助玩家提高思维和判断能力。本次研究将通过对代码的分析,探究扫雷游戏的功能和特点,并通过对其代码的分析,进一步提高我们的编程技巧和对算法的理解。
1.2游戏简介
扫雷是一款基于点击格子揭示数字的游戏,其中有些格子下面会隐藏着地雷,玩家需要通过数字推断出哪些格子存在地雷,哪些格子是安全的。在游戏中,玩家需要输入坐标来揭开某一个格子上的数字或者地雷,如果揭开的格子上没有地雷,则该数字表示周围8个格子中地雷数量的总和,否则游戏结束。玩家还可以使用标记功能来标记可能存在地雷的格子,标记后的格子被覆盖问号,以提示玩家哪些格子可能存在地雷。如果该各种周围8个格子中地雷数量的总和为零,那么会展开空白的格子。
1.3游戏功能
本游戏使用C语言编写,采用命令行界面。游戏运行后,玩家将看到游戏菜单,包括开始游戏和退出游戏两个选项。玩家可以通过键盘输入选择相应的菜单选项。
本游戏包括布置雷、猜雷和标记雷三个功能。
(1) 布置雷
游戏开始时,程序会自动布置随机的雷。玩家在游戏开始前无法知道哪些格子是有雷的,因此需要在猜雷过程中逐步发现。
(2) 猜雷
玩家需要通过猜测每个格子是否有雷来完成游戏。如果玩家猜错了一个有雷的格子,那么游戏就结束了。如果玩家成功排查出全部没有雷的格子,那么游戏胜利。
(3) 标记雷
如果玩家认为某个格子有雷,可以对该格子进行标记。标记后的雷将被带上一个?的标志,使得玩家在后续的游戏中可以快速识别哪些格子有雷。
本游戏的规则比较简单。
(1) 游戏开始时,程序会在地图上随机布置一定数量的雷。
(2) 玩家需要在未掀开的格子中选择一个,然后猜测这个格子是否有雷。如果猜测正确,游戏将显示该格子周围的雷的数量或者展开空白格子。如果猜测错误,游戏结束。
(3) 玩家还可以对格子进行标记。已经标记过的格子可以解除标记。
(4) 如果玩家成功排查出所有没有雷的格子,游戏胜利。
2 总体设计
本次课程设计要达到的目的是运用c语言中所学的知识去实现扫雷游戏的主要功能,包括实现排查雷的功能,标记雷的功能,显示周围雷的数量,第一次排查不会遇雷,如果周围没有雷则展开一片,该篇我会介绍扫雷程序的重要功能,实现效果是可以用键盘来操作棋盘,包括开始游戏,排雷,标记雷,取消标记雷,排到9格无雷会展开空白,排到雷会结束游戏,排完空雷或者标记完雷会成功排雷。
3 功能设计
3.1扫雷游戏的初次化
即棋盘的设置,用一个二维数组会出现很多问题,所以我们使用两个数组,一个mine数组来记录该坐标有无雷,一个show数组展示给用户看,未排查用‘*’表示,已排查用周围雷的数量表示。为了统一,我们使用字符数组,遇到整数时将其转化为字符存放。假设是9*9的棋盘,那我们就需要11*11的数组,我们可以只使用设定数组的内圈部分,即最外圈不再使用,用于判断。
3.2初始化函数
我设置了一个初始化函数,用于设置数组最初的元素,因为两个数组最初都是只存放一种字符,‘0’ 和‘*’,所以可以直接把字符当作实参传入函数,赋值给ret,这样就可以通用一个初始化函数了。如下图2-1
图2-2初始化函数
3.3布置雷
我们使用随机函数设置雷,我们将是雷的元素设置为’1’,没雷的设置成’0’,即使用while循环来遍历数组,将mine数组的部分随机设置成’1 ',我们使用rand函数来设置随机数,并且需要注意的是,我们只针对数组内部真正有效的部分,最外一圈不设置,所以随机的下标是1~9。值得一提的是,使用rand函数来设置随机数时,如果不设置随机数种子,导致每次随机数种子都自动设成相同值1 ,进而导致rand函数所产生的随机数值都一样。所以我们要利用stand函数来设置随机数种子,使用系统时间来初始化种子。如下图2.3
图2-3布置雷
3.4主函数
在主函数中,我们使用do-while语句设为主函数的主体,以便于用户可以多次游玩,然后我们利用Switch语句来选择是否开始游戏,在主函数中,我们需要设置随机数种子,以便后边引用。如下图2.4
图2-4主函数
3.5打印棋盘函数
打印棋盘函数打印棋盘函数的主要功能是将棋盘以表格的形式打印出来中。函数的第一步是输出一条横线作为表格的顶部边框,然后在循环中使用两个嵌套的 for 循环遍历整个棋盘,并将它们的内容输出到控制台中。具体地说,在外层循环中,从1到行数 遍历每一行,并打印该行的标号;在内层循环中,从1到列数遍历每一列并打印该位置上的字符,并且在每行结束时会打印一个换行符。最后,函数输出一条横线作为表格的底部边框。如下图2.5
.
图2-5打印棋盘函数
3.6打印功能界面
我们使用while来循环,条件是排雷的数量小于空雷的数量或者正确的标记的雷的数量小于雷的数量,如果条件成立,那打印功能界面,不成立代表游戏胜利,在循环中,我们使用3个if语句来选择功能,即排雷功能,标记雷功能和取消标记雷的功能,我们再从其中三个函数来实现功能,值得注意的是,我将排雷函数设置成int类型来返回1或者2,返回1即被炸死,返回2则代表排的坐标已被标记。如果排查过的数量等于‘0’的数量的时候或者标记正确雷的数量等于设置的雷数量结束,排雷成功。如下图2.6
图2-6打印功能界面
3.7排雷函数
函数主要功能是根据玩家输入的坐标对扫雷游戏进行排查,并更新棋盘状态。函数内部实现逻辑如下:
定义并初始化局部变量 x、y和count,用于记录玩家输入的坐标和雷的数量。
使用 printf 函数提示玩家输入排查的坐标,并使用 getchar 函数清空输入缓存。
使用 scanf 函数读取玩家输入的坐标。
检查坐标的合法性,如果不合法则提示玩家重新输入。
如果输入的坐标处是地雷,则返回 1,表示游戏失败。
如果输入的坐标处不是地雷,则继续排查。如果该位置已经被排查过,则返回 2,表示无需处理。
否则,统计该位置周围八个方格中地雷的数量,并根据数量更新 show 数组。如果该位置周围没有地雷,则调用 SpreadBlank 函数进行空白扩展。如果有地雷,则将数量转换为字符并存入 show 数组中。
使用 system 函数清空屏幕并展示更新后的棋盘状态。
如下图2.7。
图2-7排雷函数
3.8标记雷函数
首先通过 printf() 函数提示玩家输入要标记的坐标,然后通过 scanf() 函数获取用户输入的 x 和 y 坐标;判断玩家输入的坐标是否在雷区范围内,并且该位置未被标记过;如果满足条件,则判断该位置是否为地雷。如果是地雷,则将该位置标记为 "?",并将全局变量 mark_count 加一;如果不是地雷,则不做任何处理。如果玩家输入的坐标不在范围内或者已经被标记过,则输出错误提示信息。 该函数中使用了一个全局变量 mark_count,该变量用于记录玩家标记正确的地雷数量。如下图2.8
图2-8标记雷函数
3.9取消标记函数
首先通过 printf() 函数提示玩家输入要取消的坐标,然后通过 scanf() 函数获取用户输入的 x 和 y 坐标;判断玩家输入的坐标是否在雷区范围内,并且该位置未被标记过;如果满足条件,则判断该位置是否为地雷。如果是地雷,则将该位置标记为 "*",并将全局变量 mark_count 加1;如果不是地雷,则不做任何处理。如果玩家输入的坐标不在范围内或者已经被标记过,则输出错误提示信息。 该函数中使用了一个全局变量 mark_count,该变量用于记录玩家标记正确的地雷数量。如下图2.8.
图2-9取消标记函数
3.10空白展开函数
函数的主要思路为:首先判断当前格子是否在边界上(即是否超出了雷区的范围),如果是,则直接返回;否则,判断当前格子是否已经被展示过或者是否是一个雷格子,如果不是,则直接返回,否则将格子标记为展示状态,并统计已经展示的格子数量。
如果当前格子周围有雷,则将当前格子标记为周围雷的数量,并将已经展示的格子数量加 1,然后直接返回。如果当前格子周围没有雷,则将当前格子标记为无雷,将已经展示的格子数量加 1,然后依次递归调用函数,扫描周围八个格子。
需要注意的是,在递归调用函数时,要传入已经展示的格子数量的指针,并在每次展示格子后将其加 1。这是因为在该函数中有多次递归调用,不能使用局部变量来记录已经展示的格子数量,需要使用指针来实现对全局变量的修改。如下图2.10
图2-10空白展开函数
4 总结
4.1遇到的问题及解决方法
在这次程序编写中,首先遇见的问题就是如何设置棋盘,假设我们用一个二维数组,用数据0表示没有雷,1表示有雷,但是当我们排查一个点之后需要显示周围雷的数量,假设也为1,那就会产生冲突,同时也不方便统计周围雷的数量。还有一个问题数组容易出界,每次访问都需要判断,比较麻烦,容易出错。
还有个问题就是扫雷游戏需要多次输入数据,而缓冲区的数值容易被下一次输入所获取,很容易出现错误。
我选择设置两个数组,一个mine数组表示有无雷,一个show数组展示给用户看,未排查用‘*’表示,已排查用周围雷的数量表示。假设是9*9的棋盘,那我们就需要11*11的数组,我们可以只使用设定数组的内圈部分,即最外圈不再使用,用于判断。这样就不会出现边界冲突的问题。
4.2感受
编写C语言扫雷代码是一项有趣而又具有挑战性的任务。在这个过程中,我感到了自己的编程技能得到了很好的锻炼和提高。
首先,我必须理解扫雷游戏的规则和逻辑,然后将其转化为代码实现。这需要仔细思考和计划,以确保代码功能正确、稳定并且易于维护。
在编写代码的过程中,我也发现了一些问题和挑战。例如,如何设计程序的数据结构以有效地存储游戏板上的所有格子及其状态,并如何处理玩家交互事件等。
此外,我还体会到了不断调试和优化代码的重要性。每当我遇到一个问题时,我都要花费时间仔细分析它,并寻找最佳解决方案。这不仅可以帮助我解决当前问题,还可以提高我未来编写代码的能力和经验。
总的来说,编写C语言扫雷代码是一个非常有意义的过程。通过这个项目,我不仅学习了新技能和知识,还收获了一份充满成就感的完成作品。
5 参考文献
[1]赵华,王建军,基于C++的扫雷游戏程序设计,计算机时代,2015(09):35-38
[2]陈华,刘洋,C++语言扫雷游戏的设计与实现,软件导刊,2016(01):64-67.
[3]王红梅,基于C++的扫雷游戏设计,科技资讯,2017(12):178-179.
6 源代码
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE 10
int find_count=0;//判断是否为第一次排雷,防止被炸
int mark_count=0;//标记的正确的雷的数量
int digui=0;
void InitBoard(char mine[ROWS][COLS],int rows,int cols,char p);
void InitBoard(char show[ROWS][COLS],int rows,int cols,char p);
void SetMine(char board[ROWS][COLS],int row,int col);
void DisplayBoard(char board[ROWS][COLS],int row,int col);
void DisplayBoard(char show[ROWS][COLS],int row,int col);
int paicha(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col,int* Win_count);
int get_mine_count(char mine[ROWS][COLS],int x,int y);
void PlayGame(char mine[ROWS][COLS], char show[ROWS][COLS],int row,int col);
void Mark(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);
void quxiaoMark(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);
void SpreadBlank(char mine[ROWS][COLS],char show[ROWS][COLS],int x,int y,int* Win_count);
void menu()
{
printf("***********************************\n");
printf("****** 扫雷游戏 ******\n");
printf("****** 1. 开始游戏 ******\n");
printf("****** 0. 退出游戏 ******\n");
printf("***********************************\n");
}
void game()
{
char mine[ROWS][COLS]={0};//存放布置好的雷的信息
char show[ROWS][COLS]={0};//存放排查出的雷的信息
InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*');//初始化棋盘
DisplayBoard(show,ROW,COL);//打印棋盘
SetMine(mine,ROW,COL);//设置雷
DisplayBoard(mine,ROW,COL);
PlayGame(mine ,show,ROW,COL);
}
int main()
{
srand((unsigned)time(NULL));
int input=0;
int count=0;
do
{
menu();
if(count!=0)
{
char ch;
while((ch=getchar())!=EOF&&ch!='\n')
{
;
}
}
count++;
scanf_s("%d",&input);
system("cls");
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏。\n");
break;
default:
printf("选择错误,重新选择。\n");
break;
}
} while (input);
return 0;
}
void InitBoard(char board[ROWS][COLS], int rows, int cols, char ret)
{
int i=0;
int j=0;
for(i=0;i<rows;i++)
{
for(j=0;j<cols;j++)
{
board[i][j]=ret;
}
}
}
void DisplayBoard(char board[ROWS][COLS],int row,int col)
{
int i=0;
int j=0;
printf("-------------------\n");
for(i=0;i<=row;i++)
{
printf("%d ",i);
}
printf("\n");
for(i=1;i<=row;i++)
{
printf("%d ",i);
for(j=1;j<=col;j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
printf("-------------------\n");
}
void SetMine(char board[ROWS][COLS],int row,int col)
{
int count =MINE;
while (count)
{
int i=rand()%row+1;//1-9
int j=rand()%col+1;
if(board[i][j]=='0')
{
board[i][j]='1';
count--;
}
}
}
void PlayGame(char mine[ROWS][COLS], char show[ROWS][COLS],int row,int col)
{
int win_count=0;//排查过的数量
while(win_count<(row*col-MINE)&&mark_count<MINE)//排查过的数量等于空白格子或者标记正确的雷等于雷的数量
{
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;
}
else if(choice==1)
{
int win=paicha(mine,show,row,col,&win_count);
if(win==1)
{
system("cls");
printf("很遗憾,你被炸死了。\n");
DisplayBoard(mine,ROW,COL);
break;
}
else if(win==2)
{
system("cls");
printf("该点已被标记,请重新选择。\n");
DisplayBoard(show,ROW,COL);
}
}
else if(choice==2)
{
Mark(mine,show,row,col);
system("cls");
DisplayBoard(show,ROW,COL);
}
else if(choice==3)
{
quxiaoMark(mine,show,row,col);
system("cls");
DisplayBoard(show,ROW,COL);
}
}
if(win_count==(row*col-MINE)||mark_count==MINE)
{
system("cls");
printf("恭喜你,排雷成功\n");
}
}
int paicha(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col,int* Win_count)
{
int x=0,y=0,count=0;
printf("请输入排查的坐标");
char ch;
while((ch=getchar())!=EOF&&ch!='\n')
{
;
}
scanf("%d%d",&x,&y);
if(x>=1&&x<=row&&y>=1&&y<=col)//检查坐标合法性
{
if(mine[x][y]=='1')
{
return 1;
}
else if(mine[x][y]=='0')//不是雷,统计雷的个数
{
if(show[x][y]=='?')
{
return 2;
}
int count=get_mine_count(mine,x,y);
if(count==0)
{
SpreadBlank(mine,show,x,y,Win_count);
}
else
show[x][y]=count+'0';//count 是数字,用ASCII码转化为字符
system("cls");//刷新棋盘
DisplayBoard(show,ROW,COL);
}
}
else
{
printf("坐标不合法,请重新输入。");
}
}
void Mark(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col)
{
int x=0,y=0;
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");
}
}
void quxiaoMark(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col)
{
int x=0,y=0;
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");
}
}
int get_mine_count(char mine[ROWS][COLS],int x,int y)//计算九格内雷的数量
{
return mine[x-1][y]+mine[x-1][y-1]+mine[x-1][y+1]
+mine[x][y-1]+mine[x][y+1]
+mine[x+1][y+1]+mine[x+1][y]+mine[x+1][y-1]
-8*'0';
}
void SpreadBlank(char mine[ROWS][COLS],char show[ROWS][COLS],int x,int y,int* Win_count)//展开空白格子
{
if(x==0||y==0||x==ROWS-1||y==COLS-1)
return;
if(show[x][y]!='*')
return;
int count=get_mine_count(mine,x,y);
if(count>0)
{
show[x][y]=count+'0';
*(Win_count)++;
return;
}
else
{
show[x][y]=' ';
*(Win_count)++;
SpreadBlank(mine,show,x-1,y,Win_count);//递归9格的格子
SpreadBlank(mine,show,x+1,y,Win_count);
SpreadBlank(mine,show,x,y-1,Win_count);
SpreadBlank(mine,show,x,y+1,Win_count);
SpreadBlank(mine,show,x-1,y-1,Win_count);
SpreadBlank(mine,show,x+1,y-1,Win_count);
SpreadBlank(mine,show,x-1,y+1,Win_count);
SpreadBlank(mine,show,x+1,y+1,Win_count);
}
}