一. 前言 本章我们用C语言简单实现一个9×9雷阵的扫雷小游戏。 说到扫雷想必大家都不陌生,开始我们点击一个小方块,那么这个小方块上会显示它周围八个小方块里雷的个数,凭借这
一. 前言
- 本章我们用C语言简单实现一个9×9雷阵的扫雷小游戏。
- 说到扫雷想必大家都不陌生,开始我们点击一个小方块,那么这个小方块上会显示它周围八个小方块里雷的个数,凭借这个数值,在判断下一个要扫的位置(当然可能前面两步需要我们的幸运成分了),当我们除雷外的所有小方块都已经显示完了,那么扫雷成功。
- 这里我们分框架(框架代码在主函数中)展示游戏的实现。
- 游戏实现我们分装两个 .c (代码主函数与函数定义源代码)后缀的文件和一个 .h 的文件(头文件,函数声明)
- .h : game.h
.c : test.c (主函数体文件) |||||| game.c (函数定义文件)
- 这里我们先把头文件里的库函数,函数声明和定义的全局常量以及主函数的函数调用结构展示出来,以便后面有迹可循:
#define _CRT_SECURE_NO_WARNINGS
#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_COUNT 10
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 add_mine(char board[ROWS][COLS], int row, int col);
void down_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
#define _CRT_SECURE_NO_WARNINGS
// 1.初始化雷盘与扫雷盘
// 2.打印盘
// 3.布置雷
// 4.扫雷
#include "game.h"
void menu()
{
printf("****************************************\n");
printf("****************************************\n");
printf("***********>>>> 1.PLAY <<<<***********\n");
printf("***********>>>> 0.EXIT <<<<***********\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(mine, ROW, COL);
print_board(show, ROW, COL);
// 布置雷
add_mine(mine, ROW, COL);
print_board(mine, ROW, COL);
// 扫雷
down_mine(mine, show, ROW, COL);
}
void test()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:>>>>>> ");
scanf("%d", &input);
switch (input)
{
case 1:
printf("进入扫雷游戏:>>>>>>\n");
game();
printf("游戏结束:>>>>>>\n");
Sleep(1000);
system("cls");
break;
case 0:
printf("退出游戏:>>>>>>\n");
break;
default:
printf("选择错误请重新选择:>>>>>>\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
- 只要我们分别在另外两个 .c 文件(一个是主函数体一个是函数定义)中引入(#include “game.h”) 那么三个文件就相互作用了。
二. 游戏版面与开始游戏的构建
- 我们首先要打印一个菜单供玩家选择<输入1>则进入游戏,<输入0>则退出游戏,<输入其它的数>则输入错误,然后继续输入判断。为了一开始就让用户先选择,再判断输入的值然后判断是否再次输入,这里我们采用do-while循环结构,无论如何用户先选择一次,然后do-while里头采用switch-case来判断输入的值,而菜单我们调用一个menu()函数来打印,下面是在主函数里的代码实现:
#include "game.h"
void menu()
{
printf("****************************************\n");
printf("****************************************\n");
printf("***********>>>> 1.PLAY <<<<***********\n");
printf("***********>>>> 0.EXIT <<<<***********\n");
printf("****************************************\n");
printf("****************************************\n");
}
void test()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:>>>>>> ");
scanf("%d", &input);
switch (input)
{
case 1:
printf("进入扫雷游戏:>>>>>>\n");
printf("游戏结束:>>>>>>\n");
break;
case 0:
printf("退出游戏:>>>>>>\n");
break;
default:
printf("选择错误请重新选择:>>>>>>\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
- 我们可以看到, 这样简易的游戏初始菜单就制作好啦。
三. 扫雷初始化思路
- 由上方雷盘可以看到是一个9×9的格子,但是我们在写程序时,如何能够判断9×9边边格子的周围八个格子雷的个数?如果我们定义一个二维数组只给它9行9列这样一个起始雷盘,当我们在扫边边格子的雷的时候,我们可能会需要一些复杂的计算来完成相关的程序,所以,为了更好的简易化以及能够精确判断每个格子周围的雷,我们在雷盘两边再加上两行两列,也就是说,我们定义的二维数组他应该是要11行11列这样的起始雷盘。
- 我们游戏的雷盘是9×9的,但我们定义的二维数组是11×11的,所以,当我们在玩游戏时,被扫的雷盘的打印应该少去上下两行和左右两列。
- 对于程序来说,我们是否能够将布置雷盘与扫雷放在一个盘里实现呢?答案是不能的,所以这里我们要定义两个二维数组,一个用来放雷的盘不打印,另一个用来排查雷的盘要打印,并且,这两个盘之间一定要能通过某种联系来相互作用(雷盘传雷位置,扫雷盘接受,扫时能够准确定位周围雷的个数)。
- 这里我们设定,雷用字符 ‘1’ 来表示,其余地方为字符 ‘0’ ,刚开始初始化盘时,要布置雷的盘先全部元素放 ‘0’ ,用来扫雷的盘全部元素放字符 ‘ * ’ ,但是我们打印时,只打印9×9那块的区域(游戏版面设定)。
四. 雷阵与扫雷盘初始化
*由上说到,雷阵我们先全部放入字符 '0' 初始化,扫雷盘全初始化字符 ‘ * ’,由于两个盘的数组大小相同,为了减少代码量,这里写个初始化函数调用两次(雷阵初始化的调用和扫雷盘初始化的调用),所以我们需要传参每个盘的初始化内容(字符)。 以下是代码实现:
init_board(mine, ROWS, COLS, '0'); // 雷阵的初始化
init_board(show, ROWS, COLS, '*'); // 扫雷盘的初始化
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; // 初始化
}
}
}
五. 打印扫雷盘
- 在我们玩扫雷游戏中,其显示的是9×9的格子,所以我们打印的扫雷盘也是要9×9的格子(尽管初始化二维数组为11×11,那是因为便于计算扫的周围那个格子周围的雷数),但是为了玩家更好的看清楚想要扫的格子的坐标,这里在打印的每行每列开头都标上序号,这样就不用去数格子了。
- 以下是打印出来的效果: 以下是代码实现:
print_board(show, ROW, COL); // ROW = 9 ; COL = 9
void print_board(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (j = 0; j <= row; j++) // 打印列的序号
{
printf("%d ", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d", i); // 每打印一行初始化打印对应行数的序号
for (j = 1; j <= col; j++)
{
printf(" %c", board[i][j]);
}
printf("\n");
}
}
六. 布置雷
- 布置雷这一步骤需要在雷阵实现,这里规定布置十个雷,由于我们每一把游戏的雷的位置是不同的(随机的),所以布置雷这一步骤需要电脑的随机性来布置,相应函数的使用为 rand,srand(为了使rand随机化需要用到的),time(时间戳),具体的介绍在上一节”C语言实现初级三子棋“有说到,所以这里便不再详细道来。
- 由于是坐标定位,所以这里我们定义两个变量来接受电脑产生的随机值以作为二维数组的坐标来放置雷。
- 电脑随机布置时,我们应该使用循环让电脑不断布置,当选中的格子是 ‘0’ ,便放雷,这样的动作做十次,但如果有重复就会另外找位置布置,所以实际上循环可能不止十次。
- 每随机布置一个雷,我们便在初始化为字符 ‘0’ 的雷阵并且随机选中布置雷的格子上放上字符 ‘1’ ,这样我们便能分清楚雷与不是雷的区别,这样也有益于后面扫雷阶段的实现。
- 由于是在9×9的范围来布置雷,所以这里我们需要限定电脑随机产生值的范围。
- 当然如果想要看看布置雷的效果,在布置完后可以打印一下雷阵,但最终我们是不会打印的。 以下是代码实现(代码展现是本模块的全部代码实现的展现,与其他功能的结构组合须看全代码)(也就是这三段代码是分开的):
add_mine(mine, ROW, COL);
srand((unsigned int)time(NULL));
void add_mine(char board[ROWS][COLS], int row, int col)
{
int count = 0;
while (count < MINE_COUNT) // MINE_COUNT = 10(雷的个数)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count++; // 每布置一个计数一次
}
}
}
七. 扫雷
- 我们通过坐标选择需要扫的格子,当格子里的字符不是 ’1‘ 时,计算周围八个格子的雷数,值得注意的是,我们的选择和计算判断过程都是需要在雷阵盘上完成,但是我们展现出来的是这个格子周围的雷数,这就需要扫雷盘和雷盘建立起联系,一个计算分析判断,另一个接受展示,所以我们在写这么一个扫雷的函数时,两个盘的相关消息都要传递过去,这样便于两盘之间的相互联系,从而达到扫雷效果。 下面是代码展示:
down_mine(mine, show, ROW, COL); // 一个盘计算判断,另一个接受展示
int around_mine(char mine[ROWS][COLS], int x, int y)
{
return (mine[x][y + 1] + mine[x][y - 1] + mine[x + 1][y] + mine[x - 1][y] + mine[x - 1][y - 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] + mine[x + 1][y - 1] - 8 * '0');
}
void down_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = 0;
while (1)
{
printf("请输入要扫雷的坐标:>>>>>> ");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
break;
}
else
{
int ret = around_mine(mine, x, y);
show[x][y] = ret + '0';
count++;
}
}
else
{
printf("输入坐标不合法,请重新输入:>>>>>>\n");
}
if (count == ((row * col) - MINE_COUNT))
{
printf("扫雷成功!\n");
break;
}
}
}
可以看到,这里面又使用了一次函数定义调用,这次定义调用是为了计算周围的雷数,而我们扫雷盘上放的是字符,那么雷数如何转化到字符,这需要字符与整型数字之间的转化关系来完成。具体可看ASCLL码表。
八. 整个程序的代码实现:
- game.h:
#define _CRT_SECURE_NO_WARNINGS
#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_COUNT 10
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 add_mine(char board[ROWS][COLS], int row, int col);
void down_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
- test.c(主函数)(框架结构):
#define _CRT_SECURE_NO_WARNINGS
// 1.初始化雷盘与扫雷盘
// 2.打印盘
// 3.布置雷
// 4.扫雷
#include "game.h"
void menu()
{
printf("****************************************\n");
printf("****************************************\n");
printf("***********>>>> 1.PLAY <<<<***********\n");
printf("***********>>>> 0.EXIT <<<<***********\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(mine, ROW, COL);
print_board(show, ROW, COL);
// 布置雷
add_mine(mine, ROW, COL);
// print_board(mine, ROW, COL);
// 扫雷
down_mine(mine, show, ROW, COL);
}
void test()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:>>>>>> ");
scanf("%d", &input);
switch (input)
{
case 1:
printf("进入扫雷游戏:>>>>>>\n");
game();
printf("游戏结束:>>>>>>\n");
Sleep(1000);
system("cls");
break;
case 0:
printf("退出游戏:>>>>>>\n");
break;
default:
printf("选择错误请重新选择:>>>>>>\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
- game.c(函数的定义:模块功能的实现):
#define _CRT_SECURE_NO_WARNINGS
#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_board(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (j = 0; j <= row; j++)
{
printf("%d ", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d", i);
for (j = 1; j <= col; j++)
{
printf(" %c", board[i][j]);
}
printf("\n");
}
}
void add_mine(char board[ROWS][COLS], int row, int col)
{
int count = 0;
while (count < MINE_COUNT)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count++;
}
}
}
int around_mine(char mine[ROWS][COLS], int x, int y)
{
return (mine[x][y + 1] + mine[x][y - 1] + mine[x + 1][y] + mine[x - 1][y] + mine[x - 1][y - 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] + mine[x + 1][y - 1] - 8 * '0');
}
void down_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = 0;
while (1)
{
printf("请输入要扫雷的坐标:>>>>>> ");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
break;
}
else
{
int ret = around_mine(mine, x, y);
show[x][y] = ret + '0';
count++;
}
}
else
{
printf("输入坐标不合法,请重新输入:>>>>>>\n");
}
if (count == ((row * col) - MINE_COUNT))
{
printf("扫雷成功!\n");
break;
}
}
}
九. 总结
- 咱们这里实现的扫雷只是初级的,游戏里点击一个格子它可能扫出一大块,这需要更复杂的算法来实现,所以扫雷小游戏值得我们继续研究,这可以提高我们的思维和能力。
- 经过扫雷和三子棋两个小游戏的对比,我们可以发现其相同点和不同点,其算法的差异和框架的异同,独自完成这两个简易的小游戏可以大大提高我们的思维能力和代码的敏感性,建议大家能够独自完成。
- 本章到此结束,谢谢大家。