先来看一下我们实现的扫雷
!!!我们一起来学习 !!!
目录
一.游戏介绍
二.游戏框架
三.具体实现
1.进入游戏
(1)菜单的打印
(2) 选择游戏
2.生成雷盘
3.初始化雷盘
4.打印雷盘
5.布置雷盘
6.排查雷盘
(1)确定一个棋子的周围雷的个数
(2)排查雷
7.进行优化
(1)递归爆开
(2)特定标记
(3)排雷函数的改写
(4)增加清屏操作
四.完整代码
1.game.h
2.game.c
3.test.c
扫雷游戏相信大家都玩过,上图就是一个网页版的扫雷,它的规则是玩家选择一个方格,若此方格没有地雷,那么该方格会显示与它相邻的八个方格中雷的个数,若此方格有地雷,那么游戏失败,当玩家把除了有地雷的方格外的其他方格都成功翻开时,游戏胜利
test.c:游戏逻辑的测试,包含游戏菜单的打印,游戏设计的基本逻辑的展示。
game.c:游戏功能的具体实现,这部分是整个游戏的核心代码,一般不会展示给用户。
game.h:相关头文件的包含、符号的声明以及函数的声明。
这里我们写三个游戏了,我们最开始的时候都是菜单和进入游戏的选择,都是一样的道理
打印菜单依旧是我们的老方法
void menu()
{
printf("____________________________________\n");
printf("<$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>\n");
printf("<$$$$$$ $$$$$$>\n");
printf("<$$$$$$ 1.play $$$$$$>\n");
printf("<$$$$$$ 0.exit $$$$$$>\n");
printf("<$$$$$$ $$$$$$>\n");
printf("<$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>\n");
printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
}
选择游戏也依旧是我们的老方法,while循环加上switch语句来构成选择,里面的游戏函数先不要管
void test()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("欢迎下次再玩!\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
#define ROW 9//行
#define COL 9//列
#define ROWS ROW+2
#define COLS COL+2
我们定义了两个棋盘,分别用来保存布置雷的信息和排查雷的信息,这样就可以避免二者相互干扰或者相互覆盖;
同时,我们使用宏来定义雷盘的大小以及雷的个数,这样做的好处是当我们以后想使用更大的雷盘或者想增加扫雷的难度的时候,我们只需要改动这里一次即可,增加了代码的可维护性。
我们来解释一下为什么会在第二个定义里面加上2呢
如图:当我们排查1位置时,如果1处不是雷,那么我们就会依次检查1周围8个坐标是否有地雷,如果有,就会把地雷的数量显示在1位置处;但是当我们排查2位置时,我们发现, 数组排查雷时会发生越界,所以为了避免数组越界,我们就需要增加一系列限制条件,这样做无疑是比较麻烦的,所以有的大佬就想出了这样一种办法:在定义数组长度时我们直接在上下左右四个方向各多给一行的空间,并把这些空间中的数据初始化为非雷,这样,就轻松解决了数组越界的问题。
这个时候就可以在我们的game函数里面开始写我们的数组了
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
}
最开始的时候我们把mine数组元素全部初始化为字符0,把show数组元素全部初始化为字符*,我们打印出来的是show数组,是为了给玩家看的,而mine数组,是为了我们方便去写代码,和传值用的
void init_board(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;
}
}
}
因为我们两个数组都需要初始化,但是初始化的数值是不一样的,我们如果写两个初始化函数,会显得非常啰嗦,所以我们直接增加一个参数,然后传到初始化函数里面在去调用。
这个时候我们game函数里就是
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
init_board(mine, ROWS, COLS, '0');
init_board(show, ROWS, COLS, '*');
}
打印雷盘的时候,我们实际玩的是9*9的雷盘,所以我们只需要打印9*9的就好啦
为了方便我们看清楚我们排雷是在几行几列,我们就加上对应的行和列数,再加上一点修饰就好啦
void print_line(int col)
{
for (int i = 0; i <= col; i++)
{
printf("----");
}
printf("\n");
}
void print_board(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf("--------扫雷----------\n");
for (j = 0; j <= col; j++)
{
printf(" %d ", j);
}
printf("\n");
print_line(col);
for (i = 1; i <= row; i++)
{
printf("%-3d|", i);
for (j = 1; j <= col; j++)
{
printf(" %c |", board[i][j]);
}
printf("\n");
print_line(col);
}
printf("--------扫雷----------\n");
}
这里面是两个函数,因为我们想让我们的雷盘更好看一点,我们加了分割线。
第一是用于随机生成坐标的rand函数的种子srand函数只需要在main函数中声明一次即可。
第二是我们在布置雷的时候需要检查该位置是否已经有雷,避免重复布置。
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int count = MINE_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
这里的我们只需要9*9,就跟打印棋盘的时候是一样的,我们game里面传来的是ROWS和COLS,所以我们接收的时候也需要,我们只需要在接收后面的参数的时候,接收row和col就可以了
生成随机的雷,我们拉出来
这里是我们生成雷的坐标,在我们二维数组里面,我们实际玩的雷盘就是9*9,所以我们生成1~9
int x = rand() % row + 1;
int y = rand() % col + 1;
循环的部分,我们拉出来
int count = MINE_COUNT;
while (count)
第一个是我们写的count,是为了方便看我们布置了多少个雷了,也方便我们后面循环布置雷,来当判断的条件,所以我们在头文件就要加上雷的数量的定义
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE_COUNT 10
可以用循环来写,去判断周围有多少个雷,再去返回,是字符1的就是雷
int get_mine(char show[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int count = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '1')
{
count++;
}
}
}
return count;
}
我们直接用返回来写
这个理解就是,我们在写mine和show这两个数组的时候,都定义的是字符类型的数组,方便我们传参数,这里也是一样的,我们在定义雷和非雷的时候,就是字符‘1’和字符‘0’,来写的,所以我们这里也需要转化为数组传过去有几个雷,下面再用到字符打印的时候,我们再转化为字符就好啦。
int get_mine(char show[ROWS][COLS], int x, int y)
{
return (show[x - 1][y - 1] + show[x - 1][y] + show[x - 1][y + 1] + show[x][y - 1]
+ show[x][y + 1] + show[x + 1][y - 1] + show[x + 1][y] + show[x + 1][y + 1] - 8 * '0');
}
数字化为字符数字,就是加上‘0’
字符数组化为数字,就是减去‘0’
排查雷的时候我们首先需要让用户输入需要排查的坐标,然后判断坐标的合法性及该坐标是否已被排查,其次再判断该坐标是否有雷,如果没有,就递归检查它周围的坐标,直到遇到有雷的坐标才停止递归,再让用户选择是否需要标记雷的信息,最后检查是否满足游戏胜利的条件。
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (win < (row * col - MINE_COUNT))
{
printf("请输入要排雷的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("输入坐标已排,请重新输入!\n");
continue;
}
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
print_board(mine, ROW, COL);
break;
}
else
{
int n = get_mine(mine, x, y);
show[x][y] = n + '0';
print_board(show, ROW, COL);
win++;
}
}
else
{
printf("输入坐标非法,请重新输入!\n");
}
}
if (win == (row * col - MINE_COUNT))
{
print_board(mine,ROW,COL);
printf("恭喜你,扫雷成功!\n");
}
}
中间有好几个判断语句,因为是我们输入的坐标,所以就会存在错误,我们就去判断:
首先是判断输入的坐标是否合法,是不是在我的雷盘里面
然后再判断我这个地方是不是排查过了
然后再判断这个地方是雷不是,是的话就游戏结束了
不是的话,就要显示出来这个地方周围的雷数
然后再打印出来,方便玩家看
最后我们出循环又有一个判断
可能是炸死,也可能是全部排完,所以我们加个判断条件win
我们总共循环71=81-10次,当我们已经循环了71次后,就代表我们已经将所有非雷区域全部排除了,我们在前面定义了一个count如果我们排对了就++,当达到了71次就代表我们已经将所有非雷区域全部排除了,所以我们就定义一个win,来去判断最后到底排完了没有,也可以当作我们循环找雷的判断条件。
这个时候我们的game函数里面就是,先去定义,然后初始化,设置雷,打印,找雷
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
init_board(mine, ROWS, COLS, '0');
init_board(show, ROWS, COLS, '*');
print_board(show, ROW, COL);
set_mine(mine, ROW, COL);
find_mine(mine, show, ROW, COL);
}
当然我们的头文件里面也要包含我们写的函数的定义
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE_COUNT 10
#include
#include
#include
#include
void init_board(char board[ROWS][COLS], int rows, int cols, char ret);
void print_board(char board[ROWS][COLS], int row, int col);
void set_mine(char mine[ROWS][COLS], int row, int col);
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
这个时候我们发现上面写的,已经可以基本实现我们的扫雷了,但是对比我们玩的,我们发现
当用户点击一个坐标,如果该坐标及其周围的坐标都没有雷,那么雷盘就会一次性展开一片,而这样设计也是比较合理的,因为如果每一个非雷坐标都需要玩家排查的话十分影响游戏体验;所以,这里我们就利用递归的实现模拟实现了这个功能。
在网页版的扫雷中,如果我们确定某一位置一定是雷时,我们可以利用标记功能来标识该坐标,方便我们后面的判断,本代码中,我们用字符 ! 来标识雷。
我们开始一个一个优化
需要:该坐标不是雷,该坐标周围没有雷,该坐标没有被排查过,该坐标赋值空白,对该坐标周围八个坐标进行递归操作
这个思路也非常的简单,也是先去判断坐标是否合法,如果我们第一开始排的不是雷,就去判断它周围的八个,同时把中间的变为空格,然后进行递归,当有雷的时候,我们就显示它周围的雷数就好了。
我们这里用了一个指针,因为我们要用win在我们排雷的函数里面,进行判断,看到底是不是全部排完了,所以我们这里用指针找到win,去修改win的值,因为我的递归爆开,也是我们排雷的次数,所以放到我们里面,当没有雷了,我们给这个地方写雷的个数的时候,也是一次排雷。
void boom_broad(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* pw)
{
int i = 0;
int j = 0;
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
int n = get_mine(mine, x, y);
if (n == 0)
{
(*pw)++;
show[x][y] = ' ';
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*')
{
boom_broad(mine, show, row, col, i, j, pw);
}
}
}
}
else
{
(*pw)++;
show[x][y] = n + '0';
}
}
}
特定标记也是一样的,我们先去判断坐标是否合法,是否是我们排查过的地方,只有没有排查过的地方才能标记,直接赋值为!就好
void mark_mine(char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入你想标记的位置的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
show[x][y] = '!';
break;
}
else
{
printf("该位置已被排查,请重现输入!\n");
}
}
else
{
printf("输入坐标非法,请重新输入!");
}
}
}
因为我们增加了两个优化,所以我们的排雷函数更改一些地方
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
int* pw = &win;
char ch = 0;
while (win < (row * col - MINE_COUNT))
{
printf("请输入要排雷的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("输入坐标已排,请重新输入!\n");
continue;
}
if (mine[x][y] == '1')
{
system("cls");
printf("很遗憾,你被炸死了!\n");
print_board(mine, ROW, COL);
break;
}
else
{
boom_broad(mine, show,row,col, x, y, pw);
system("cls");
print_board(show, ROW, COL);
printf("需要标记的雷的位置请输入y/Y,否则请输入任意键:>");
while ((ch = getchar()) != '\n');
scanf("%c", &ch);
if (ch == 'y' || ch == 'Y')
{
mark_mine(show, row, col);
system("cls");
print_board(show, row, col);
}
else
{
continue;
}
}
}
else
{
printf("输入坐标非法,请重新输入!\n");
}
}
if (win == (row * col - MINE_COUNT))
{
system("cls");
printf("恭喜你,扫雷成功!\n");
print_board(mine, ROW, COL);
}
}
我们把我们改的地方拉出来来看,把两次的都拉出来对比
第一次
else
{
int n = get_mine(mine, x, y);
show[x][y] = n + '0';
print_board(show, ROW, COL);
win++;
}
第二次
else
{
boom_broad(mine, show,row,col, x, y, pw);
print_board(show, ROW, COL);
printf("需要标记的雷的位置请输入y/Y,否则请输入任意键:>");
while ((ch = getchar()) != '\n');
scanf("%c", &ch);
if (ch == 'y' || ch == 'Y')
{
mark_mine(show, row, col);
system("cls");
print_board(show, row, col);
}
else
{
continue;
}
}
首先我们直接写上了我们的递归爆开,然后打印出来,因为这个时候已经实现了我们的递归,我们已经爆开了。
然后我们可以选择标记,也可以选择不标记,我们输入字符,但是,我们在输入字符的时候,我们回车键,会加上我们的‘\0’,所以我们要清理缓冲区,把里面先的清理掉,然后我们再输入,如果是y/Y,我们就进行标记的操作。
就引用我们的标记函数,然后再次打印出来就好了,如果我们不进行标记,我就直接返回上面的循环,进行排雷,重复操作,直到循环满71一次,或者你炸死了,停止判断输赢。
为了我们游戏的方便,我们增加了一些清屏的操作,好看便捷
加上睡眠函数,一段时间后进行清屏操作,记得引用头文件呀!!!
Sleep(6000);
system("cls");
#define _CRT_SECURE_NO_WARNINGS 1
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE_COUNT 10
#include
#include
#include
#include
void init_board(char board[ROWS][COLS], int rows, int cols, char ret);
void print_board(char board[ROWS][COLS], int row, int col);
void set_mine(char mine[ROWS][COLS], int row, int col);
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void init_board(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 print_line(int col)
{
for (int i = 0; i <= col; i++)
{
printf("----");
}
printf("\n");
}
void print_board(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf("-----------------扫雷-------------------\n");
for (j = 0; j <= col; j++)
{
printf(" %d ", j);
}
printf("\n");
print_line(col);
for (i = 1; i <= row; i++)
{
printf("%-3d|", i);
for (j = 1; j <= col; j++)
{
printf(" %c |", board[i][j]);
}
printf("\n");
print_line(col);
}
printf("-----------------扫雷-------------------\n");
}
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int count = MINE_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
int get_mine(char show[ROWS][COLS], int x, int y)
{
return (show[x - 1][y - 1] + show[x - 1][y] + show[x - 1][y + 1] + show[x][y - 1]
+ show[x][y + 1] + show[x + 1][y - 1] + show[x + 1][y] + show[x + 1][y + 1] - 8 * '0');
}
//int get_mine(char show[ROWS][COLS], int x, int y)
//{
// int i = 0;
// int j = 0;
// int count = 0;
// for (i = x - 1; i <= x + 1; i++)
// {
// for (j = y - 1; j <= y + 1; j++)
// {
// if (show[i][j] == '1')
// {
// count++;
// }
// }
// }
// return count;
//}
void boom_broad(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* pw)
{
int i = 0;
int j = 0;
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
int n = get_mine(mine, x, y);
if (n == 0)
{
(*pw)++;
show[x][y] = ' ';
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*')
{
boom_broad(mine, show, row, col, i, j, pw);
}
}
}
}
else
{
(*pw)++;
show[x][y] = n + '0';
}
}
}
void mark_mine(char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入你想标记的位置的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
show[x][y] = '!';
break;
}
else
{
printf("该位置已被排查,请重现输入!\n");
}
}
else
{
printf("输入坐标非法,请重新输入!");
}
}
}
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
int* pw = &win;
char ch = 0;
while (win < (row * col - MINE_COUNT))
{
printf("请输入要排雷的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("输入坐标已排,请重新输入!\n");
continue;
}
if (mine[x][y] == '1')
{
system("cls");
printf("很遗憾,你被炸死了!\n");
print_board(mine, ROW, COL);
break;
}
else
{
boom_broad(mine, show,row,col, x, y, pw);
system("cls");
print_board(show, ROW, COL);
printf("需要标记的雷的位置请输入y/Y,否则请输入任意键:>");
while ((ch = getchar()) != '\n');
scanf("%c", &ch);
if (ch == 'y' || ch == 'Y')
{
mark_mine(show, row, col);
system("cls");
print_board(show, row, col);
}
else
{
continue;
}
}
}
else
{
printf("输入坐标非法,请重新输入!\n");
}
}
if (win == (row * col - MINE_COUNT))
{
system("cls");
printf("恭喜你,扫雷成功!\n");
print_board(mine, ROW, COL);
}
}
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
printf("____________________________________\n");
printf("<$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>\n");
printf("<$$$$$$ $$$$$$>\n");
printf("<$$$$$$ 1.play $$$$$$>\n");
printf("<$$$$$$ 0.exit $$$$$$>\n");
printf("<$$$$$$ $$$$$$>\n");
printf("<$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>\n");
printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
init_board(mine, ROWS, COLS, '0');
init_board(show, ROWS, COLS, '*');
print_board(show, ROW, COL);
Sleep(6000);
system("cls");
set_mine(mine, ROW, COL);
find_mine(mine, show, ROW, COL);
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("欢迎下次再玩!\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}