C语言应用——《扫雷》初阶、进阶

游戏介绍

扫雷游戏(初阶)规则如下:

  • 游戏发生在一张9X9的网格之上,在其中任意十个位置上布上地雷,其位置对玩家不可见;
  • 玩家自由选择合适的坐标,表示自己要扫的区域;
  • 在玩家选择后,给出提示;若所扫位置有雷则游戏失败,向玩家展示地雷位置;否则在玩家所扫位置给出一个数字,该数字表示的是以其为中心的周边一圈,八个网格中存在的地雷数量;
  • 玩家扫除所有不含雷的位置则表示玩家胜利,至此游戏结束。游戏结束后显示地雷分布。

需求分析(初阶)

  1. 首先需要初始化一张9X9的棋盘,在此基础上实现游戏功能;
  2. 电脑能够随机布置地雷,地雷位置玩家不可见;
  3. 玩家选择位置,要能判断该处是否有地雷,游戏能否继续进行;
  4. 要能统计非地雷处周边位置的地雷数量,并在玩家扫过后展示给玩家,以供玩家继续游戏;
  5. 在玩家扫除所有不含地雷的位置后,要能及时结束游戏并给出胜利提示;
  6. 玩家可以选择退出、或者继续游戏;

关于游戏设计的说明

1.在设计中我们将用 ‘1’ 表示某处有雷;用 ‘0’ 表示某处无雷
2. 考虑到游戏功能的需求,如:我们需要对用户隐藏地雷位置,但是在用户扫过一块区域并且没有碰雷的条件下,我们又要将该处附近的地雷数量展现给用户。在此过程中可能会出现问题,我们在说明1中声明 ‘1’ 表示此处有雷,如果用户扫到一块无雷区域,再次周围有一颗雷,那么我们要将该处置 ‘1’ ,这个时候会发生矛盾,此区域会被人为定义为雷区,引发程序错误。所以我们设计用两张9X9的棋盘完成这样的需求,一张表示地雷分布,一张用来向用户展示
3. 实现过程中我们又发现,会有很多网格处在边缘区域,如果我们将他们单独处理,不仅很不方便也会浪费我们大量的时间。于是我们想到可以扩大棋盘11X11 ,但是只向用户展示中间 9X9 的部分,便可以解决这个问题。这样即使玩家扫到边缘区域,我们也可以保证该处周围存在八个网格,便于我们统计。

游戏实现(初阶)

(1)game.h

此文件包含的是,对在此项目所要用到的函数的声明

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//表示向用户展示的列、行数
#define ROW 9
#define COL 9
//表示设计中实际需要的行、列数
#define ROWS ROW+2
#define COLS COL+2
//NUM表示地雷数,MAX表示棋盘规模
#define NUM 80
#define MAX 81

//打印菜单
void menu();

//初始化棋盘
void init(char arr[ROWS][COLS], int rows, int cols,char ret);

//打印棋盘
void print(char arr[ROWS][COLS], int row, int col);

//随机放置地雷
void put_bone(char board[ROWS][COLS], int num,int row,int col);

//玩家扫雷
char player_do(char board[ROWS][COLS],char bone[ROWS][COLS],int x,int y);

//统计无雷区域周围地雷数
int count(char board[ROWS][COLS], int x, int y);

(2)game.c

此文件包含的是,游戏功能实现所需函数的定义

1、打印菜单

#include"game.h"
void menu()
{
	printf("***********************************\n");
	printf("*******      1、play       ********\n");
	printf("*******      0、exit       ********\n");
	printf("***********************************\n");
}

2、初始化网格

void init(char arr[ROWS][COLS], int rows, int cols,char ret)
{
	int i = 0, j = 0;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			arr[i][j] = ret; 
	//ret 接受的是字符,表示初始化样式,在后面我们用 ‘0’ 初始化地雷分布网格,用 ‘*’ 初始化展示网格
		}
	}
}

说明:我们初始化网格需要将整个网格全部初始化,ret 表示用何种字符来初始化。也就是说我们将用 ret 初始化 11X11 的网格。

3、打印网格

void print(char arr[ROWS][COLS], int row, int col)
//row,col 分别接收行、列
{
	int i = 0, j = 0;
	for (i = 0; i <= row; i++)
	{
		printf("%d ", i);
	}
	//为了方便玩家定位,将列号先打印好
	printf("\n");
	for (i = 1; i <= row; i++)
	//下标从1开始,到row结束
	{
		printf("%d ", i);
		//同样是为了方便定位,将行号打印
		for (j = 1; j <= col; j++)
		//下标从1开始,到col结束
		{
			printf("%c ", arr[i][j]);
			//打印网格中的元素
		}
		printf("\n");
	}
}

说明:传几行几列就打印几行几列,我们希望向用户展示 9X9 的网格,即只打印中间部分,所以我们设置下标从 1 开始,这样可以正好将我们需要的部分打印出来。

4、布置地雷

void put_bone(char board[ROWS][COLS], int num,int row,int col)
{
	while (num>0)
	//我们事先设置好地雷数量,直到地雷全部布置完毕,退出循环
	{
		int i = rand() % row + 1;
		int j = rand() % col + 1;
		//与srand使用,生成随机地址
		if (board[i][j] != '1')
		//判断该处是否已经被布置过地雷了,没有则向下执行
		{
			board[i][j] = '1';
			//布置地雷
			num--;		
			//布置完成后,自然地雷数减一	
		}
	}
}

说明:布置地雷过程,应该只发生在布置地雷网格,我们可以在传参时进行控制。循环控制将所有地雷都布置完毕,条件控制避免发生某处重复布置地雷。

5、周边地雷数统计

int count(char board[ROWS][COLS], int x, int y)
{
	return board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] + board[x][y - 1] + board[x][y + 1] + board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] - 8 * '0';
	//返回周边地雷数量,返回一个整形值
}

6、玩家扫雷

char player_do(char board[ROWS][COLS], char bone[ROWS][COLS], int x, int y)
{
	if (board[x][y] == '1')
	//判断用户是否碰雷,是的话返回 ‘g'表示游戏结束 给出提示
	{
		printf("很遗憾,你被炸死了!\n");
		return 'g';
	}
	//否则在用户展示网格中展示周围地雷数,返回’c'表示游戏 继续进行
	else
	{
		bone[x][y] = '0' + count(board, x, y);
		return 'c';
	}
}

说明:网格中只能存放字符类型的值,为了表示周围地雷数,我们先求出数量,再加上 ’0‘ 即可得到这个数字的字符表示。例如 : 8+’0‘ -> ‘8’

(3)main.c

#include "game.h"
//游戏主题逻辑
void game()
{
	srand((unsigned int)time(NULL));
	//定义两个网格,board表示地雷分布网格,bone表示用户展示网格
	char board[ROWS][COLS];
	char bone[ROWS][COLS];

	//用'0'初始化11X11的地雷分布网格,'*'初始化11X11的用户展示网格
	init(board, ROWS, COLS,'0');
	init(bone, ROWS, COLS, '*');
	
	//在向玩家展示的9X9的网格中布置地雷
	put_bone(board,NUM,ROW,COL);
	
	//打印用户展示网格,初始全为'*'
	print(bone, ROW, COL);
	
	//MAX-NUM表示用户获得胜利需要扫雷的 次数
	int i = MAX-NUM;
	//while循环可以控制在用户没有踩雷的条件下,让用户一直游戏,直到踩雷或者获胜
	while (i > 0)
	{
		int x = 0, y = 0;
		printf("请选择要扫描的位置->");
		scanf("%d %d", &x, &y);
		//考虑玩家可能重复扫同一个网格,用if语句控制
		if (bone[x][y] != '*')
		{
			printf("该位置已经被扫过啦!请重试!\n");
			continue;
			//continue,防止用户重复扫描记作一次有效扫雷
		}
		char r=player_do(board, bone, x, y);
		if (r =='g')
		{
		//'g' 表示踩雷了,那么直接退出循环,结束本次游戏
			print(board, ROW, COL);
			break;
		}
		else
		{
		//'c' 表示游戏继续,玩家可以进行下一次扫雷
			print(bone, ROW, COL);
			i--;
		}
	}

	if (i == 0)
	{
		printf("恭喜,扫雷成功!\n");
		print(board, ROW, COL);
	}

}
//开始游戏
void game_play()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择-> ");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();//找到game函数,进入游戏主体部分。
			break;
		case 0:
			printf("退出成功!");
			break;
		default:
			printf("非法输入!请重新输入-> ");
			break;
		}
	} while (input);
	//do while 语句控制玩家可以选择退出、继续游戏
}

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

说明:初始化的时候,我们要将 11X11 的网格全部初始化才方便我们之后的操作。而我们只需要将 9X9 的网格展示给玩家,这就需要我们传参时注意。同样布置地雷也应该发生在 9X9 的网格中。另外,我们扫雷过程中要防止玩家重复输入同一位置,即无效输入。处理方式见上方代码。关于游戏结束标志的判断,我们采用计数的方式,用户找到所有不含雷的位置即为胜利。

运行结果

(为了结果方便展示,我们布置80颗雷,这样我们只需要扫一次就可以出结果。并且在布置地雷后将地雷分布网格,也用print函数打印出来。这样我们可以精准得到地雷位置)
具体实现只要将#define NUM 值设置为80

在这里插入图片描述

需求分析(进阶)

  • 在原有基础上进行游戏,我们发现一局游戏可能会花费玩家大量时间,并且存在没有办法获胜的可能。我们做出改善,节省玩家时间,同时也增加游戏获胜的可能性。我们作出改动如下:如果用户扫的区域无雷,并且周围无雷,则自动向外展开,直到出现周围一圈有雷为止,自动将周围地雷数量填在周围有雷的网格上。这样可以大大增快游戏进度。

需求实现(进阶)

  • 说明:根据需求,我们可以知道,如若玩家翻开周围无雷的区域,我们要将周围区域一同展开。首先从周围八格开始,继而研究这八个网格周围是否有雷,若没有再以此格为中心向外扩展,直到外缘被数字包裹。
    很容易想到用递归去处理这样的问题,我们选中一格后只要递推周围八格即可,直到出现周围有地雷的情况。但是我们很开就会发现,如果递归仅仅只是这样很容易出现死递归的情况。这就需要我们给出限制条件来解决死递归的问题。

具体实现(进阶)

  • player_do函数改动
char player_do(char board[ROWS][COLS], char bone[ROWS][COLS], int x, int y)
{
	if (board[x][y] == '1')
	{
		printf("很遗憾,你被炸死了!\n");
		return 'g';
	}
	else
	{
		//如果board[x][y]!='1'表示该处没有炸弹,下面调用unflod递归函数
		unfold(board,bone, x, y, '0' + count(board, x, y));
		return 'c';
	}
}

说明:这里要注意我们传的参数,我们将 ‘0’+count(board, x, y) 传给 char r,目的是在进入unflod函数后进行判断。r==‘0’ 说明周围没有地雷,则进行递推步骤,否则将地雷数量展示。

  • 递归函数unflod
void unfold(char board[ROWS][COLS],char bone[ROWS][COLS],int x,int y,char r)
{ 
	if (r == '0')
	//r表示附近地雷数,只有附近无雷才可以进行递推
	{
		if (bone[x][y] != ' ')
		//判断条件,避免死递归,如果bone[x][y]==' ' 表示该处被处理过了,如果继续递推就会发生死递归。
		{
			bone[x][y] = ' ';
			//满足继续递推条件后先将该处赋空,表示已经被处理过,下次遇到后不再处理这个网格
			unfold(board, bone, x - 1, y - 1, '0' + count(board, x - 1, y - 1));
			unfold(board, bone, x - 1, y , '0' + count(board, x - 1, y ));
			unfold(board, bone, x - 1, y + 1, '0' + count(board, x - 1, y + 1));
			unfold(board, bone, x , y - 1, '0' + count(board, x , y - 1));
			unfold(board, bone, x , y + 1, '0' + count(board, x , y + 1));
			unfold(board, bone, x + 1, y - 1, '0' + count(board, x + 1, y - 1));
			unfold(board, bone, x + 1, y , '0' + count(board, x + 1, y ));
			unfold(board, bone, x + 1, y + 1, '0' + count(board, x + 1, y + 1));
		}
	}
	else
	{
		bone[x][y] = '0' + count(board, x, y);
	}
}

实现效果

在这里插入图片描述
完整版代码请移步:GitHub

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值