C语言实现扫雷(递归函数的应用)


封面
扫雷游戏作为一款我们小时候电脑上为数不多的几个游戏之一,其功能简单,界面简洁。本篇将介绍如何用C语言实现扫雷游戏。本片以介绍9×9的棋盘,内含10颗雷的游戏难度为大家介绍,大家也可以在代码中更改棋盘的尺寸和雷的个数。

游戏介绍

扫雷,顾名思义就是要找到埋在地里的雷,游戏中9×9的棋盘代表了所有土地,每一个小格代表土地的一部分,其中可能埋着雷,也可能没有雷。玩家选择一个坐标来扫雷,若被选中的坐标区域有雷,则游戏失败;若没有雷,则会显示其周围八个坐标的有雷坐标的个数,若其周围没有雷,则会对周围坐标显示其周围的雷数。若将全部没有埋雷的坐标都扫完,则游戏胜利。

实现各功能代码

游戏的控制代码

如果我们要写一个游戏代码,首先要写一个控制代码,用来实现游戏的进入或退出。
那么我们希望程序运行后会出现一个菜单,提示我们:输入1进入游戏,输入2退出游戏,其他输入则提示输入错误,并重新输入。
实现该功能的代码如下:

main()
{
another:
	printf("--------------------------\n");
	printf("---------1  PLAY----------\n");
	printf("---------2  EXIT----------\n");
	printf("--------------------------\n");
	printf("请选择:");
	int input = 0;
	scanf("%d", &input);
	if (input == 1) {
		game();
	}
	else if (input == 2) {
		printf("游戏退出\n");
	}
	else {
		printf("输入错误,请重新输入。\n");
		goto another;
	}
	return 0;
}

实现游戏功能的代码

通过上述代码不难看出,若输入1,则会执行game()函数,执行完后程序结束。那么game()函数该具备那些功能呢?

初始化两个棋盘

大家如果流览过其他的扫雷游戏的代码,会发现都使用了两个二维数组来表示棋盘,那么为什么要用两个数组呢?一个棋盘用一个数组不久可以表示了吗?原因是这样,如果用一个数组来表示棋盘,将不利于分辨该坐标是否被排查过,同时也不利于棋盘的输出。那么我们就用到了两个棋盘,第一个棋盘用来布置雷和计算周围雷的个数,这个棋盘不会输出给玩家看;第二个棋盘初始化全都为*来表示未知,排查过的坐标则显示该坐标周围的雷的个数。这样做的好处很显然,第一个棋盘(即数组)是不会改变的,这样对我们统计某个坐标周围雷的个数时很方便的,如果只使用一个棋盘,则会非常混乱。
我们将第一个数组初始化为0,并随机产生10个坐标,将数组的值改为1,这样当我们计算某个坐标周围的雷的个数时,只需遍历他周围的8个坐标,并将数组的值相加,相加的结果即为雷的个数。
到这里有人就会发现问题了,这种计算雷的个数的方法,对于处在棋盘中间的坐标是可行的,但是对于在棋盘边上的坐标是不可行的,因为这种计算方法会导致数组越界,即计算(9,9)这个位置的周围雷的个数时,会将他周围一圈的坐标的数组值相加,这样就会出现(10,8)(10,9)(10,10)(9,10)(8,10)这几个坐标,很显然我们的数组就会越界,那么为了避免这种情况,我们可以选择将数组定义为[ROW+2][COL+2]这种大小(ROW表示棋盘的行数,COL表示棋盘的列数),即将棋盘向外扩展了一圈,我们将拓展出来的这一圈坐标对应的数组值定义为0,随机生成雷的时候,限制雷不能生成在最外圈,这样就可以完美契合我们的思路了,这一步是整个扫雷游戏代码的最精华的思想。
我们可以先创建一个头文件,然后在头文件中设置棋盘的尺寸和雷的个数。代码如下:

#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 mine_number 10    //设置雷的个数

首先我们要初始化两个棋盘,可以创建一个code.c文件,这个文件中用于存放游戏部分的函数代码。
数组初始化函数:

//将数组初始化为字符'set'
void Init(char arr[ROWS][COLS], int rows, int cols, char set)
//注意传递的数组的行数和列数,这里数组的实际大小为[ROWS][COLS],且我们想要全部初始化,
//所以后面两个参数我们定义的是 rows 和 cols ,即提醒我们传递 ROWS 和 COLS,而不是传递 ROW 和 COL 
#define COLS
{
	int i = 0;
	for (i = 0; i < rows; i++) {
		int j = 0;
		for (j = 0; j < cols; j++) {
			arr[i][j] = set;
		}
	}
}

布置雷

棋盘初始化好了之后,我们就可以随机放置地雷了。
布置雷函数:

void Set_mine(char arr[ROWS][COLS])
{
	srand((unsigned int)time(NULL));
	int num = mine_number;
	while (num) {
		int x = rand() % 9 + 1; //生成1~9之间的随机数,不能生成0和10,因为最外圈不布置雷
		int y = rand() % 9 + 1;
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			num--;
		}
	}
}

棋盘的输出

布置好雷之后,我们可以将棋盘输出给玩家看了,但我们输出的一定是第二个棋盘,那个被我们初始化为*的棋盘,这里如果一不小心将第一个棋盘输出了,那就相当于是把答案告诉玩家了,不过在我们调试程序的过程中可以将第一个棋盘打印出来,用来辅助我们调试。为了方便输出,我们可以写一个输出数组的函数。
数组输出函数:

void Print(char arr[ROWS][COLS], int row, int col)
//这里数组的实际大小虽然为[ROWS][COLS],但我们只需要打印棋盘大小的数组即可,
//所以我们后续的实参需要给的是 ROW 和 COL ,这里定义形参为 row 和 col 起提示作用
{
	for (int r = 0; r <= rows; r++) //输出横坐标轴
		printf("%d ", r);
	printf("\n");
	int i = 0;
	for (i = 1; i <= rows; i++) {
		printf("%d ", i); //输出纵坐标轴
		int j = 0;
		for (j = 1; j <= cols; j++) {
			printf("%c ", arr[i][j]);
		}
		printf("\n");
	}
}

计算周围雷的个数

玩家输入了想要扫雷的坐标后,程序要计算该坐标周围雷的个数,并将该值替换第二个数组的*,然后输出第二个数组给玩家显示扫雷后的结果。
同时在头文件中定义一个全局数组win[1]并初始化为0,然后每执行一次计算函数,win[0]的值就+1,这样win[0]的值就可以代表已扫过的坐标个数。(指针还没学,暂时用数组代替)
计算函数的实现很简单,只是需要注意我们最开始定义的是字符数组,所以在进行加减计算时,记得'0'的计算。
计算坐标周围雷的个数,并将该值替换在第二个数组中,函数代码如下:

void Count(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
	int num = 0;
	for (int i = x - 1; i <= x + 1; i++)
	{
		for (int j = y - 1; j <= y + 1; j++) {
			if (i == x && j == y)
				continue;
			else {
				num += arr1[i][j] - '0';
			}
		}
	}
	arr2[x][y] = num + '0';
	win[0]++;
}

扫雷函数

扫雷函数首先要是一个循环,在雷未被扫完时,一直进行这个循环,在雷被扫完后,跳出循环,并提示玩家游戏胜利。判断游戏胜利的条件可以为win[0]==ROW*COL-mine_number,即扫过的坐标个数=棋盘的总坐标个数-埋了雷的坐标个数;也可以遍历第二个数组,看该数组中有多少个*,如果*的个数等于雷的个数,则游戏胜利,这种方式不需要用到win[0],所以在上一步中也无需定义win[1]
循环体中,首先需要玩家输入要判断的坐标,然后判断该坐标是否合法,再判断该坐标是否有雷,再判断该坐标是否已排查过,最后进行扩展函数,输出扩展后的第二个数组。(扩展函数见下)
扫雷函数:

void Clear_mine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col) {
	while (win[0] < ROW * COL - mine_number)   //替换部分
	//while(Num(arr2, ROW, COL) > mine_number)
	{
		printf("请输入扫雷的坐标:");
		int x = 0;
		int y = 0;
		scanf("%d%d", &x, &y);
		if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { //判断坐标是否合法
			printf("\n");
			if (arr1[x][y] == '1') {
				printf("游戏失败。\n");
				Print(arr1, ROW, COL); //游戏失败后,显示埋雷情况
				break;
			}
			else {
				if (arr2[x][y] != '*') {
					printf("这个位置已经排查过了,请重新选择。\n");
				}
				else if(arr1[x][y]=='0') {
					Extend(arr1, arr2, x, y); //周围展开函数
					Print(arr2, ROW, COL);
				}
			}
		}
		else {
			printf("非法输入,请重新选择。\n");
		}
		if (win[0] == ROW * COL - mine_number) {   //替换部分
			printf("恭喜你!游戏获胜!\n");          //替换部分
			break;                                 //替换部分
		}                                          //替换部分
		/*if (Num(arr2, ROW, COL) == mine_number) {
			printf("恭喜你!游戏获胜!\n");
			break;
		}*/
	}
}

上述代码使用的是win[0]==ROW*COL-mine_number这个判断条件,如果想使用另一个判断条件,就替换上述代码中的注释部分,并在这个函数前面定义一个数第二个数组中*的个数的函数即可。(下面的代码一定要在上面的代码之前)
*函数:

int Num(char arr[ROWS][COLS], int row , int col )
{
	int number = 0;
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			if (arr[i][j] == '*')
				number++;
		}
	}
	return number;
}

扩展函数(递归部分)

扩展函数是整个程序最大的难点,什么条件下执行递归,如何停止递归。
Clear_mine函数中,如果arr1[x][y]=='0'成立,就执行Extend(arr1, arr2, x, y),所以我们第一步就是先计算(x,y)坐标周围的雷的个数,所以Extend函数的第一步就是执行一次Count函数。
扫雷游戏在什么情况下会展开周围的坐标呢?我们可以玩两把扫雷游戏找找规律,展开对主坐标和展开坐标都有要求。首先主坐标不能雷,且周围雷的个数为0,代码表示为if (arr1[x][y] == '0' && arr2[x][y] == '0') ,然后遍历周围八个坐标,如果其不是雷,则以该坐标作为主坐标,执行Extend
那么我们的代码就可以表示为:

void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
	Count(arr1, arr2, x, y);
	if (arr1[x][y] == '0' && arr2[x][y] == '0') 
	{
		for (int i = x - 1; i <= x + 1; i++)
		{
			for (int j = y - 1; j <= y + 1; j++) 
			{
				if (arr1[i][j] == '0') 
					Extend(arr1, arr2, i, j);
			}
		}
	}
}

如果用这个代码去调试,就会发现程序进入了死循环,这是为什么呢?
原来是已经扫过的坐标又被作为周围坐标,然后符合做主坐标的条件,从而进入了死循环。即以A坐标作为主坐标展开,展开遇到B坐标,B坐标符合做主坐标的条件,所以以B作为主坐标展开,展开遇到A坐标,A坐标符合…从而进入死循环,那么为了避免这种情况,我们可以设置某个坐标只有未被展开过才能以其为主坐标执行Extend
代码表示为:

void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
	Count(arr1, arr2, x, y);
	if (arr1[x][y] == '0' && arr2[x][y] == '0') 
	{
		for (int i = x - 1; i <= x + 1; i++)
		{
			for (int j = y - 1; j <= y + 1; j++) 
			{
				if (arr1[i][j] == '0' && arr2[i][j] == '*') 
					Extend(arr1, arr2, i, j);
			}
		}
	}
}

这样代码就不会死循环了,运行调试感觉也像模像样的。
在这里插入图片描述
但我们多试几次,就会发现代码是有问题的。
在这里插入图片描述
可以看到,左边部分的代码展开是没有问题的,但是为什么右面也有部分代码被展开了呢?
经过数个小时的调试,终于发现了问题所在,原来我们之前设置的最外面一圈数也参与进了递归的判断。
也就是说,除了输出的9×9棋盘,最外面还有一圈0,这一圈0符合本身没有雷,所以只要他的周围也没有雷,就会以他作为主坐标展开,就会有可能逐个以最外圈的坐标作为主坐标展开,从而展开到很远的地方,上述情形就是如此。
所以在判断条件中还应该限制坐标在(1,1)~(9,9)之间。
所以最终我们的Extend函数应该为:

void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
	Count(arr1, arr2, x, y);
	if (arr1[x][y] == '0' && arr2[x][y] == '0') 
	{
		for (int i = x - 1; i <= x + 1; i++)
		{
			for (int j = y - 1; j <= y + 1; j++) 
			{
				if (arr1[i][j] == '0' && i >= 1 && j >= 1 && arr2[i][j] == '*' && i<=ROW && j<=COL)
					Extend(arr1, arr2, i, j);
			}
		}
	}
}

至此我们的扫雷游戏各个部分就已经都准备好了。

参考代码

code.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "code.h"
#include<time.h>


extern mine;
extern show;
extern int win[1] = { 0 };


void Init(char arr[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++) {
			arr[i][j] = set;
		}
	}
}


void Print(char arr[ROWS][COLS], int row, int col)
{
	for (int r = 0; r <= row; r++)
		printf("%d ", r);
	printf("\n");
	int i = 0;
	for (i = 1; i <= row; i++) {
		printf("%d ", i);
		int j = 0;
		for (j = 1; j <= col; j++) {
			printf("%c ", arr[i][j]);
		}
		printf("\n");
	}
}

void Set_mine(char arr[ROWS][COLS])
{
	srand((unsigned int)time(NULL));
	int num = mine_number;
	while (num) {
		int x = rand() % 9 + 1;
		int y = rand() % 9 + 1;
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			num--;
		}
	}
}

void Count(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
	int num = 0;
	for (int i = x - 1; i <= x + 1; i++)
	{
		for (int j = y - 1; j <= y + 1; j++) {
			if (i == x && j == y)
				continue;
			else {
				num += arr1[i][j] - '0';
			}
		}
	}
	arr2[x][y] = num + '0';
	win[0]++;
}

void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
	Count(arr1, arr2, x, y);
	if (arr1[x][y] == '0' && arr2[x][y] == '0') 
	{
		for (int i = x - 1; i <= x + 1; i++)
		{
			for (int j = y - 1; j <= y + 1; j++) 
			{
				if (arr1[i][j] == '0' && i >= 1 && j >= 1 && arr2[i][j] == '*' && i<=ROW && j<=COL) 
					Extend(arr1, arr2, i, j);
			}
		}
	}
}

void Clear_mine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col) {
	while (win[0] < ROW * COL - mine_number)
	//while(Num(arr2, ROW, COL) > mine_number)
	{
		printf("请输入扫雷的坐标:");
		int x = 0;
		int y = 0;
		scanf("%d%d", &x, &y);
		if (x >= 1 && x <= ROW && y >= 1 && y <= COL) {
			printf("\n");
			if (arr1[x][y] == '1') {
				printf("游戏失败。\n");
				Print(arr1, ROW, COL);
				break;
			}
			else {
				if (arr2[x][y] != '*') {
					printf("这个位置已经排查过了,请重新选择。\n");
				}
				else if(arr1[x][y]=='0') {
					Extend(arr1, arr2, x, y);
					Print(arr2, ROW, COL);
				}
			}
		}
		else {
			printf("非法输入,请重新选择。\n");
		}
		if (win[0] == ROW * COL - mine_number) {
			printf("恭喜你!游戏获胜!\n");
			break;
		}
		/*if (Num(arr2, ROW, COL) == mine_number) {
			printf("恭喜你!游戏获胜!\n");
			break;
		}*/
	}
}

code.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 mine_number 10 


//初始化
void Init(char arr[ROWS][COLS], int rows, int cols, char set);

//打印数组
void Print(char arr[ROWS][COLS], int rows, int cols);

//埋雷
void Set_mine(char arr[ROWS][COLS]);

//扫雷
void Clear_mine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col);

//展开周围
void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y);

Mine_game.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "code.h"

void game()
{
	//创建两个二维数组并初始化
	char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };
	Init(mine,ROWS,COLS, '0');
	Init(show, ROWS, COLS, '*');
	//打印
	//Print(mine, ROW, COL);
	//Print(show, ROW, COL);
	printf("该棋盘中共有%d个雷,请将其他区域选择出来。\n", mine_number);
	//随机加入地雷
	Set_mine(mine);
	//Print(mine, ROW, COL);//调试时可以取消注释,打印mine数组
	Print(show, ROW, COL);
	//扫雷
	Clear_mine(mine,show,ROW,COL);
}



main()
{
another:
	printf("--------------------------\n");
	printf("---------1  PLAY----------\n");
	printf("---------2  EXIT----------\n");
	printf("--------------------------\n");
	printf("请选择:");
	int input = 0;
	scanf("%d", &input);
	if (input == 1) {
		game();
	}
	else if (input == 2) {
		printf("游戏退出\n");
	}
	else {
		printf("输入错误,请重新输入。\n");
		goto another;
	}
	return 0;
}

运行演示

控制代码

在这里插入图片描述

扫雷游戏

为了方便演示,这里设置雷的个数为3。
在这里插入图片描述

结语

扫雷游戏是C语言的一道很老的练手题了,最巧妙的是用两个棋盘来分别表示埋雷和扫雷。但哪怕是在知道这种方法的情况下,想完整的写出扫雷游戏对初学者来说仍很困难,在我写的过程中,耗费时间最长的就是Extend函数的递归,在何种情况下进入递归,文章中写的过程就是我试错的过程,写出来后确实看着不难,但想写出来确实不简单。
除此之外,扫雷游戏还可以再完善,比如:第一步一定不会踩到雷,标记雷的功能,记录走的步数,记录游戏时间等等,这些就属于细枝末节了,想加的话可以自己写一写。
最后,祝大家都能写出来!

  • 31
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值