C语言逐步实现扫雷游戏
游戏介绍
扫雷游戏(初阶)规则如下:
- 游戏发生在一张9X9的网格之上,在其中任意十个位置上布上地雷,其位置对玩家不可见;
- 玩家自由选择合适的坐标,表示自己要扫的区域;
- 在玩家选择后,给出提示;若所扫位置有雷则游戏失败,向玩家展示地雷位置;否则在玩家所扫位置给出一个数字,该数字表示的是以其为中心的周边一圈,八个网格中存在的地雷数量;
- 玩家扫除所有不含雷的位置则表示玩家胜利,至此游戏结束。游戏结束后显示地雷分布。
需求分析(初阶)
- 首先需要初始化一张9X9的棋盘,在此基础上实现游戏功能;
- 电脑能够随机布置地雷,地雷位置玩家不可见;
- 玩家选择位置,要能判断该处是否有地雷,游戏能否继续进行;
- 要能统计非地雷处周边位置的地雷数量,并在玩家扫过后展示给玩家,以供玩家继续游戏;
- 在玩家扫除所有不含地雷的位置后,要能及时结束游戏并给出胜利提示;
- 玩家可以选择退出、或者继续游戏;
关于游戏设计的说明
1.在设计中我们将用 ‘1’ 表示某处有雷;用 ‘0’ 表示某处无雷;
2. 考虑到游戏功能的需求,如:我们需要对用户隐藏地雷位置,但是在用户扫过一块区域并且没有碰雷的条件下,我们又要将该处附近的地雷数量展现给用户。在此过程中可能会出现问题,我们在说明1中声明 ‘1’ 表示此处有雷,如果用户扫到一块无雷区域,再次周围有一颗雷,那么我们要将该处置 ‘1’ ,这个时候会发生矛盾,此区域会被人为定义为雷区,引发程序错误。所以我们设计用两张9X9的棋盘完成这样的需求,一张表示地雷分布,一张用来向用户展示;
3. 实现过程中我们又发现,会有很多网格处在边缘区域,如果我们将他们单独处理,不仅很不方便也会浪费我们大量的时间。于是我们想到可以扩大棋盘至 11X11 ,但是只向用户展示中间 9X9 的部分,便可以解决这个问题。这样即使玩家扫到边缘区域,我们也可以保证该处周围存在八个网格,便于我们统计。
游戏实现(初阶)
(1)game.h
此文件包含的是,对在此项目所要用到的函数的声明。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//表示向用户展示的列、行数
#define ROW 9
#define COL 9
//表示设计中实际需要的行、列数
#define ROWS ROW+2
#define COLS COL+2
//NUM表示地雷数,MAX表示棋盘规模
#define NUM 80
#define MAX 81
//打印菜单
void menu();
//初始化棋盘
void init(char arr[ROWS][COLS], int rows, int cols,char ret);
//打印棋盘
void print(char arr[ROWS][COLS], int row, int col);
//随机放置地雷
void put_bone(char board[ROWS][COLS], int num,int row,int col);
//玩家扫雷
char player_do(char board[ROWS][COLS],char bone[ROWS][COLS],int x,int y);
//统计无雷区域周围地雷数
int count(char board[ROWS][COLS], int x, int y);
(2)game.c
此文件包含的是,游戏功能实现所需函数的定义。
1、打印菜单
#include"game.h"
void menu()
{
printf("***********************************\n");
printf("******* 1、play ********\n");
printf("******* 0、exit ********\n");
printf("***********************************\n");
}
2、初始化网格
void init(char arr[ROWS][COLS], int rows, int cols,char ret)
{
int i = 0, j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
arr[i][j] = ret;
//ret 接受的是字符,表示初始化样式,在后面我们用 ‘0’ 初始化地雷分布网格,用 ‘*’ 初始化展示网格
}
}
}
说明:我们初始化网格需要将整个网格全部初始化,ret 表示用何种字符来初始化。也就是说我们将用 ret 初始化 11X11 的网格。
3、打印网格
void print(char arr[ROWS][COLS], int row, int col)
//row,col 分别接收行、列
{
int i = 0, j = 0;
for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
//为了方便玩家定位,将列号先打印好
printf("\n");
for (i = 1; i <= row; i++)
//下标从1开始,到row结束
{
printf("%d ", i);
//同样是为了方便定位,将行号打印
for (j = 1; j <= col; j++)
//下标从1开始,到col结束
{
printf("%c ", arr[i][j]);
//打印网格中的元素
}
printf("\n");
}
}
说明:传几行几列就打印几行几列,我们希望向用户展示 9X9 的网格,即只打印中间部分,所以我们设置下标从 1 开始,这样可以正好将我们需要的部分打印出来。
4、布置地雷
void put_bone(char board[ROWS][COLS], int num,int row,int col)
{
while (num>0)
//我们事先设置好地雷数量,直到地雷全部布置完毕,退出循环
{
int i = rand() % row + 1;
int j = rand() % col + 1;
//与srand使用,生成随机地址
if (board[i][j] != '1')
//判断该处是否已经被布置过地雷了,没有则向下执行
{
board[i][j] = '1';
//布置地雷
num--;
//布置完成后,自然地雷数减一
}
}
}
说明:布置地雷过程,应该只发生在布置地雷网格,我们可以在传参时进行控制。循环控制将所有地雷都布置完毕,条件控制避免发生某处重复布置地雷。
5、周边地雷数统计
int count(char board[ROWS][COLS], int x, int y)
{
return board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] + board[x][y - 1] + board[x][y + 1] + board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] - 8 * '0';
//返回周边地雷数量,返回一个整形值
}
6、玩家扫雷
char player_do(char board[ROWS][COLS], char bone[ROWS][COLS], int x, int y)
{
if (board[x][y] == '1')
//判断用户是否碰雷,是的话返回 ‘g'表示游戏结束 给出提示
{
printf("很遗憾,你被炸死了!\n");
return 'g';
}
//否则在用户展示网格中展示周围地雷数,返回’c'表示游戏 继续进行
else
{
bone[x][y] = '0' + count(board, x, y);
return 'c';
}
}
说明:网格中只能存放字符类型的值,为了表示周围地雷数,我们先求出数量,再加上 ’0‘ 即可得到这个数字的字符表示。例如 : 8+’0‘ -> ‘8’
(3)main.c
#include "game.h"
//游戏主题逻辑
void game()
{
srand((unsigned int)time(NULL));
//定义两个网格,board表示地雷分布网格,bone表示用户展示网格
char board[ROWS][COLS];
char bone[ROWS][COLS];
//用'0'初始化11X11的地雷分布网格,'*'初始化11X11的用户展示网格
init(board, ROWS, COLS,'0');
init(bone, ROWS, COLS, '*');
//在向玩家展示的9X9的网格中布置地雷
put_bone(board,NUM,ROW,COL);
//打印用户展示网格,初始全为'*'
print(bone, ROW, COL);
//MAX-NUM表示用户获得胜利需要扫雷的 次数
int i = MAX-NUM;
//while循环可以控制在用户没有踩雷的条件下,让用户一直游戏,直到踩雷或者获胜
while (i > 0)
{
int x = 0, y = 0;
printf("请选择要扫描的位置->");
scanf("%d %d", &x, &y);
//考虑玩家可能重复扫同一个网格,用if语句控制
if (bone[x][y] != '*')
{
printf("该位置已经被扫过啦!请重试!\n");
continue;
//continue,防止用户重复扫描记作一次有效扫雷
}
char r=player_do(board, bone, x, y);
if (r =='g')
{
//'g' 表示踩雷了,那么直接退出循环,结束本次游戏
print(board, ROW, COL);
break;
}
else
{
//'c' 表示游戏继续,玩家可以进行下一次扫雷
print(bone, ROW, COL);
i--;
}
}
if (i == 0)
{
printf("恭喜,扫雷成功!\n");
print(board, ROW, COL);
}
}
//开始游戏
void game_play()
{
int input = 0;
do
{
menu();
printf("请选择-> ");
scanf("%d", &input);
switch (input)
{
case 1:
game();//找到game函数,进入游戏主体部分。
break;
case 0:
printf("退出成功!");
break;
default:
printf("非法输入!请重新输入-> ");
break;
}
} while (input);
//do while 语句控制玩家可以选择退出、继续游戏
}
int main()
{
game_play();
return 0;
}
说明:初始化的时候,我们要将 11X11 的网格全部初始化才方便我们之后的操作。而我们只需要将 9X9 的网格展示给玩家,这就需要我们传参时注意。同样布置地雷也应该发生在 9X9 的网格中。另外,我们扫雷过程中要防止玩家重复输入同一位置,即无效输入。处理方式见上方代码。关于游戏结束标志的判断,我们采用计数的方式,用户找到所有不含雷的位置即为胜利。
运行结果
(为了结果方便展示,我们布置80颗雷,这样我们只需要扫一次就可以出结果。并且在布置地雷后将地雷分布网格,也用print函数打印出来。这样我们可以精准得到地雷位置)
具体实现只要将#define NUM 值设置为80
需求分析(进阶)
- 在原有基础上进行游戏,我们发现一局游戏可能会花费玩家大量时间,并且存在没有办法获胜的可能。我们做出改善,节省玩家时间,同时也增加游戏获胜的可能性。我们作出改动如下:如果用户扫的区域无雷,并且周围无雷,则自动向外展开,直到出现周围一圈有雷为止,自动将周围地雷数量填在周围有雷的网格上。这样可以大大增快游戏进度。
需求实现(进阶)
- 说明:根据需求,我们可以知道,如若玩家翻开周围无雷的区域,我们要将周围区域一同展开。首先从周围八格开始,继而研究这八个网格周围是否有雷,若没有再以此格为中心向外扩展,直到外缘被数字包裹。
很容易想到用递归去处理这样的问题,我们选中一格后只要递推周围八格即可,直到出现周围有地雷的情况。但是我们很开就会发现,如果递归仅仅只是这样很容易出现死递归的情况。这就需要我们给出限制条件来解决死递归的问题。
具体实现(进阶)
- player_do函数改动
char player_do(char board[ROWS][COLS], char bone[ROWS][COLS], int x, int y)
{
if (board[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
return 'g';
}
else
{
//如果board[x][y]!='1'表示该处没有炸弹,下面调用unflod递归函数
unfold(board,bone, x, y, '0' + count(board, x, y));
return 'c';
}
}
说明:这里要注意我们传的参数,我们将 ‘0’+count(board, x, y) 传给 char r,目的是在进入unflod函数后进行判断。r==‘0’ 说明周围没有地雷,则进行递推步骤,否则将地雷数量展示。
- 递归函数unflod
void unfold(char board[ROWS][COLS],char bone[ROWS][COLS],int x,int y,char r)
{
if (r == '0')
//r表示附近地雷数,只有附近无雷才可以进行递推
{
if (bone[x][y] != ' ')
//判断条件,避免死递归,如果bone[x][y]==' ' 表示该处被处理过了,如果继续递推就会发生死递归。
{
bone[x][y] = ' ';
//满足继续递推条件后先将该处赋空,表示已经被处理过,下次遇到后不再处理这个网格
unfold(board, bone, x - 1, y - 1, '0' + count(board, x - 1, y - 1));
unfold(board, bone, x - 1, y , '0' + count(board, x - 1, y ));
unfold(board, bone, x - 1, y + 1, '0' + count(board, x - 1, y + 1));
unfold(board, bone, x , y - 1, '0' + count(board, x , y - 1));
unfold(board, bone, x , y + 1, '0' + count(board, x , y + 1));
unfold(board, bone, x + 1, y - 1, '0' + count(board, x + 1, y - 1));
unfold(board, bone, x + 1, y , '0' + count(board, x + 1, y ));
unfold(board, bone, x + 1, y + 1, '0' + count(board, x + 1, y + 1));
}
}
else
{
bone[x][y] = '0' + count(board, x, y);
}
}
实现效果
完整版代码请移步:GitHub