一、 前期分析
扫雷游戏就是用户选择一个坐标,如果位置是雷,就被炸死,游戏结束,如果位置不是雷,就显示该坐标周围有几个雷,直到将所有⾮雷都找出来,排雷成功。(这里我们就以简单版本的9*9的棋盘为例)。
按照这个思路,我们可以设置一个9*9的二维数组来表示棋盘,然后设置雷,用1表示雷,用0表示非雷,这样统计出某个坐标周围有几个雷,然后把该坐标上的数字改成几即可。但是这样做有三个问题。
第一,如果统计的坐标周围有1个雷,将该坐标改为1,那么下次统计其它坐标周围雷的个数时,这个排查出来的1和设置的雷的1就会产生混淆。(如统计图中(2,4)坐标处,它周围有一个雷)

想到如果把雷和非雷的信息不用数字表示,而是用字符表示就可以避免混淆。
第二,如果把排查出的雷的信息和设置的雷的信息放在一张棋盘上的话,那么展示给用户的时候,用户就可以直接看出哪里有雷,就做不到隐秘的效果了。所以我们可以用两张棋盘,一张用于存放设置雷的信息,另一张用于存放排查出的雷的信息;同时如果这么做,第一个会混淆的问题也可以被解决,而我们仍然可以用1表示雷,用0表示非雷。
展示给用户的棋盘要有排查出的雷的信息,同时为了保持神秘,可以将未排查的部分初始化为字符'*'。由于既有数字又有字符,而周围雷的个数又一定不会超过8,所以我们可以统一用字符来表示,这就确定了我们要用到二维字符数组。再想想设置雷的数组,用1和0表示雷和非雷,这也可以用字符’1’和字符’0’表示,如果两个棋盘都使用了字符数组,那么后续就可以用同一套字符数组的函数来操作了,这就简便了许多。
第三,统计最外围一圈的坐标周围雷的个数时,如统计图中的(3,9)坐标时,最右边的三个坐标就会越界。我们可以将棋盘扩大一圈(这里就变成11*11的棋盘),而仍将雷布置在中间的(9*9)区域,最外围一圈不用统计,这样就避免了越界的问题。
二、 代码实现
解决了上述最基本的问题,下面就来探讨游戏逻辑的实现。
为了完成这个游戏,需要用到许多函数,为了将程序模块化,我们可以将游戏的外围大框架写在一个源文件中,记为test.c,用于测试游戏的整体逻辑,将游戏细节部分的逻辑实现写在另一个源文件game.c中,再添加一个头文件game.h,在其中声明game.c中的函数。
在test.c中首先给出是否进入游戏的选项,可以定义一个menu函数,打印出菜单,让用户可以自由选择进入游戏或退出,可以在do-while循环中使用switch语句进行判断。
void menu()
{
printf("*****************************\n");
printf("******** 0.exit **********\n");
printf("******** 1.play **********\n");
printf("*****************************\n");
}
void test()
{
int input = 0;
do
{
menu();
printf("请选择:>\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
}
这里我们可以先测试一下,在game函数中打印“扫雷游戏”。

没有问题,继续往下写。
在game函数中需要定义两个11*11的二维数组,一个用于存放设置雷的信息,一个用于存放排查出的雷的信息,为了方便改变棋盘的规模,我们可以在头文件game.h在定义行和列两个常量。由于后续设置雷要在中间的9*9棋盘内,排查雷又需要整个11*11的棋盘,所以既需要中间的行列数,又需要扩大后的行列数。
//扩大前的行列数
#define ROW 9
#define COL 9
//扩大后的行列数
#define ROWS ROW + 2
#define COLS COL + 2
那么二维数组就容易定义了
char mine[ROWS][COLS] = { 0 };//存放设置雷的信息
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
有了棋盘,先要对棋盘进行初始化,设置雷的棋盘未设置前要初始化为'0',而排查雷的棋盘未开始前要初始化为'*',初始化的内容不同,那么我们就可以将要初始化的内容作为参数传到函数中,这样就可以用同一个函数进行初始化了。
InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*');
具体怎么初始化呢?就是遍历二维数组,然后将每个元素都赋值为传过去的参数。
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
有没有初始化成功呢?可以把棋盘打印出来看一下。定义一个打印的函数,将数组和要打印的行列传过去(注意这里传过去的是11*11的数组,只是要用到中间的9*9的格子)。
DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
为了将每次的棋盘间隔开,我们可以在每次打印前加上“-----扫雷游戏-----”。
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("-----扫雷游戏-----\n");
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
效果如下

初始化成功了,但是选择要排查的坐标时,一排一排的数很麻烦,为了快速锁定坐标,可以在每一行、每一列前加上序号。
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("-----扫雷游戏-----\n");
//为方便快速锁定坐标,打印时可以加上行号和列号
int i = 0;
for (i = 0; i <= col; i++)//打印列号
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);//打印行号
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
效果如下

这里补充一点,由于两个源文件都需要包含<stdio.h>和"game.h",我们就可以在自己写的"game.h"中包含<stdio.h>,这样两个源文件就只需要包含"game.h"一个头文件。(后续设置随机数要用到的头文件也可以定义在"game.h"中)
下一步是设置雷,这就需要生成随机数。在main函数中设置srand((unsigned int)time(NULL))作为rand函数的种子(一个工程中只要设置一次),然后就可以调用rand函数生成我们想要的随机数。由于是9*9的棋盘,就要生成1~9的数字,rand() % ROW + 1和rand() % COL + 1就可以满足这个要求。假设要设置简单的10个雷,为方便修改,也可以将雷的个数在头文件中定义。(#define EASY_COUNT 10)
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count) //共有 EASY_COUNT 个雷,设置完了就停止
{
int x = rand() % ROW + 1; //每次都要重新生成随机数
int y = rand() % COL + 1;
if (board[x][y] == '0') //判断该位置是否被设置过雷
{
board[x][y] = '1';
count--;
}
}
}
设置完了雷,可以打印出来看看是否成功。
下一步就到了排查雷的环节,需要根据用户输入的坐标,在mine数组中数出周围雷的个数,然后展示到show数组中。输入坐标后要判断坐标是否在合法范围内(是否越界),如果合法,还需要判断是否已经被排查过。满足了上述两个条件,就要将该坐标周围雷的个数加起来,由于数组中放的是字符,减去8个字符'0'才得到总的个数。
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +
mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] +
mine[x + 1][y] + mine[x + 1][y + 1] - '0' * 8;
}
还有另一种方法,因为选择的坐标不是雷时才要统计雷的数量,所以可以将周围(包括自己)9个坐标分别减去字符'0'后相加。
int GetMineCount1(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int sum = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
sum += (mine[i][j] - '0');
}
}
return sum;
}
最后如果排查完了还需要判断输赢,也就是将非雷的71个位置都找到,可以定义一个变量int win = row * col - EASY_COUNT,每排查一个位置win就减去1,直到win等于0就跳出循环。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = row * col - EASY_COUNT;
while (win)
{
printf("请输入要排查的坐标: ");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("该坐标已经被排查过,请重新输入\n");
}
else
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");//失败了,展示棋盘
DisplayBoard(mine, ROW, COL);
break;
}
else
{
//计数并赋值(一定不是雷,才进入到这里)
int count = GetMineCount1(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win--;
}
}
}
else
{
printf("输入坐标非法,请重新输入\n");
}
}
if (win == 0)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
完成后如果想要测试成功排完雷的逻辑,需要执行71次,太麻烦,我们可以将雷设置为80个,然后打印出设置雷的棋盘,再进行测试,这样只需排一次就可以成功了。
三、完整代码
下附每个部分的完整代码:
//game.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
//设置雷的数量
#define EASY_COUNT 10
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//设置雷
void SetMine(char board[ROWS][COLS],int row, int col);
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//game.c
#include "game.h"
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("-----扫雷游戏-----\n");
int i = 0;
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % ROW + 1;
int y = rand() % COL + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +
mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] +
mine[x + 1][y] + mine[x + 1][y + 1] - '0' * 8;
}
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = row * col - EASY_COUNT;
while (win)
{
printf("请输入要排查的坐标: ");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("该坐标已经被排查过,请重新输入\n");
}
else
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");//失败了,展示棋盘
DisplayBoard(mine, ROW, COL);
break;
}
else
{
//计数并赋值(一定不是雷,才进入到这里)
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win--;
}
}
}
else
{
printf("输入坐标非法,请重新输入\n");
}
}
if (win == 0)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
//test.c
#include "game.h"
void menu()
{
printf("*****************************\n");
printf("******** 0.exit **********\n");
printf("******** 1.play **********\n");
printf("*****************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*');
//设置雷
SetMine(mine, ROW, COL);
//打印棋盘
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//排查雷
FindMine(mine, show, ROW, COL);
}
void test()
{
int input = 0;
do
{
menu();
printf("请选择:>\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
}
int main()
{
srand((unsigned int)time(NULL));
test();
return 0;
}
可以发现,这个游戏虽然看起来很复杂,但把它拆分成一个个小的模块来写,就会容易一些。当我们掌握了基本的语法和框架,就有能力去写出一个个看似遥不可及的较大的程序。也许写的过程中会遇到很多次无法执行,但这是无法避免的磨练的过程,我们需要消除恐惧,耐下心来,找到错误点,改正错误,这样才能在一次次错误中加深印象,写出更加规范合理的代码。
此外,我们还应养成写一段测一段的习惯,写好外面的框架,就测试一下逻辑是否可行,写好某一个细节部分,就测试一下是否正确。如果写完了几百行甚至上千行代码才发现里面有错误,那找出错误点将会变得非常困难。
1848

被折叠的 条评论
为什么被折叠?



