【C语言】扫雷小游戏初学者版

成功的秘诀就是每天都比别人多努力一点。

今天给大家带来一款非常经典的小游戏------扫雷的实现和讲解

这里是目录

前言

在开始之前我们先来了解一下扫雷游戏的实现流程。

了解了游戏的基本流程,接下来我们就要跟着流程一步一步实现功能

整体框架

c 复制代码
//test.c
void test()
{
	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);
}
int main()
{
	test();
	return 0;
}

整体框架和之前讲过的猜数字小游戏一致,如有不了解的地方可以前往猜数字小游戏进行了解

1.打印菜单

c 复制代码
void menu()
{
	printf("****************************\n");
	printf("********   1.play   ********\n");
	printf("********   0.exit   ********\n");
	printf("****************************\n");
}

有了菜单我们就可以写game函数了,那么game函数怎么实现呢?

我们来分析一下:我们的下一步是要布置雷,但是布置雷之前是不是得有一个数组?

所以我们的下一步就是创建一个二维数组

2.创建二维数组

我们先创建一个9*9的二维数组,并布置雷。

假设"1"是我们布置好的雷,布置好雷后我们跟着流程继续往下走

接下来就是排查雷

那么我们在排查雷之前回去看一遍这个二维数组,如果我们在这个二维数组里进行排查会不会出现什么问题呢?

我们来试验一下

当我们把雷的个数打印出来的时候就出现问题了,这个"1"到底是排查出来他周围有"1"个雷的"1"呢?还是这个"1"是雷?

那我们能不能把雷改成" * "或者" # "呢?

答案是可行的,但是不建议这么做,这会让后面一些功能的实现变得困难,我们接着往下看

既然这样做会产生歧义,那我们应该怎么修改呢?

其实很简单

我们只需要创建2个二维数组就可以解决这个问题,1个二维数组用来存放雷的坐标,另一个二维数组用来存放排查出的雷的信息,这样我们在进行游戏的时候只需要打印排查出的雷的信息的数组就可以了

让我们再来分析一遍这个数组

如果我们每次统计都要对坐标的合法性进行判断,这就会非常的麻烦,那么怎么办呢?

方法也很简单,我们只需要在棋盘周围多创建 2行2列(11*11的数组) 就可以了,这样就不会有数组越界的情况发生了

定义棋盘大小:

cpp 复制代码
//game.h
//定义mine数组的行和列的大小为:9
#define ROWM 9
#define COLM 9
//定义show数组的行和列的大小为mine数组的行/列+2
#define ROWS ROWM + 2
#define COLS COLM + 2

创建棋盘:

c 复制代码
//test.c
void game()
{
	//1.需要存放布置好的雷的信息和排查雷的信息,所以需要2个数组
	char mine[ROWS][COLS] = { 0 };//布置好的雷
	char show[ROWS][COLS] = { 0 };//排查出的雷

}

如果show数组创建成9X9那么排查的时候需要计算数组的坐标太麻烦,所以我们把show数组也创建成11X11这样排查的坐标就可以直接套用

但实际只显示出9X9的棋盘

3.初始化棋盘

现在我们希望mine数组全部初始化为0,show数组全部初始化为*
我们可以写一个初始化函数来实现这个功能

game.h

c 复制代码
//定义数组的行和列的大小
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2

//声明初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);

test.c

c 复制代码
void game()
{
	//1.需要存放布置好的雷的信息和排查雷的信息,所以需要2个数组
	char mine[ROWS][COLS] = { 0 };//布置好的雷

	//如果show数组创建成9X9那么排查的时候需要计算数组的坐标太麻烦
	//所以我们把show数组也创建成11X11这样排查的坐标就可以直接套用
	//但实际只显示出9X9的棋盘
	char show[ROWS][COLS] = { 0 };//排查出的雷

	//传递棋盘,行和列,希望初始化的字符
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');

}

game.c

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

这样棋盘就初始化好了,那我们现在还需要一个打印函数来查看棋盘的状态

4.打印棋盘

打印函数的实现:

注意:我们只需要打印9X9的数组,而非11X11,但是在接收数组的时候要用11X11的数组接收

game.h

c 复制代码
//打印棋盘声明
void DisplayBoard(char board[ROWS][COLS], int row, int col);

game.c

c 复制代码
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	//注意:传递的数组是11X11的,并非9X9
	int i = 1;
	for (i = 1; i <= row; i++)
	{
		int j = 1;
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		//打印完一行后换行
		printf("\n");
	}
}

test.c中的game函数

c 复制代码
void game()
{
	//1.需要存放布置好的雷的信息和排查雷的信息,所以需要2个数组
	char mine[ROWS][COLS] = { 0 };//布置好的雷

	//如果show数组创建成9X9那么排查的时候需要计算数组的坐标太麻烦
	//所以我们把show数组也创建成11X11这样排查的坐标就可以直接套用
	//但实际只显示出9X9的棋盘
	char show[ROWS][COLS] = { 0 };//排查出的雷

	//传递棋盘,行和列,希望初始化的字符
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');

	//打印棋盘
	DisplayBoard(mine, ROW, COL);
	DisplayBoard(show, ROW, COL);
}

运行效果:

可以看到,我们的棋盘成功打印了,但是不难发现,我们很难确定行和列的坐标,所以我们还需要对打印函数进行优化

注:这里我们并不要打印mine数组,这里打印只是为了演示

优化后的打印函数:

c 复制代码
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	//注意:传递的数组是11X11的,并非9X9
	int i = 1;
	int j = 1;
	printf("--------扫雷--------\n");
	//打印列号
	for (j = 0; j <= col; j++)
	{
		printf("%d ", j);
	}
	printf("\n");

	for (i = 1; i <= row; i++)
	{
		//打印行之前先打印行号
		printf("%d ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		//打印完一行后换行
		printf("\n");
	}
	printf("--------扫雷--------\n");

}

运行效果:


5.布置棋盘中的雷

有了棋盘我们接下来要做的就是布置雷

布置雷的思路是布置到中间9X9的数组中,红色方框的位置
注意:传递的数组依旧为11X11,只是我们只用了其中的9X9,所以接收的数组也要用11X11来接收

我们需要在棋盘上随机布置雷,需要生成随机坐标(rand函数),我们就假定棋盘中只有10个雷

可以用#define来定义雷的个数,方便以后修改难度,我这边就定死为10个,自己实现的时候建议#define定义

game.h

c 复制代码
//布置雷函数的声明
void SetMine(char mine[ROWS][COLS], int row, int col);

game.c

c 复制代码
//布置雷函数的实现
void SetMine(char mine[ROWS][COLS], int row, int col)
{
	//雷的个数
	int count = 10;
	
	while (count)
	{
		//生成随机下标
		//注意行和列都是从1开始的
		//rand函数需要初始化,在test函数开头加上srand((unsigned int)time(NULL));
		int x = rand() % row + 1;
		int y = rand() % col + 1;

		//布置雷
		//判断坐标是否有雷
		if (mine[x][y] == '0')
		{
			//注意赋值不要写成:==
			mine[x][y] = '1';
			count--;
		}
	}
}

这样十颗雷就布置好了,打印出来看看效果:


6.排查雷和统计雷

有了棋盘,有了雷之后我们就可以排查雷了

排雷的参数和之前的一致,还是11X11中间的9X9

统计雷的思路:

game.h

c 复制代码
//排查雷函数的声明
void FindMind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

game.c

c 复制代码
//统计雷函数的实现
int get_mine_count(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] + mine[x + 1][y + 1] +
		mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0');
}

//排查雷的实现
void FindMind(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 - 10))
	{
		printf("请输入要排查的坐标->");
		//输入坐标
		scanf("%d %d", &x, &y);

		//判断坐标是否合法
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//判断坐标是否被排查过
			if (show[x][y] != '*')
			{
				printf("该坐标已被排查\n");
				continue;
			}
			//判断是否为雷
			if (mine[x][y] == '1')
			{
				printf("坐标为雷,游戏结束\n");
				DisplayBoard(mine, ROW, COL);
				break;
			}
			else
			{
				//统计坐标周围的雷
				int n = get_mine_count(mine, x, y);
				//注意返回的是int,我们棋盘的类型是char,所以需要转换
				show[x][y] = n + '0';
				DisplayBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("无效的坐标,请重新输入\n");
		}
	}
	if (win == (row * col - 10))
	{
		printf("游戏胜利\n");
		DisplayBoard(mine, ROW, COL);
	}
}

这样,我们的扫雷游戏就实现好了

运行效果:

总体代码

test.c

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include "game.h"

void menu()
{
	printf("****************************\n");
	printf("********   1.play   ********\n");
	printf("********   0.exit   ********\n");
	printf("****************************\n");
}

void game()
{
	//1.需要存放布置好的雷的信息和排查雷的信息,所以需要2个数组
	char mine[ROWS][COLS] = { 0 };//布置好的雷

	//如果show数组创建成9X9那么排查的时候需要计算数组的坐标太麻烦
	//所以我们把show数组也创建成11X11这样排查的坐标就可以直接套用
	//但实际只显示出9X9的棋盘
	char show[ROWS][COLS] = { 0 };//排查出的雷

	//传递棋盘,行和列,希望初始化的字符
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');

	//打印棋盘
	DisplayBoard(show, ROW, COL);
	//DisplayBoard(mine, ROW, COL);

	//布置雷
	SetMine(mine, ROW, COL);
	
	//排查雷
	FindMind(mine, show, ROW, COL);
}

void test()
{
	srand((unsigned int)time(NULL));
	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);
}

int main()
{
	test();
	return 0;
}

game.c

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)
{
	//注意:传递的数组是11X11的,并非9X9
	int i = 1;
	int j = 1;
	printf("--------扫雷--------\n");
	//打印列号
	for (j = 0; j <= col; j++)
	{
		printf("%d ", j);
	}
	printf("\n");

	for (i = 1; i <= row; i++)
	{
		//打印行之前先打印行号
		printf("%d ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		//打印完一行后换行
		printf("\n");
	}
	printf("--------扫雷--------\n");

}

//布置雷函数的实现
void SetMine(char mine[ROWS][COLS], int row, int col)
{
	//雷的个数
	int count = 10;
	
	while (count)
	{
		//生成随机下标
		//注意行和列都是从1开始的
		int x = rand() % row + 1;
		int y = rand() % col + 1;

		//布置雷
		//判断坐标是否有雷
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

//统计雷函数的实现
int get_mine_count(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] + mine[x + 1][y + 1] +
		mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0');
}

//排查雷的实现
void FindMind(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 - 10))
	{
		printf("请输入要排查的坐标->");
		//输入坐标
		scanf("%d %d", &x, &y);

		//判断坐标是否合法
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//判断坐标是否被排查过
			if (show[x][y] != '*')
			{
				printf("该坐标已被排查\n");
				continue;
			}
			//判断是否为雷
			if (mine[x][y] == '1')
			{
				printf("坐标为雷,游戏结束\n");
				DisplayBoard(mine, ROW, COL);
				break;
			}
			else
			{
				//统计坐标周围的雷
				int n = get_mine_count(mine, x, y);
				//注意返回的是int,我们棋盘的类型是char,所以需要转换
				show[x][y] = n + '0';
				DisplayBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("无效的坐标,请重新输入\n");
		}
	}
	if (win == (row * col - 10))
	{
		printf("游戏胜利\n");
		DisplayBoard(mine, ROW, COL);
	}
}

game.h

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//定义mine数组的行和列的大小为:9
#define ROW 9
#define COL 9
//定义show数组的行和列的大小为mine数组的行/列+2
#define ROWS ROW + 2
#define COLS COL + 2

//声明初始化棋盘
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 mine[ROWS][COLS], int row, int col);

//排查雷函数的声明
void FindMind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

进阶(递归展开)

递归展开函数的作用是为了达到周围没雷时直接展开周围所有空间,利用递归不断向外展开,直至周围有雷为止

代码如下:

添加is_win函数和Exp函数并修改FindMind函数逻辑即可

c 复制代码
//判断是否胜利
int is_win(char show[ROWS][COLS])
{
	int count1 = 0;
	int i = 0, j = 0;
	for (i = 1; i <= ROW; i++)
	{
		for (j = 1; j <= COL; j++)
		{
			if (show[i][j] != '*')
			{
				count1++;
			}
		}
	}
	return count1;
}

//递归展开函数
Exp(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{

	if (x >= 1 && x <= ROW && y >= 1 && y <= COL)//防止越界
	{
		int count = get_mine_count(mine, x, y);//获取该位置周围雷数
		if (count == 0)//周围没雷
		{
			show[x][y] = ' ';//赋值为空字符
			int i = 0, j = 0;
			for (i = x - 1; i <= x + 1; i++)//限制行
			{
				for (j = y - 1; j <= y + 1; j++)//限制列
				{
					if (show[i][j] == '*' && (i != x || j != y))//防止重复访问
					{
						Exp(mine, show, i, j);//递归调用
					}

				}
			}
		}
		else
		{
			show[x][y] = count + '0';//显示雷数
		}
	}
}

//排查雷的实现
void FindMind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	//可以在适当的位置加入清屏函数来实现界面整洁:system("cls");
	int x = 0;
	int y = 0;
	//记录排查过的不是雷的个数
	int win = 0;
	//如果所有不为雷的个数都被排查过,游戏胜利
	while(win < (row * col - 10))
	{
		printf("请输入要排查的坐标->");
		//输入坐标
		scanf("%d %d", &x, &y);

		//判断坐标是否合法
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//判断坐标是否被排查过
			if (show[x][y] != '*')
			{
				printf("该坐标已被排查\n");
				continue;
			}
			//判断是否为雷
			if (mine[x][y] == '1')
			{
				printf("坐标为雷,游戏结束\n");
				DisplayBoard(mine, ROW, COL);
				break;
			}
			else
			{
				//统计坐标周围的雷
				Exp(mine, show, x, y);
				win = is_win(show);
				DisplayBoard(show, ROW, COL);
			}
		}
		else
		{
			printf("无效的坐标,请重新输入\n");
		}
	}
	if (win == (row * col - 10))
	{
		printf("游戏胜利\n");
		DisplayBoard(mine, ROW, COL);
	}
}

递归展开运行效果:

以上就是本篇文章的全部内容了,希望大家看完能有所收获

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜7 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa7 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio