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

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


总结

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

相关推荐
ChoSeitaku14 分钟前
链表循环及差集相关算法题|判断循环双链表是否对称|两循环单链表合并成循环链表|使双向循环链表有序|单循环链表改双向循环链表|两链表的差集(C)
c语言·算法·链表
娅娅梨16 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
DdddJMs__13519 分钟前
C语言 | Leetcode C语言题解之第557题反转字符串中的单词III
c语言·leetcode·题解
汤米粥22 分钟前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾25 分钟前
EasyExcel使用
java·开发语言·excel
拾荒的小海螺31 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
娃娃丢没有坏心思1 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
白-胖-子1 小时前
【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-统计数字
开发语言·c++·算法·蓝桥杯·等考·13级
好睡凯1 小时前
c++写一个死锁并且自己解锁
开发语言·c++·算法