目录
[2.1 游戏菜单](#2.1 游戏菜单)
[2.2 创建雷盘思路](#2.2 创建雷盘思路)
[2.3 构建雷盘](#2.3 构建雷盘)
[2.4 雷盘展示](#2.4 雷盘展示)
[2.4.1 初始化雷盘](#2.4.1 初始化雷盘)
[2.4.2 打印雷盘](#2.4.2 打印雷盘)
[3.1 布置雷](#3.1 布置雷)
[3.2 排查雷](#3.2 排查雷)
前言
C语言实现扫雷小游戏,帮我们更进一步的掌握数组、模块化思想等知识。
一、分模块化
对于扫雷小游戏,相信老铁们应该不陌生,根据信息进行排雷,大家可以参考网页版的扫雷游戏:扫雷小游戏
对于扫雷小游戏,虽然是一个小项目,代码量不多,但对于初学者来说,重要的是学习如何分模块开发项目。
本文将该游戏分成三个文件:
- test.c游戏测试文件
- game.c游戏执行逻辑(函数实现)
- game.h游戏声明(函数声明,头文件)
二、准备雷盘
扫雷游戏,首先需要一个雷盘,本文讲解的雷盘为9×9规格。
2.1 游戏菜单
一个游戏,最先呈现给用户看的是游戏菜单,对于游戏菜单,可以用do...while循环 + switch选择语句完成,因为无论用户是否开始还是退出游戏,程序都会运行一次。
cpp
test.c文件
#include "game.h"
void menu()
{
printf("********************\n");
printf("***** 1、play ******\n");
printf("***** 0、exit ******\n");
printf("********************\n");
}
int main()
{
int input = 0;
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;
}
game.h文件
#pragma once
#include <stdio.h>
2.2 创建雷盘思路
对于雷盘,使用二维数组存储数据是最合适的,符合行列。那一个雷盘就可以了吗?
我们规定:雷用字符1表示,非雷用字符0表示。
因此冲突点就出现了,如果对某个位置进行排雷,统计雷数时,正好也为1,那究竟是雷还是雷数呢?
为此,这里用了一个很巧妙的方法,我们定义两个大小一致的二维数组,也就是两个雷盘,一个雷盘用于布置雷,一个雷盘用于存放统计后雷的个数。因此,我们有以下规定:
- 布置雷的二维数组叫mine。
- 存放雷数的二维数组叫show。因为要展示给用户看选择的位置周围的雷数,因此叫show。
- mine雷盘中字符0为非雷,字符1为雷。
- show雷盘中字符*表示未排查,字符数字表示该位置已被排查。
前面说过,我们的雷盘是一个9×9的规格,那相对于的两个数组的大小也是9×9吗?其实不是,应该为11×11,因为当我们对最上、最下、最左、最右中的位置进行排雷时,统计周围有几颗雷时,会造成数组越界的情况,因此我们定义多2行2列的数据,这部分位置不存放雷,将雷存放在9×9的雷盘中,多定义的2行2列只是为了防止数组越界。
2.3 构建雷盘
对于数组的大小,我们不能写死,应该用#define标识符常量来表示,方便以后想扩大雷盘。并且我们需要分别定义9×9和11×11的标识符,因为在布置雷、打印雷盘、排雷的功能中只需要用到9×9的区域,而在初始化雷盘时,需要用到11×11区域。
cpp
test.c文件
#include "game.h"
void menu()
{
printf("********************\n");
printf("***** 1、play ******\n");
printf("***** 0、exit ******\n");
printf("********************\n");
}
void game()
{
//定义两个大小相同的二维数组
//这里的数组大小最好用#define标识符表示,后续想改变就该#define就好了
//用于布置雷,用字符数组的原因是规定字符0为非雷,字符1为雷
char mine[ROWS][COLS] = { 0 };
//用于存放排查雷的信息(主要给用户看的页面
//用字符数组的原因是规定雷数用字符数字表示,其它位置用字符*表示
char show[ROWS][COLS] = { 0 };
}
int main()
{
int input = 0;
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;
}
game.h文件
#include <stdio.h>
/*
解释一下这里定义的标识符:
该游戏的排雷范围为9×9。
点击一处位置,那周围的8处位置就要判断有几个雷。
当位置位于上下一行时或者左右一行时,那访问周围8处位置时,
就会造成越界访问了,因此这里可以加多两行两列,目的是为了不越界访问
这两行两列不放置雷,雷的范围只在9×9中
*/
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
2.4 雷盘展示
2.4.1 初始化雷盘
首先,我们要对六个雷盘进行初始化操作,mine雷盘一开始全为字符0,因为还没布置雷;show雷盘一开始全为字符*,因为还没开始排雷。
cpp
test.c文件
#include "game.h"
void game()
{
//初始化两个棋盘
//mine雷盘一开始全是字符0,因为还没布置雷
InitializeMinefield(mine, ROWS, COLS, '0');
//show雷盘一开始全是字符*,因为还没统计周围雷数
InitializeMinefield(show, ROWS, COLS, '*');
}
game.h文件
#pragma once
#include <stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//初始化雷盘
//这里的参数写什么?
//既然是初始化雷盘,那要有个数组来接收传来的mine数组和show数组
//初始化9×9还是11×11?11×11,这样方便后续计算雷的个数
//还有一个参数很重要,那就是set,既然用一个函数就初始化两个雷盘
//那就要将标识字符传过来,进行设置
void InitializeMinefield(char init[ROWS][COLS],int rows,int cols,char set);
game.c文件
#include "game.h"
//初始化雷盘
void InitializeMinefield(char init[ROWS][COLS], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
init[i][j] = set; //set为标识符号,mine雷盘传'0',show雷盘传'*'
}
}
}
初始化两个雷盘,要为每个雷盘分别写一个函数吗?
不需要,mine雷盘初始化时将字符0当成参数;show雷盘初始化时将字符*当成参数即可。
2.4.2 打印雷盘
打印雷盘的注意点在于打印多大的雷盘:是9×9,还是11×11?
前面说过,其实我们真正的雷盘大小为9×9的,11×11是为了让数组不越界。
cpp
test.c文件
#include "game.h"
void game()
{
//打印雷盘,给用户展示的是show雷盘
//打印多大?9×9,因为这是真正排雷区域
PrintMindefield(show, ROW, COL);
}
game.h文件
#pragma once
#include <stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//打印雷盘
//这里需要注意,接收数组的大小需要写11×11,因为定义数组的时候就是11×11
//打印时只打印9×9,但在语法上来说,数组的大小为11×11。
void PrintMindefield(char print[ROWS][COLS],int row,int col);
game.c文件
#include "game.h"
//打印雷盘
void PrintMindefield(char print[ROWS][COLS], int row, int col)
{
printf("-------扫雷--------\n");
//给雷盘编号,这样用户更快找到坐标进行排雷
//上边编号
for (int i = 0; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
//初始值从下标1开始,最后下标row/col
//因为数组的大小为11×11,只打印9×9时,就不能从0开始了,结尾也同理
for (int i = 1; i <= row; i++)
{
printf("%d ", i); //左边编号
for (int j = 1; j <= col; j++)
{
printf("%c ", print[i][j]);
}
//每打印一行就\n
printf("\n");
}
}
虽然我们只打印9×9的雷盘,但是定义数组时的大小为11×11,在函数形参接收时,数组中的[][]应为11×11,但传过去的行和列为9、9。
雷盘准备完毕,我们看看效果:
三、排雷
3.1 布置雷
布置雷的思路很简单,我们在mine数组上,生成随机坐标,需要注意的如下:
- 该坐标位置没布置过雷(坐标内容为字符0)。
- 生成随机数的函数,srand随机数生成器和rand函数搭配使用。
- #define标识符定义雷的个数。
cpp
test.c文件
#include "game.h"
void game()
{
//布置雷
//在mine数组中布置
//#define标识符定义雷数
//同样,只需要在row × col中布置
Place_mine(mine, ROW, COL);
}
game.h文件
#pragma once
#include <stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//雷的总个数
#define Mine_Sum 10
//布置雷
void Place_mine(char mine[ROWS][COLS], int row, int col);
game.c文件
#include "game.h"
//布置雷
//随机布置,随机生成坐标,使用rand函数前要设置随机生成器srand
/*
条件:
坐标不是雷,即不是'1'
*/
void Place_mine(char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = Mine_Sum;
//总雷数,当count为0时,表示已布置好雷
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
3.2 排查雷
对于用户操作方面:
- 用户通过坐标进行排雷。
- 输入的坐标有边界条件:x>=1 && x<=row && y>=1 && y<=col
- 输入的坐标不能重复
什么时候结束呢?两种情况:
- 被炸死
- 排雷成功:排雷次数<row*col - 雷数
如何统计周围雷数:
- 周围坐标之和 - 8*字符0。
- 因为是mine数组中只有字符0或字符1.
- 字符数字转为数字:字符数字-字符0 = 数字。因为字符进行加减操作时,是以ascll码进行。
- 数字转为字符数字:数字+字符0 = 数字。
- 因此周围坐标之和再给每个坐标依次减字符0,那最终的结果就为周围雷的个数(数字)。
- 统计完成后,将个数转为字符个数,存储到对应位置的show雷盘中。
cpp
test.c文件
#include "game.h"
void game()
{
//排雷(用户输入坐标排雷)
/*
排雷功能需要两个雷盘,mine雷盘用来检查周围雷的个数
show雷盘用于存放统计好周围雷的个数,并展示
*/
Examine(mine, show, ROW, COL);
}
game.h文件
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.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_Sum 10
//排雷
void Examine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
game.c文件
#include "game.h"
//排雷
/*
两个核心:
1、通过用户坐标排雷
2、检查周围坐标有几个雷后,放置到show雷盘中显示
*/
//统计该坐标周围的雷数
//此时的查找的雷盘范围为:11×11,防止越界
int StatisNumberMines(char mine[ROWS][COLS], int x, int y)
{
/*
mine雷盘中放的都是:字符0 或 字符1
前备知识:
字符数字转为数字: 字符数字 - 字符0 = 数字
数字转字符数字:数字 + 字符0 = 字符数字
原因:
因为在进行字符间的加减操作时,是以ascll码进行的。
现在mine雷盘中放的都是字符数字,那将8个位置的字符数字相加后: (8个位置之和) - 8*字符0
再依次减字符0,那就能统计处周围有几个雷
*/
return (mine[x + 1][y] + mine[x + 1][y - 1] +
mine[x][y - 1] + mine[x - 1][y - 1] + mine[x - 1][y] +
mine[x - 1][y + 1] + mine[x][y + 1] + mine[x + 1][y + 1]) - 8 * '0';
}
void Examine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
/*
坐标限制:
1、不能重复
2、不能超出有效范围:x>=1 && x<=row && y>=1 && y<=col
*/
/*
什么时候结束呢?两种情况:
1、被炸死。
2、排雷成功:排雷次数<row*col - 雷数
*/
int num = 0;
while (num< row * col-Mine_Sum)
{
printf("请玩家输入排雷坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//输入坐标不能重复
/*
检查show雷盘,未排雷过的坐标为字符*,排查过的坐标字符0或字符数字
*/
if (show[x][y] == '*')
{
//判断是否是雷
if (mine[x][y] == '0')
{
/*
输入的坐标不是雷
统计周围8个坐标有几颗雷
*/
//在mine雷盘中统计
int count = StatisNumberMines(mine, x, y);
//统计好后放到show雷盘中
//当然,要将数字转为字符数字,因为数组时char类型
show[x][y] = count + '0';
PrintMindefield(show, ROW, COL);
num++;
}
//是雷就结束
else
{
printf("您被炸死了\n");
PrintMindefield(mine, ROW, COL);
break;
}
}
else
{
printf("该坐标已被排查\n");
}
}
else
{
printf("输入的坐标超出范围\n");
}
}
if (num == row*col - Mine_Sum)
{
printf("恭喜你,排雷成功!\n");
}
}
四、进阶版扫雷
简易版的扫雷有什么缺陷呢?
其实有很多功能都没有实现,比如即时、标记、剩余雷数等等,但最重要的是展开一片的功能没有实现,因此进阶版扫雷,将修改排雷函数,让用户输入坐标后,将不是周围一大片不是雷的地方展开,并提供更多雷数信息。
此时的扫雷,只能一个一个坐标的排查,并不会展开一片。
展开一大片的关键点:
- 该坐标不是雷
- 该坐标周围坐标没有雷
- 排查坐标没有被排查过。(为什么?因为当有一个坐标满足三个条件后,然后该坐标周围又有坐标满足条件,进行展开,那原先满足条件的坐标也算是它周围8个坐标,那还要判断它吗?它已经不是雷了,因此这个条件很重要,容易造成死递归)
cpp
//统计该坐标周围的雷数
//此时的查找的雷盘范围为:11×11,防止越界
int StatisNumberMines(char mine[ROWS][COLS], int x, int y)
{
/*
mine雷盘中放的都是:字符0 或 字符1
前备知识:
字符数字转为数字: 字符数字 - 字符0 = 数字
数字转字符数字:数字 + 字符0 = 字符数字
原因:
因为在进行字符间的加减操作时,是以ascll码进行的。
现在mine雷盘中放的都是字符数字,那将8个位置的字符数字相加后: (8个位置之和) - 8*字符0
再依次减字符0,那就能统计处周围有几个雷
*/
return (mine[x + 1][y] + mine[x + 1][y - 1] +
mine[x][y - 1] + mine[x - 1][y - 1] + mine[x - 1][y] +
mine[x - 1][y + 1] + mine[x][y + 1] + mine[x + 1][y + 1]) - 8 * '0';
}
//排雷,展开一片
/*
基本思路:
1、递归,终止条件为周围坐标之和不为0,说明周围有雷
2、如果为0,那将该坐标设为空字符,然后依次查看周围坐标的周围坐标是否为0,如果是那就设置为空
但前提是查看的坐标没有被排查过,就是坐标里的值是'*',并且该坐标没超出范围
*/
void Unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int count = StatisNumberMines(mine, x, y);
if (count == 0)
{
show[x][y] = ' ';
int i = 0;
int j = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*' && i > 0 && i < 10 && j>0 && j < 10)
{
Unfold(mine, show, i, j);
}
}
}
}
else
{
show[x][y] = count + '0';
}
}
//查看show雷盘中还剩多少个位置没排查,当只剩Mine_Sum个时,排雷成功
int Win(char show[ROWS][COLS],int row,int col)
{
int count = 0;
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (show[i][j] == '*')
{
count++;
}
}
}
return count;
}
void Examine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
/*
坐标限制:
1、不能重复
2、不能超出有效范围:x>=1 && x<=row && y>=1 && y<=col
*/
/*
什么时候结束呢?两种情况:
1、被炸死。
2、排雷成功:排雷次数<row*col - 雷数
*/
int num = 0;
while (num< row * col-Mine_Sum)
{
printf("请玩家输入排雷坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//输入坐标不能重复
/*
检查show雷盘,未排雷过的坐标为字符*,排查过的坐标字符0或字符数字
*/
if (show[x][y] == '*')
{
//判断是否是雷
if (mine[x][y] == '0')
{
//进阶版----展开一片
Unfold(mine, show, x, y);
PrintMindefield(show, ROW, COL);
}
//是雷就结束
else
{
printf("您被炸死了\n");
PrintMindefield(mine, ROW, COL);
break;
}
}
else
{
printf("该坐标已被排查\n");
}
}
else
{
printf("输入的坐标超出范围\n");
}
if (Win(show, row, col) == Mine_Sum)
{
printf("恭喜你,排雷成功!\n");
PrintMindefield(mine, ROW, COL);
break;
}
}
}
使用递归实现,当周围坐标没有雷时,将该坐标设置为空字符,然后再依次对周围坐标判断是否被排查过并且不超过有效范围,如果满足,则递归,看该坐标的周围坐标是否符合没有雷条件。直到周围坐标有雷,递归就结束,开始返回。
总结
这就是扫雷小游戏,希望对您有所帮助,后续会出更多干货,多多支持,关注我**❤❤❤!**源代码自取。