一、扫雷玩法介绍
在下面方格中,有若干个雷随机分布在格子中,当玩家选择任意一个格子时,若格子不是“雷”,则会显示以此格子为中心,显示附近八个格子的“雷”的数量。
如果玩家选择的区域是“雷”则游戏结束
下面让我们用C语言一步步实现这个游戏
二、扫雷的逻辑实现
由游戏UI我们可以知道,我们需要准备一个格子盘(在下文统称“棋盘”),并在棋盘内设置若干个雷,当玩家选择棋盘上某个格子时,显示当前棋盘及其有当前位置有关雷的信息。
1.棋盘的准备
由扫雷UI界面不难看出,我们对于棋盘中格子的信息可以用数组的形式来存放。但我们需要考虑这个数组的类型,假定我们用1和0分别来对应雷和非雷,在这里很多人第一时间想的是用int类型的数组,但这样会和以下问题有混淆:雷在这个程序中以1来定义,那么当我们选择一个格子时,格子周围八个区域刚好只有一个雷,那么程序就很容易搞混;那么我们可以使用char类型的数组来存放雷,即字符”1“与字符”0“。
那么确认了数组的类型后,接下来我们来考虑下一个问题,我在这个棋盘上需要完成布置雷,与每次扫雷后统计周围雷的数量,那么在这个棋盘上要设置的参数就有3个,那么我们有可能会混淆,因此,我们可以设置一个相同规格的棋盘,原本的棋盘用来布置雷的,另一个棋盘用来排查雷。简单理解为,平时玩的扫雷游戏它棋盘的数据是完全可视化的,但反馈给玩家的棋盘却是部分可视化。
这是用于布置雷的棋盘,但我们在游戏中并不会将这个棋盘显示给玩家:
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | ||||||||
这是显示给玩家的棋盘(还没进行扫雷的区域用*显示),因此,我们只需要在上方的棋盘完成游戏内的逻辑,再在下方的棋盘显示信息。
* | * | * | * | * | * | * | * | * |
当我们进行排雷时,会根据选择的位置对周围八个格子进行排查并显示雷的个数,那么就会发现,在最外围的边与列所统计到的格子数并不一致,我们以下面5*5的棋盘为例
我们在最外围的行与列统计到的是周围3个格子的信息,而其他方位的格子统计到的是除自身外的8个位置,此时,我们可以在选择格子的时候附带一个判断,如果是最外围的格子,就统计周围3个格子的信息,如果不是最外围的格子就统计周围8个格子的信息。如果是这样的话,那么我们就会面临一个新的问题:当我们选择不同方向的最外围格子时,需要统计的格子位置都是不一样的,这样就会让我们编写统计格子信息时的工作量呈指数型上升。
如果我们为了能让统计周围格子信息的时候都是统计周围8个格子,那么就是很好的方法,我们不妨让9*9的棋盘再大一圈,即变成一个11*11的棋盘,那么我们在这个11*11的棋盘中的9*9棋盘中统计任意格子周围的信息时都是统计八个格子;我们用5*5的棋盘为例,将它扩大成7*7的棋盘,然后在7*7的棋盘里的5*5棋盘完成一系列操作。而当玩家进行游戏时,我们只需要将这个7*7棋盘显示出其中的5*5棋盘即可。
首先,确定了要建立一个n*n的棋盘,我们可以通过宏定义(显示9*9的棋盘,那么我们需要创建一个11*11的棋盘)
COL与ROW是显示给玩家的棋盘大小
#define COLS 11 //列
#define ROWS 11//行
#define COL COLS-2
#define ROW ROWS-2
#define fires 10 //布置的雷的数量
由此,我们可以这样创建两个棋盘:
char show[ROWS][ROWS];//显示排查雷的信息,即正式游戏中显示的扫雷棋盘
char behind_show[ROWS][COLS];//安排雷的棋盘
2.对雷区和非雷区的准备
既然我们选择了以字符’1‘与字符’0‘分别代替雷和非雷,*表示未排雷区,首先,我们需要对这两个棋盘进行初始化,对于展现给玩家的棋盘全部初始化为*,布置雷的棋盘全部初始化为字符‘0’。
而在初始化棋盘的函数中,我们需要接收的形参为:一个二维数组,行数,列数,以及要初始化成的元素 set ,如字符‘1’,‘0’,‘*‘。然后通过两个for循环遍历整个二维数组,将数组元素初始化。
/棋盘初始化(接收一个二维数组,初始化为set)
void star(char arr[ROWS][COLS], int row, int col, char set) {
for (int i = 0; i <row; i++) {
for (int j = 0; j < col; j++) {
arr[i][j] = set;
}
}
}
将两个棋盘通过star函数初始化:
star(show, ROWS, COLS, '*');
star(behind_show, ROWS, COLS, '0');
初始化后,我们可以先验证这两个二维数组是否初始化成功,再进行后续的操作:
这里我们可以编写一个查看二维数组元素的函数get_show,对于查看二维数组的函数,我们需要查看这个二维数组的每一行和每一列的元素,因此,函数的形参可以设置为:一个二维数组,行数,列数。
void get_show(char arr[ROWS][COLS], int row, int col) {
for (int i = 1; i <= ROW; i++) {
for (int j = 1; j <= COL; j++) {
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
int main(){
//我们需要查看的棋盘大小为9*9,二维数组11*11行与列最大下标为10,通过控制循环的初始条件与终止范围可以取得其中9*9的棋盘
get_show(show,ROW,COL);
get_show(behind_show,ROW,COL);
}
此时调用get_show函数可以看到,两个棋盘初始化成功了。但是,对于第几行第几列的元素并不直观。
那么,我们可以在棋盘的上方与左侧打印出相对应的行数与列数,以便更直观的知道对应位置的元素。对于棋盘的上方,在遍历函数之前,我们可以通过循环打印出坐标数1-9,对于左侧,在打印棋盘每一行之前,先打印其对应的行号,再打印棋盘,只需要添加几行代码即可完成。 此时我们再调用get_show函数查看效果
void get_show(char arr[ROWS][COLS], int row, int col) {
//先通过一个循环打印行数的标记
for (int i = 0; i <= 9; i++) {
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= ROW; i++) {
printf("%d ", i);//每次打印新的一行前,先打印当前的行号
for (int j = 1; j <= COL; j++) {
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
int main(){
get_show(show,ROW,COL);
get_show(behind_show,ROW,COL);
}
3.安排雷区
在完成上述工作后,我们就可以在棋盘上布置雷了。我们需要在11*11的二维数组棋盘上的9*9区域里布置雷,显然,传递给安排雷区的函数的形参是和打印棋盘函数的形参是一致的。对于每次游戏雷的位置都是不一样的,我们需要用到随机数。
在这里,我们先认识一下函数rand(),它包含在库函数中,需要调用头文件stdio.h,但是注意,它并不是正在意义上的随机数,是一个伪随机数,范围是0-Rand_Max;为了解决这个问题,我们可以用srand()函数,通过更新时间种子来获取新的随机数。用法如下:
srand((unsigned int)time(NULL));
int a=rand();
认识了rand函数和srand函数后,我们就可以完成安排雷区的函数:
void mine(char arr[ROWS][COLS], int row, int col) {
int count = fires;//总共需要布置的雷数
while (count) {
int a = rand() % row + 1;//得到的位置是1-9(得到的随机数取余n后,得到的余数最大为n-1,最小为0)
int b = rand() % col + 1;//得到的位置是1-9
//判断是否为雷区,如果该位置没有雷,则布置雷
if (arr[a][b] == '0') {
arr[a][b] = '1';
count--;
}
}
}
接下来,让我们验证这个函数功能是否成功实现:
//棋盘初始化(接收一个二维数组,初始化为set
void star(char arr[ROWS][COLS], int row, int col, char set) {
for (int i = 0; i <row; i++) {
for (int j = 0; j < col; j++) {
arr[i][j] = set;
}
}
}
//棋盘的打印
void get_show(char arr[ROWS][COLS], int row, int col) {
for (int i = 0; i <= 9; i++) {
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= ROW; i++) {
printf("%d ", i);
for (int j = 1; j <= COL; j++) {
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
//安排雷
void mine(char arr[ROWS][COLS], int row, int col) {
int count = fires;
while (count) {
int a = rand() % row + 1;
int b = rand() % col + 1;
if (arr[a][b] == '0') {
arr[a][b] = '1';
count--;
}
}
}
int main(){
char show[ROWS][ROWS];//显示排查雷的信息,即正式游戏中显示的扫雷棋盘
char behind_show[ROWS][COLS];//安排雷的棋盘
//对两个棋盘初始化
star(show, ROWS, COLS, '*');
star(behind_show, ROWS, COLS, '0');
//查看布置雷的棋盘
get_show(behind_show,ROW,COL);
printf("\n");
//安排雷
mine(behind_show, ROW, COL);
//查看是否安排雷成功
get_show(behind_show,ROW,COL);
}
可以看到,已经在棋盘上成功布置雷,且每次游戏雷的位置都是不一样的:
4.开始扫雷
现在我们到了最关键的一步——扫雷。当我们选择一个格子时,游戏会返回给玩家反馈:
- 选择的区域是雷,游戏结束;
- 选择的区域是非雷,当前区域显示一个数字表示周围八个格子雷的数量;
- 如果选择的格子是第9*9棋盘上除去雷区的最后一个格子,游戏胜利。
那么,在这一步最大的问题是,当我们选择一个格子时,如何统计周围八个格子的雷的数量? ,假如我们输入的位置是a,b,那么在arr[a][b]里,周围八个格子的方位如下所示
在计算机中,字符以ASCII码的形式存储,字符1的ASCII码是49,字符0是48,两者的差刚好是1,通过计算周围八个格子的ASCII码的和,减去周围八个格子都是ASCII码‘0’的和,所得的差就是周围八个格子的雷的数量,然后我们再转为整形即可。通过一个sum_mine函数实现:
int sum_mine(char arr[ROWS][COLS], int a, int b) {
return (arr[a - 1][b + 1] + arr[a - 1][b] + arr[a - 1][b - 1] + arr[a][b - 1] + arr[a + 1][b - 1] + arr[a + 1][b] + arr[a + 1][b + 1] + arr[a][b - 1]) - 8 * '0';
}
那么到这里,我们就完成了百分之八十的内容,现在,让我们来完成选择棋盘格子后的逻辑函数。
- 玩家输入所选择的行数与列数。
- 判断该位置是否为雷,是雷则游戏结束,非雷的话调用sum_mine函数返回周围八个格子的雷的个数
- 如果选择的格子是第9*9棋盘上除去雷区的最后一个格子,游戏胜利。
在这个函数形参上与之前的函数有所不同,它需要传递两个二维数组。 因为输入选择的行数与列数,雷区的安排等操作在behind_show[ROWS][COLS]数组上,show[ROWS][COLS]数组显示选择区域的信息。选择的坐标遍布在11*11中的9*9,所以当我们调用这个函数时,传参应该是ROW与COL,也就是9与9。
void seek_mine(char show[ROWS][COLS], char behind_show[ROWS][COLS], int row, int col) {
int a = 0;
int b = 0;
int win = 0;
while (fires > 0) {
printf("请输入要排除的坐标:");
scanf_s("%d %d", &a, &b);
if (a >= 1 && a <= row && b >= 1 && b <= col) {
if (behind_show[a][b] == '1') {
printf("你被炸死了\n");
get_show(behind_show, row, col);
break;
}
else {
//统计附近八个格子的雷
int sum = sum_mine(behind_show, a, b);
show[a][b] = '0' + sum;
win++;
get_show(show, row, col);
}
}
else {
printf("坐标非法,重新输入\n");
}
}
if (win == row * col - fires) {
printf("恭喜你赢了\n");
get_show(behind_show, row, col);
}
}
至此,整个游戏所需的函数皆已完成,最后,我们只需要如同积木一般将这些函数组装在主函数起来。
5.游戏实现与原码
为了界面不显得那么单调,我们可以设置一个menu函数,使得界面美观起来。
void menu() {
printf("-------\n");
printf("-------\n");
printf("-------\n");
printf("0.退出游戏\n");
printf("1.开始游戏\n");
}
对于这个游戏,在主函数的逻辑也是非常清晰的
- 游戏主界面
- 选择是否游戏
- 进入游戏后,第一次选择格子,随后判断
- 游戏胜利与否
而在主函数,我们需要判断输入的数来选择相应的选项,可以通过do.....while循环来实现。而对于上面所有的函数,我们都可以放在一个open_game()函数中。
那么,在头文件中,我们存放宏定义与函数的声明
#pragma once
//注意,宏定义中不分大小写
#define COLS 11 //行
#define ROWS 11//列
#define COL COLS-2
#define ROW ROWS-2
#define fires 10
//头文件放函数的声明
//棋盘初始化
void star(char arr[ROWS][COLS], int row, int col, char set);
//打印棋盘
void get_show(char arr[ROWS][COLS], int row, int col);
//
void open_game();
//游戏菜单
void menu();
//安排雷
void mine(char arr[ROWS][COLS], int row, int col);
//排查雷
void seek_mine(char show[ROWS][COLS], char behined_show[ROWS][COLS], int row, int col);
//统计雷
int sum_mine(char arr[ROWS][COLS], int a, int b);
在存放函数主体的.c文件中:
#include<stdio.h>
#include"game_head.h"
#include<Windows.h>
#include <stdlib.h>
//游戏开始前页面
void menu() {
printf("-------\n");
printf("-------\n");
printf("-------\n");
printf("0.退出游戏\n");
printf("1.开始游戏\n");
}
//游戏逻辑函数
void open_game() {
char show[ROWS][ROWS];//显示排查雷的信息,即正式游戏中显示的扫雷棋盘
char behind_show[ROWS][COLS];//安排雷的棋盘
//对两个棋盘初始化
star(show, ROWS, COLS, '*');
star(behind_show, ROWS, COLS, '0');
//查看棋盘
get_show(show,ROW,COL);
printf("\n");
get_show(behind_show,ROW,COL);
printf("\n");
//安排雷
mine(behind_show, ROW, COL);
//给玩家显示游戏棋盘
//get_show(show, ROW, COL);
get_show(behind_show, ROW, COL);
//开始排查雷
//seek_mine(show, behind_show, ROW, COL);
}
//棋盘初始化(接收一个二维数组,初始化为set
void star(char arr[ROWS][COLS], int row, int col, char set) {
for (int i = 0; i <row; i++) {
for (int j = 0; j < col; j++) {
arr[i][j] = set;
}
}
}
//棋盘的打印
void get_show(char arr[ROWS][COLS], int row, int col) {
for (int i = 0; i <= 9; i++) {
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= ROW; i++) {
printf("%d ", i);
for (int j = 1; j <= COL; j++) {
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
//安排雷
void mine(char arr[ROWS][COLS], int row, int col) {
int count = fires;
while (count) {
int a = rand() % row + 1;
int b = rand() % col + 1;
if (arr[a][b] == '0') {
arr[a][b] = '1';
count--;
}
}
}
//排查雷
void seek_mine(char show[ROWS][COLS], char behind_show[ROWS][COLS], int row, int col) {
int a = 0;
int b = 0;
int win = 0;
while (fires > 0) {
printf("请输入要排除的坐标:");
scanf_s("%d %d", &a, &b);
if (a >= 1 && a <= row && b >= 1 && b <= col) {
if (behind_show[a][b] == '1') {
printf("你被炸死了\n");
get_show(behind_show, row, col);
break;
}
else {
//统计附近八个格子的雷
int sum = sum_mine(behind_show, a, b);
show[a][b] = '0' + sum;
win++;
get_show(show, row, col);
}
}
else {
printf("坐标非法,重新输入\n");
}
}
if (win == row * col - fires) {
printf("恭喜你赢了\n");
get_show(behind_show, row, col);
}
}
//统计附近的雷
//字符1的ASCII码是49,0是48
int sum_mine(char arr[ROWS][COLS], int a, int b) {
return (arr[a - 1][b + 1] + arr[a - 1][b] + arr[a - 1][b - 1] + arr[a][b - 1] + arr[a + 1][b - 1] + arr[a + 1][b] + arr[a + 1][b + 1] + arr[a][b - 1]) - 8 * '0';
}
在main函数文件里:
#include<stdio.h>
#include"game_head.h"
#include<Windows.h>
#include <time.h>
int main() {
int input_number = 0;;
srand((unsigned int)time(NULL));
do {
menu();
scanf_s("%d", &input_number);
switch (input_number) {
case 1:
open_game();
break;
case 0:
printf("退出游戏成功");
break;
default:
printf("请重新输入\n");
break;
}
} while (input_number);
return 0;
}
至此,我们这个扫雷小游戏便完成了,但它目前还有些许不足,当第一次选择格子时,无法衍生到带有雷区信息的格子,后面,我们会完善这个游戏。