递归还能这么玩?递归实现扫雷游戏

🥳🥳🥳大家好啊,想必在C语言的学习中,总想自己实现一些小项目,那么,今天就由我来带领大家实现一个扫雷小游戏吧。

如果对其它小游戏有兴趣的话可以看看👀C语言实现三子棋?五子棋?不,是n子棋

如果想了解更多C语言的小知识也可以 关注🎄我的博客φ(゜▽゜)♪*

让我们看看今天的内容吧

功能的主要实现:

  • 可以判断一个坐标附近的雷的数量

  • 可以使用标记来记录雷的位置

  • 选择一个坐标,可以展开一片无效区域(递归实现)

    效果图:
    image-20220508190437718


🏆头文件 mine_clear.h

#pragma once
#ifndef MINE_CLEAR_H
#define MINE_CLEAR_H

//确定棋盘大小
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
//确定雷的数量
#define MINE 10

//包含的头文件
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//数组的声明
void mine_clear();
void InitBoard(char board[ROWS][COLS], char ch);
void DisplayBoard(char board[ROWS][COLS]);
void Setmine(char mine[ROWS][COLS]);
void ChectMine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);
void ShowMark(char show[ROWS][COLS], int x, int y, char set);
int IsWin(char show[ROWS][COLS]);
int IsMine(char mine[ROWS][COLS], int x, int y);

#endif

有细心的小伙伴肯定发现了,我在确定雷盘大小的时候,有一个 ROWS 和一个 COLS 这又有什么用呢?

image-20220507222945593

假设这是一个 9*9 的扫雷盘,当我们用用 数组 访问到边界的时候,有可能会产生 越界访问 ,为了避免这样的情况,我们可以选择将数组 扩大一圈,这样可以防止搜索的时候数组越界,而显示的时候只要不将多余的隐藏边缘显示出来就好了

image-20220507224713142

🏆游戏的具体实现

🤔🤔既然如此,我们又应该怎样实现呢?就随着我一起接着往下看吧🧐🧐

🔥主要流程 test.c

#include "mine_clear.h"

void mine_clear()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int mx = 0;//标记用横坐标
	int my = 0;//标记用纵坐标
	char show[ROWS][COLS] = { 0 };//展示的盘
	char mine[ROWS][COLS] = { 0 };//记录雷的盘
	//初始化棋盘
	InitBoard(show, '*');
	InitBoard(mine, '0');
	
	//设置雷
	Setmine(mine);

	while (1)
	{
		system("cls");
		DisplayBoard(show);

    again:
		printf("************************\n");
		printf("*    1 -> 排雷         *\n");
		printf("*    2 -> 标记         *\n");
		printf("*    3 -> 取消标记     *\n");
		printf("************************\n");
		printf("\t\t请选择>:");
		scanf("%d", &input);

		switch (input)
		{
		case 1:

			printf("\t请输入坐标(x, y)>:");
			scanf("%d,%d", &x, &y);
			ChectMine(mine, show, x, y);
			break;
		case 2:
			printf("\t请输入坐标(x, y)>:");
			scanf("%d,%d", &mx, &my);
			ShowMark(show, mx, my, '#');
			break;
		case 3:
			printf("\t请输入坐标(x, y)>:");
			scanf("%d,%d", &mx, &my);
			ShowMark(show, mx, my, '*');
			break;
		default:
                printf("输入错误,请从新输入\n");
                goto again;
			break;
		}

		DisplayBoard(show);
		
		//踩雷直接退出
		if (IsMine(mine, x, y))
		{
			printf("你输了\n");
			printf("雷图\n");
			DisplayBoard(mine);
			break;
		}
		//获胜直接退出
		if (IsWin(show))
		{
			printf("你赢了\n");
			DisplayBoard(mine);
			break;
		}
	}
}

void menu()
{
	printf("************************\n");
	printf("**** 0. 退出游戏 *******\n");
	printf("**** 1. 开始游戏 *******\n");
	printf("************************\n");
	printf("\t\t请选择>:");
}

void Start()
{
	int input = 0;
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
			case 0:
				break;
			case 1:
				mine_clear();
				break;
			default:
				printf("输入错误,请从新输入\n");
				break;
		}
	} while (input);
}

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

注意: 展示用数组和,雷盘的数组大小都要相同(😅目的主要是为了用一个函数来操作两个数组)

🔥主要功能实现 mine_clear.c

🚀初始化棋盘

//初始化棋盘
void InitBoard(char board[ROWS][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;
		}
	}
}

注意: 为了能达到想初始化成什么样的棋盘就初始成什么样的棋盘,我们需要一个 set 来方便我们规定棋盘的样子

效果如图:

image-20220507235109844

🚀棋盘的打印

你也许很好奇,我是怎样打印上面的棋盘的吧😝(我不管,你很好奇)

//打印棋盘
void DisplayBoard(char board[ROWS][COLS])
{
	int i = 0;
	int j = 0;
	printf("\n");
	for (j = 0; j < COL+1; j++)
	{
		printf("%d ", j);
	}
	printf("\n");
	for (i = 1; i < ROW+1; i++)
	{
		printf("%d ", i);
		for (j = 1; j < COL+1; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}

**为了使选择坐标的时候更方便观看,我们可以在打印棋盘的时候加上 横纵坐标 **

🚀地雷的设置

单有棋盘,没有雷,那也能叫扫雷?所以我需要生成一些雷,来给人排雷

具体实现如下:

//设置雷
void Setmine(char mine[ROWS][COLS])
{
	srand((unsigned int)time(NULL));
	for (int i = 0; i < MINE; i++)
	{
		int x = rand() % COL + 1;
		int y = rand() % ROW + 1;
		if (mine[y][x] == '0')
		{
			mine[y][x] = '1';
		}
		else if (mine[y][x] == '1')	
		{
			i--;
		}
    }
}

我们将雷的位置用 字符'1' 表示,可以方便我们后续的程序编写

🚀递归排雷

//检查雷的数量
static int get_mine_num(char mine[ROWS][COLS], int x, int y)
{
	return mine[y - 1][x - 1] + mine[y - 1][x] + mine[y - 1][x + 1]
		+ mine[y][x - 1] + mine[y][x + 1]
		+ mine[y + 1][x - 1] + mine[y + 1][x] + mine[y + 1][x + 1] - 8*'0';
}

用于检查雷的数量

image-20220508173613841

思想:

  • 因为我们初始化时使用的是字符类型,所以我们将周围 8个点所对应的数字字符ASCII值 相加后,再减去 8 * ‘ 0 ’,结果相当于 8 个整数的和

    image-20220508175129769

void ChectMine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
	char minenum = get_mine_num(mine, x, y) + '0';
	if (show[y][x] == '*')
	{
		//如果全0就继续递归
		if (minenum == '0')
		{
			show[y][x] = '0';
			ChectMine(mine, show, x-1, y-1);
			ChectMine(mine, show, x, y-1);
			ChectMine(mine, show, x+1, y-1);
			ChectMine(mine, show, x-1, y);
			ChectMine(mine, show, x+1, y);
			ChectMine(mine, show, x-1, y+1);
			ChectMine(mine, show, x, y+1);
			ChectMine(mine, show, x+1, y+1);

		}
		//如果有一个大于0,就将 * 变成 雷的数量
		if (minenum > '0')
		{
			show[y][x] = minenum;
		}
	}
}

🤔🤔思考:

  • 往8个方向一直扩展我们可以 利用递归show 数组
  • 当周围没有雷的时候向外扩展
  • 想要实现递归首先要有限制条件
  • 递归时必定会产生 查询重复

解决方案:

  1. 当前检测的位置 (show 数组) 为 '*' 时,才能进行替换
  2. 当检测到周围雷的数量为 0 时,才能继续扩展
  3. 若周围雷的数量不为 0,则将雷的数量显示在 show 数组(主要防止一键扫雷😅)

注意:

在进行扩展输入坐标时不要传错了😂😂

image-20220508182309719

🚀标记功能

标记可以说作为扫雷游戏中并不怎么起眼的一个作用,其主要作用就是防止玩家 误触错误选择 而存在,在进行递归排雷的时候也会主动规避掉标记,由此来设计我们的标记功能就很明确了

//标记
void ShowMark(char show[ROWS][COLS], int x, int y, char set)
{
	if (set == '#')//标记
	{
		if (show[y][x] == '*')
		{
			show[y][x] = '#';
		}
		else
		{
			printf("此处无法标记\n");
		}
	}
	if (set == '*')//取消标记
	{
		if (show[y][x] == '#')
		{
			show[y][x] = '*';
		}
		else
		{
			printf("此处无法取消标记\n");
		}
	}
}

show 数组 上的当前位置 为 '*'的时候才能 进行标记(防止无效标记引起BUG)

show 数组 上的当前位置 为 '#'的时候才能 取消标记(防止无效取消标记引起BUG)

🚀判断是否获胜

//判断是否获胜,获胜返回1 , 否返回 0
int IsWin(char show[ROWS][COLS])
{
	int i = 0;
	int j = 0;
	int count = 0;
	for (i = 1; i < ROW + 1; i++)
	{
		for (j = 1; j < COL + 1; j++)
		{
			if (show[i][j] == '*' || show[i][j] == '#')
			{
				count++;
			}
		}
	}
	if (count == MINE)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

判断获胜只要我们棋盘上未显示的区域等于雷的数量就可以表示获胜

🚀判断当前位置是否是雷

int IsMine(char mine[ROWS][COLS], int x, int y)
{
	return mine[y][x] - '0';
}

因为我们将雷设置成 字符'1', 所以直接返回 当前位置减去 '0'的值就可以判断真或者假了

🏆代码的总体预览

🔥mine_clear.h

#pragma once
#ifndef MINE_CLEAR_H
#define MINE_CLEAR_H

//确定棋盘大小
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2

//确定雷的数量
#define MINE 10

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void mine_clear();
void InitBoard(char board[ROWS][COLS], char ch);
void DisplayBoard(char board[ROWS][COLS]);
void Setmine(char mine[ROWS][COLS]);
void ChectMine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);
void ShowMark(char show[ROWS][COLS], int x, int y, char set);
int IsWin(char show[ROWS][COLS]);
int IsMine(char mine[ROWS][COLS], int x, int y);

#endif

🔥test.c

#include "mine_clear.h"

void mine_clear()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int mx = 0;
	int my = 0;
	char show[ROWS][COLS] = { 0 };//展示的盘
	char mine[ROWS][COLS] = { 0 };//记录雷的盘
	//初始化棋盘
	InitBoard(show, '*');
	InitBoard(mine, '0');

	//设置雷
	Setmine(mine);

	while (1)
	{
		system("cls");
		DisplayBoard(show);

	again:
		printf("************************\n");
		printf("*    1 -> 排雷         *\n");
		printf("*    2 -> 标记         *\n");
		printf("*    3 -> 取消标记     *\n");
		printf("************************\n");
		printf("\t\t请选择>:");
		scanf("%d", &input);

		switch (input)
		{
		case 1:

			printf("\t请输入坐标(x, y)>:");
			scanf("%d,%d", &x, &y);
			ChectMine(mine, show, x, y);
			break;
		case 2:
			printf("\t请输入坐标(x, y)>:");
			scanf("%d,%d", &mx, &my);
			ShowMark(show, mx, my, '#');
			break;
		case 3:
			printf("\t请输入坐标(x, y)>:");
			scanf("%d,%d", &mx, &my);
			ShowMark(show, mx, my, '*');
			break;
		default:
			printf("输入错误,请从新输入\n");
			goto again;
			break;
		}

		DisplayBoard(show);
		
		//踩雷直接退出
		if (IsMine(mine, x, y))
		{
			printf("你输了\n");
			printf("雷图\n");
			DisplayBoard(mine);
			break;
		}
		//获胜直接退出
		if (IsWin(show))
		{
			printf("你赢了\n");
			DisplayBoard(mine);
			break;
		}
	}
}

void menu()
{
	printf("************************\n");
	printf("**** 0. 退出游戏 *******\n");
	printf("**** 1. 开始游戏 *******\n");
	printf("************************\n");
	printf("\t\t请选择>:");
}

void Start()
{
	int input = 0;
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
			case 0:
				break;
			case 1:
				mine_clear();
				break;
			default:
				printf("输入错误,请从新输入\n");
				break;
		}
	} while (input);
}

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

🔥mine_clear.c

#include "mine_clear.h"

//初始化棋盘
void InitBoard(char board[ROWS][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 i = 0;
	int j = 0;
	printf("\n");
	for (j = 0; j < COL+1; j++)
	{
		printf("%d ", j);
	}
	printf("\n");
	for (i = 1; i < ROW+1; i++)
	{
		printf("%d ", i);
		for (j = 1; j < COL+1; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}

//设置雷
void Setmine(char mine[ROWS][COLS])
{
	srand((unsigned int)time(NULL));
	for (int i = 0; i < MINE; i++)
	{
		int x = rand() % COL + 1;
		int y = rand() % ROW + 1;
		if (mine[y][x] == '0')
		{
			mine[y][x] = '1';
		}
		else if (mine[y][x] == '1')	
		{
			i--;
		}
	}
}

//检查雷的数量
static int get_mine_num(char mine[ROWS][COLS], int x, int y)
{
	return mine[y - 1][x - 1] + mine[y - 1][x] + mine[y - 1][x + 1]
		+ mine[y][x - 1] + mine[y][x + 1]
		+ mine[y + 1][x - 1] + mine[y + 1][x] + mine[y + 1][x + 1] - 8*'0';
}

void ChectMine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
	char minenum = get_mine_num(mine, x, y) + '0';
	if (show[y][x] == '*')
	{
		//如果全0就继续递归
		if (minenum == '0')
		{
			show[y][x] = '0';
			ChectMine(mine, show, x-1, y-1);
			ChectMine(mine, show, x, y-1);
			ChectMine(mine, show, x+1, y-1);
			ChectMine(mine, show, x-1, y);
			ChectMine(mine, show, x+1, y);
			ChectMine(mine, show, x-1, y+1);
			ChectMine(mine, show, x, y+1);
			ChectMine(mine, show, x+1, y+1);

		}
		//如果有一个大于0,就将 * 变成 雷的数量
		if (minenum > '0')
		{
			show[y][x] = minenum;
		}
	}
}

//标记
void ShowMark(char show[ROWS][COLS], int x, int y, char set)
{
	if (set == '#')//标记
	{
		if (show[y][x] == '*')
		{
			show[y][x] = '#';
		}
		else
		{
			printf("此处无法标记\n");
		}
	}
	if (set == '*')//取消标记
	{
		if (show[y][x] == '#')
		{
			show[y][x] = '*';
		}
		else
		{
			printf("此处无法取消标记\n");
		}
	}
}

//判断是否获胜,获胜返回1 , 否返回 0
int IsWin(char show[ROWS][COLS])
{
	int i = 0;
	int j = 0;
	int count = 0;
	for (i = 1; i < ROW + 1; i++)
	{
		for (j = 1; j < COL + 1; j++)
		{
			if (show[i][j] == '*' || show[i][j] == '#')
			{
				count++;
			}
		}
	}
	if (count == MINE)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

int IsMine(char mine[ROWS][COLS], int x, int y)
{
	return mine[y][x] - '0';
}

🏆一定要看这里😘

既然你都看到这里了,也不打算给一个三连吗?😢

就算没有三连,给一个赞👍,留下你的声音,让我知道你好吗😭

下面就是作者的犯病了,未成年人请在大人的陪同下观看

我要三连,快给我三连,我受不了了,在没有三连,我就要…啊…不行了,要坏掉了…快…快给我三连…

  • 29
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 30
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泡泡牛奶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值