【C语言】简易版扫雷+进阶版扫雷

目录

前言

一、分模块化

二、准备雷盘

[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;
		}
	}
}

使用递归实现,当周围坐标没有雷时,将该坐标设置为空字符,然后再依次对周围坐标判断是否被排查过并且不超过有效范围,如果满足,则递归,看该坐标的周围坐标是否符合没有雷条件。直到周围坐标有雷,递归就结束,开始返回。


总结

这就是扫雷小游戏,希望对您有所帮助,后续会出更多干货,多多支持,关注我**❤❤❤!**源代码自取。

相关推荐
isyangli_blog12 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb20081113 小时前
FastAPI APIRouter
开发语言·python
Benszen13 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆13 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木13 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充13 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~13 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball61613 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
春生野草14 小时前
反射、Tomcat执行
java·开发语言
雪的季节15 小时前
企业级 Qt 全功能项目
开发语言·数据库·qt