首先进行规化,三子棋的实现中已说明过的点在此处不在赘述
一.菜单
相关作用在实现三子棋时已进行过概述,此处直接上代码:
void menu()
{
printf("***************扫雷小游戏 ***************\n");
printf("*************** 1. play ***************\n");
printf("*************** 0. exit ***************\n");
printf("*************** V 1.0.0 ***************\n");
}
二.菜单选项基本运行逻辑
同上,直接上代码
int input = 0;
do
{
menu();
printf("请输入选项>:\n");
scanf("%d", &input);
switch (input)
{
case 1:
printf("打电动咯\n");
game();
break;
case 0:
printf("游戏结束,欢迎下次游玩\n");
break;
default:
system("cls");
printf("输入错误,请重新输入\n");
break;
}
} while (input);
三.game()函数-游戏的实现
扫雷游戏的实现同样可分为以下几个部分:
1.棋盘实现 2.输入实现-玩家 3.输赢判断
一.棋盘实现
1.那么效仿三子棋,直接定义一个棋盘,然后进行初始化,放置地雷,打印......等等,是否有哪里不对?
我们玩一次扫雷时,地雷的位置应该是确定的,但是作为玩家我们应该是看不见的,而我们要做的就是进行排雷。试想一下,假如就这么定义一个棋盘实现功能,那地雷位置生成后,要进行棋盘打印,如何让地雷对玩家来说不可视呢?
比较好的思路是,创建两个相同大小的棋盘,一个为"实际棋盘",存放着地雷的信息,一个为"影棋盘",用于在每次排雷后打印出来给玩家提供信息。
#define ROW 9
#define COL 9
//1.棋盘实现 实际棋盘 显示棋盘
char board[ROW+2][COL+2] = { 0 };
char board_shadow[ROW+2][COL+2] = { 0 };
此处需要注意的是棋盘大小的设置。假如我们需要对9*9的棋盘进行排雷,那么我们应该创建多大的数组呢?
9*9? 如果我们这样创建的话,试想一下,当我们进行排雷时,应对周围8个格子中的地雷数目进行检视并打印,若排查格位于中央,自然写个函数对周围8格计数即可。但若排查格位于边缘,此时想通过这个函数对周围8格计数,就会造成数组越界!
因此最好的做法是,创建一个11*11的数组,使外面形成一圈"保护圈",虽然我们并不需要对其进行打印和排查。
2.棋盘初始化,遍历数组即可,不再赘述,上代码
void Init_board(char board[ROW+2][COL+2])
{
int i = 0;
int j = 0;
for (i = 0; i < ROW+2; i++)
{
for (j = 0; j < COL+2; j++)
{
board[i][j] = ' ';
}
}
}
需注意对2个数组均应调用此函数初始化
3.棋盘打印,与三字棋思路一致,依旧是分割线与棋盘列,只不过样式稍有差异
注意我们的数组大小为(ROW+2)*(COL+2),而实际上我们想要的雷阵大小为(ROW)*(COL),最外圈元素为无效元素。因此我们只需产生(1~ROW)*(1~COL)的随机数即可,下标为1即为数组第2行,下标为ROW即为倒数第2行,正好为除去外圈的雷阵
void print_board(char board[ROW+2][COL+2])
{
int i = 0;
int j = 0;
printf(" ");
for (j = 1;j <= COL; j++)//列信息显示
{
printf("%3d ", j);
}
printf("\n");
for (i = 1; i <= ROW + 1; i++)
{
printf(" ");
for (j = 1; j <= COL + 1; j++)
{
printf("|");
if (j <= COL)
printf("---");
}
printf("\n");
if (i <= ROW)
{
printf("%4d", i); //行信息显示
for (j = 1; j <= COL + 1; j++)
{
printf("|");
if (j <= COL)
printf(" %c ", board[i][j]);
}
}
printf("\n");
}
}
不同点在于,为了方便玩家输入坐标,在最开头采用简单循环实现列数信息打印,循环内printf语句实现行数信息打印,效果如下:
4.随机生成雷
#define Num_Boom 1
此处为便于修改,地雷数目也用标识符变量定义
以下为随机产生地雷函数:
void generate_boom(char board[ROW+2][COL+2])
{
int count = Num_Boom;
int x = 0;
int y = 0;
while (count)
{
x = rand() % ROW + 1;
y = rand() % COL + 1;
if (board[x][y] != '*')
{
board[x][y] = '*';
count--;
}
}
}
与三子棋类似,用rand()函数产生随机数并判断合法性,若合法(即此处还未放置地雷),则修改数组元素,此处将字符'*'作为地雷。每修改一次count计数器自减1,当count==0时,则说明指定地雷数已全部放置,循环结束。
二.玩家输入/判断输赢
由于扫雷只需玩家输入->判断输赢->玩家输入->判断输赢,而非三子棋玩家和电脑均需判断输赢,此处将玩家输入和判断输赢集成为一个函数。即玩家输入后,判断输赢。若胜负未分,则继续输入,否则给出结果。
void player_input(char board[ROW + 2][COL + 2], char board_shadow[ROW + 2][COL + 2])
{
printf("雷数一共为: %d\n", Num_Boom);
int x = 0;
int y = 0;
int count=0;
while (1)
{
printf("请输入排查坐标>: (x y)\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= ROW && y >= 1 && y <= ROW)
{
if (board_shadow[x][y] == ' ')
{
if (board[x][y] == '*')
{
system("cls");
printf("很遗憾,排雷失败\n");
printf("此次排雷情况如下>:\n");
print_board(board);
break;
}
else
{
int ret = num_boom(board, x, y);
if (ret == 0)
{
auto_open(board, board_shadow, x, y);
}
else
{
board_shadow[x][y] = num_boom(board, x, y) + 48;
board[x][y] = num_boom(board, x, y) + 48;
}
if (is_full(board_shadow))
{
system("cls");
printf("恭喜你,排雷成功\n");
print_board(board);
break;
}
system("cls");
printf("此次成功排查,请继续>:\n");
print_board(board_shadow);
}
}
else
printf("此处已进行排查,请输入下一排查坐标\n");
}
else
printf("输入坐标非法,请重新输入\n");
}
}
基本逻辑即:玩家输入,判断合法性,若不合法则重新输入
若合法,则检视其是否为雷,若为雷,则玩家输,游戏结束。
否则不为雷,需要检视这个排雷格周围的8个元素,并将其打印在"影棋盘"上给玩家提供信息,即修改"影棋盘"->打印"影棋盘"
此处先岔开话题,让我们来实现这个检视周围雷数的功能。只需遍历周围8格即可,若为存在格中元素为'*',则计数器count自增1,最终返回计数器count的值 (此处循环对本排查格中元素也进行了检索,此时本格必不为雷,不影响结果)
int num_boom(char board[ROW + 2][COL + 2], 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 (board[i][j] == '*')
count++;
}
}
return count;
}
回到前文,调用此函数获得周边雷数信息后,直接修改"真实棋盘"和"影棋盘"的值,注意棋盘为字符数组创建,而此函数返回值为整型int,因此修改时应为x=y+48; 48为字符0的ascii码值,则x为整型变量y的字符形式。
之后判断是否满足排雷成功条件,即"影棋盘"中剩余' '元素的个数是否已经等于雷数Num_Boom,若已经相等,那么剩下的' '必然全都是雷(不然游戏在此之前就结束了),用is_full()函数进行计数
int is_full(char board[ROW + 2][COL + 2])
{
int i = 0;
int j = 0;
int count = 0;
for (i = 1; i <= ROW; i++)
{
for (j = 1; j <= COL; j++)
{
if (board[i][j] == ' ')
count++;
}
}
if (count == Num_Boom)
return 1;
else
return 0;
}
至此,若满足胜利条件,则进入下方的if语句,提示扫雷成功,跳出循环,简单的扫雷已经可以运行起来了
但是,当我们玩玩后就会发现,我们平时玩的扫雷,有时候我们排查一个坐标,电脑会自动在附近展开一大片,给了更多的有效信息,而我们的代码至此并没有自动展开功能。如何实现此功能呢?
首先我们要明白自动展开的条件:当排查坐标周围雷数为0时,即对周围8个待排格进行检索并排查,不为0时则直接提供信息。 那么当对周围待排格进行排查操作时,同样有当排查坐标周围雷数为0时,即又对周围8个待排格进行检索并排查.........
这显然形成了一个递归。因此我们用递归算法来实现。
当雷数为0时和不为0时情况不同,故需要if语句进行判断。雷数不为0时则进行前文操作即可,若雷数为0时则调用自动展开函数
if (ret == 0)
{
auto_open(board, board_shadow, x, y);
}
else
{
board_shadow[x][y] = num_boom(board, x, y) + 48;
board[x][y] = num_boom(board, x, y) + 48;
}
即这一段代码。
接下来对自动展开函数auto_open进行实现,由于需要对2个棋盘进行修改,故需传参2个函数及坐标x,y。下面为实现:
void auto_open(char board[ROW + 2][COL + 2], char board_shadow[ROW + 2][COL + 2], int x, int y)
{
if (x >= 1 && x <= ROW && y >= 1 && y <= COL && board [x][y]== ' ')//重大bug!!!!!
{
int ret = num_boom(board, x, y);
board[x][y] = ret + 48;
board_shadow[x][y] = ret + 48;
if (ret == 0)
{
int i = 0;
int j = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
auto_open(board, board_shadow, i, j);
}
}
}
}
}
实现逻辑为:1.判断坐标是否合法(否则递归会到棋盘外圈,因此棋盘所有元素都将被自动排查,就是这个一开始没写,忽略了外圈元素,使我绞尽脑汁的想bug出在哪了?) 2.判断此元素是否为雷
若坐标不合法或元素为雷,那当然不需要自动展开
若坐标合法且不为雷,则此时对此坐标进行排查,并将信息输入数组,接下来判断,若雷数不为0,则不需要自动展开,此次函数结束。雷数为0,则对周围8个元素进行函数递归即可
至此自动展开函数auto_open便已经实现
void game() //1.棋盘实现 2.输入实现-玩家 3.输赢判断
{
//1.棋盘实现 实际棋盘 显示棋盘
char board[ROW+2][COL+2] = { 0 };
char board_shadow[ROW+2][COL+2] = { 0 };
//棋盘初始化
Init_board(board);
Init_board(board_shadow);
//棋盘打印
//随机生成雷 雷:*
generate_boom(board);
print_board(board_shadow);
//玩家输入/判断输赢
player_input(board,board_shadow);
}
game()函数对这些功能的函数进行调用,完美的实现了扫雷的功能。
最后附上头文件:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9 //行
#define COL 9 //列
#define Num_Boom 10 //地雷数目
void game();
void Init_board(char board[ROW+2][COL+2]);
void print_board(char board[ROW+2][COL+2]);
void generate_boom(char board[ROW+2][COL+2]);
void player_input(char board[ROW+2][COL+2], char board_shadow[ROW+2][COL+2]);
int num_boom(char board[ROW + 2][COL + 2],int x,int y);
int is_full(char board[ROW + 2][COL + 2]);
void auto_open(char board[ROW + 2][COL + 2], char board_shadow[ROW + 2][COL + 2], int x, int y);
int num_message(char board[ROW + 2][COL + 2], int x, int y);
效果图展示: