前言
扫雷游戏是一种益智类游戏,目标是通过揭示方块出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
游戏规则
扫雷游戏规则如下:
(1).输入坐标位置是雷,游戏失败。(2).如果不是雷,则在该位置标记出周围一圈雷的个数,玩家继续输入,
(3).如果不是雷,而且周围一圈都没有雷(即显示数字0),那么,将显示周围一片周围雷数都为0的区域只到遇到非零。
(4).当排除所有不是雷的地方(即只剩下有雷的地方未显示)游戏胜利。
游戏结构的分析
扫雷过程中,布置的雷,排查的雷的信息都需要存储,所以我们需要一定的数据结构来存储这些信息。
这个时候我们会想到一个9*9的矩阵。(10颗雷)
我们将有雷的地方设置为‘1’,将没雷的设置为‘0’。
示例
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
3 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
4 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
5 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |
6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
7 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
8 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
9 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
假设我排查(7,3),那么在(7,3)周围一圈有雷的话就会被记下来,并且在(7,3)处显示有几个雷,打印到屏幕上。如果我们要排除(8,9)或者(9,6)时,我的访问就会越界,控制台就会乱码,为了解决这个问题,我们会将棋盘设计成[rows+2][cols+2]的数组。而多加的两行的目的是防止越界,所以不需要布置雷。
如图所示
继续分析,当我们排查到一个雷时,需要将雷的信息储存起来,然后打印,作为排雷的重要参考信息,那么这个雷的个数信息应该存放在哪呢?因为存放雷的信息和雷的个数信息会在打印上出现困难。
为了清晰的将棋盘上有雷、非雷的信息和排查出雷的个数在棋盘上展示出来,我们采用另外一个方案,我们专门给一个棋盘(对应一个数组mine)存放布置好的雷的信息,再给另外一个棋盘(对应另外一个数组show)排查出的雷的信息。这样问题就解决了,把雷布置的到mine数组,在mine数组中排查雷,排查出的数据存放在show数组中,并且打印首位数组的信息到后期排查使用。
对于show数组就是将所有的'1','0'换成'*'。
游戏的程序设计
一、创建项目
这里为了提高代码的组织性、可维护性、可重用性和扩展性,提高编译效率和测试效率,我们使用多文件的编程方式来设计扫雷的程序代码。
首先我们创建项目准备一个头文件game.h用来做函数的声明,一个源文件game.c用来写函数的实现,最后用一个源文件test.c来作主函数的测试运行。
二、菜单的创作
框架(test.c)
我们写一个基础的框架,将头文件都放在game.h里面,就需要包含头文件。
注意,需要在game.c和text.c中包含一下头文件,要注意自己写的头文件要用双引号引用如下:
接着定义一个主函数main,为了避免代码的混乱,我不想把所以函数写到一个主函数里,所以我定义一个test函数来完成游戏的测试逻辑,在函数test中实现。
//主函数
int main() {
test();
return 0;
}
void test()
{
//游戏逻辑的实现在test函数中完成,避免主函数的代码混乱
}
为了让玩家有好的游戏体验,我们得建立一个游戏菜单供玩家选择是否开始游戏,所以打印菜单menu(),让玩家进行选择。
void menu()
{
printf("*******扫雷游戏*******\n");
printf("****** 1. play *******\n");
printf("****** 0. exit *******\n");
printf("**********************\n");
system("color 71");
}
为了美化游戏体验,我们将游戏页面控制台的文字和背景给点颜色,需要包含以下头文件,用来写一个color()函数来设置控制台的颜色。
#include<stdlib.h>
为了方便大家了解color()函数对控制台颜色的整理,于是用以下代码来实现颜色的整理
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
void color(int k)
{
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), k);
}
int main()
{
for (int i = 1; i <= 255; i++)
{
color(i);
printf("%3d ", i);
if (i%15==0)
printf("\n");
}
color(15);
return 0;
}
以上代码的运行结果如下
C语言字体颜色教学可点击此处 ,学习大佬的博客
接着函数进来是直接用do-whlie进行打印菜单,然后可以玩家进行选择,在控制台输入'1'则进入游戏game()函数,输入'0',则退出游戏,如果输入其他数则提示错误,需要重新输入。
void test()
{
int input = 0;
srand((unsigned int)time(NULL));//调用srand(),用于下面rand()产生随机数时的设置随机种子数
do
{
menu();
printf("请选择是否开始游戏:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
三、棋盘制作以及初始化
接下来为了实现游戏的功能,我们需要设计和制作雷盘(以下统称棋盘);
回到我们的设计目标,我们需要设计的是一个9*9,的棋盘,为了防止越界,需要对棋盘进行扩大成11*11的棋盘,而为了不把棋盘设定的太死板,所以我在头文件game.h中来定义棋盘的行列和纵列,故我定义变量ROW和COL为棋盘的行列和纵列。定义ROWS和COLS为扩大后的棋盘。
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
那么我们需要制作两个棋盘。
第一个用来存放布置好的雷的信息(数组mine[ROWS][COLS])
第二个用来存放排查出的雷的信息(数组show[ROWS][COLS])
接着我们的整体思路是将数组初始化,即将棋盘初始化,首先将棋盘1全部初始化为字符'0',然后将棋盘2全部初始化为'*'。
为了同时将棋盘1和棋盘2初始化,我们先定义一个函数为InitBoard(),用于全部初始化棋盘,而二者所要初始化的字符不同,所以我们定义一个变量set用来储存初始化的值。
为了使用InitBoard()函数,我们需要在game.c头文件中声明函数,对其声明同时,我们定义三个参数,rows、cols、set分别储存行列数和初始值,并传给test.c,在game.c中实现。
game.h
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//声明函数
//初始化棋盘,将数据传给test.c,在game.c中实现
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
test.c
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的个数信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
game.c
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;
}
}
}
到这里,我们的棋盘的制作和初始化便已经完成了,那么我们可以将棋盘打印出来,检查是否是我们想要的。
四、雷盘的打印
为了打印我们的雷盘,和上述所说的一样,我们定义一个函数DisplayBoard();用于检查雷盘的准确性。
game.h
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
test.c
void game()
{
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的个数信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
}
game.c
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("--------扫雷游戏-------\n");
//打印列号
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 game()
{
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的个数信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
//DisplayBoard(show, ROW, COL);
}
做完前期雷盘的编排和布置后我们接下来就是需要确保游戏可以玩,所以我们需要开始设计埋雷的代码。
五、埋雷的设计
目的:在9*9的棋盘上随机布置10颗雷,布置在9*9的棋盘上
我们创建SetMine()函数,用来随机布置10颗雷
首先,我们需要定义雷的个数,我将雷定义为EASY_COUNT,令雷的个数为10,并且声明SetMine()函数。
game.h
#define EASY_COUNT 10
void SetMine(char board[ROWS][COLS], int row, int col);
test.c
#include <stdlib.h>
#include <time.h>
SetMine(mine, ROW, COL);
注意:mine()数组是11*11,所以传过去的数也必须是11*11,不能因为你仅仅操作11*11格子里面的9*9格子而传9*9,所以声明函数时用ROWS和COLS,而不是ROWS和COLS。
接着声明和定义完成后,我们需要在game.c中设计SetMine()函数,因为布雷是随机的,所以就要用到一个随机数,所以用到随机数函数rand(),这里需要包含头文件<stdlib.h>和<time.h>。(在头文件中引入即可)
在调用rand()函数之前,可以使用srand()函数设置随机数种子,如果没有设置随机数种子,rand()函数在调用时,自动设计随机数种子为1。随机种子相同,每次产生的随机数也会相同。
srand()用来设置rand()产生随机数时的随机数种子。
上文已调用,如下图所示
因为不需要time函数的参数,所以传入一个空指针NULL,并强制返回类型为unsigned int
game.c
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;//row=9,rand函数的使用:rand()%row+1表示产生1到9以内的随机整数
int y = rand() % col + 1;//col=9,rand函数的使用:rand()%col+1表示产生1到9以内的随机整数
//所以此时的[x][y]是一个随机坐标
//布置雷,如果board[x][y]坐标上没有雷('0'),则在这个坐标生成一个雷,然后总雷数count减1
//如果有雷('1'),则重新循环,直到count=0
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
如果想对随机数函数rand()有更加深入的了解,可以参考以下链接
https://blog.csdn.net/chikey/article/details/66970397
再次注意:time函数和rand函数需要包含头文件<stdlib.h>和<time.h>,不要忘记了!!!
此时我们布置雷的信息已经设计结束,我们可以测试以下所写代码是否符合我们的想法和是否复制成功,我们可以打印将布置的mine雷盘打印出来,我们使用函数DisplayBoard()
void game()
{
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的个数信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
//DisplayBoard(show, ROW, COL);
// 布置雷
SetMine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL);//用来检测,在正式使用游戏时需要注释掉
}
运行结果
以上10颗雷已经布置完毕,说明我们的代码正确
六、排查雷的设计
目的:将排查的雷的周围剩余的雷数
在排查雷之前我们需要打印雷盘,一开始雷盘还没开始排查所以,每个格子都是 '*'
test.c
void game()
{
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的个数信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
//DisplayBoard(show, ROW, COL);
// 布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//打印雷盘
DisplayBoard(show, ROW, COL);
}
我们创建FindMine()函数,用于排查雷,在mine数组中排查,然后将排查的信息放入show数组中,因此在传参的时候会同时涉及到mine数组和show数组,所以两个数组都需要传
test.c
void game()
{
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的个数信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
//DisplayBoard(show, ROW, COL);
// 布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//打印雷盘
DisplayBoard(show, ROW, COL);
//排查雷
FindMine(mine, show, ROW, COL);
}
game.h
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
函数的实现
game.c
//此函数用与统计排查坐标周围函数有几个雷
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x][y - 1] +
mine[x + 1][y - 1] +
mine[x+1][y-1] +
mine[x + 1][y + 1] +
mine[x][y + 1] +
mine[x - 1][y + 1] -
8 * '0');
}//‘1’的值是49;'0'的值是48;‘1’-‘0’=1是个整数。我们将周边的数都加起来-8*‘0’就能得到雷的数。
void FindMine(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 - EASY_COUNT)
{
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
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 == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
不希望别人看到,static是静态的意思,静态函数只能在声明它的文件中可见,其他文件不能引用该函数。
到这里我们的扫雷设计就已经全部完成了,以下是所有代码的整合
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);//将数据传给test.c,在game.c中实现
//打印棋盘
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);
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
#include <stdlib.h>
#include <windows.h>
//游戏逻辑的测试,包含游戏菜单的打印,游戏设计的基本逻辑的展示。
void menu()
{
printf("*******扫雷游戏*******\n");
printf("****** 1. play *******\n");
printf("****** 0. exit *******\n");
printf("**********************\n");
system("color 71");
}
void game()
{
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的个数信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
//DisplayBoard(show, ROW, COL);
// 布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//打印雷盘
DisplayBoard(show, ROW, COL);
//排查雷
FindMine(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);
return 0;
}
int main() {
test();
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#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)
{
int i = 0;
printf("--------扫雷游戏-------\n");
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)
{
//布置10个雷
//⽣成随机的坐标,布置雷
srand((unsigned int)time(NULL));//随机种子数
/*int count = EASY_COUNT;*/
/*for (int i = 1; i <= count; i++)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '1')
{
count--;
continue;
}
board[x][y] = '1';
}*/
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--;
}
}
}
//此函数用与统计排查坐标周围函数有几个雷
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x+1][y-1]+
mine[x + 1][y + 1] + mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0');
}
void FindMine(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 - EASY_COUNT)
{
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
//判断坐标的有效性
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
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 == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
扫雷游戏的扩展
• 是否可以选择游戏难度
◦ 简单 9*9 棋盘,10个雷
◦ 中等 16*16棋盘,40个雷
◦ 困难 30*16棋盘,99个雷
• 如果排查位置不是雷,周围也没有雷,可以展开周围的一片
• 是否可以标记雷
• 是否可以加上排雷的时间显示
扩展加紧更新中。。。。。。