[c语言]--实现扫雷小游戏

开门见山

 给出我在写这篇代码时的思维导图,这篇博客也是根据思维导图写的

[c语言]--实现扫雷小游戏

 

 

游戏结果展示

 //因为本人有点小菜,所以就设置4*4 -->4的规格,即16格子找出4个雷以演示游戏代码,想挑战高难度的小伙伴可自行设置.那么就见笑了

[c语言]--实现扫雷小游戏[c语言]--实现扫雷小游戏

 

游戏思路分析

先展示本次编写代码所用到的头文件和源文件展示,方便后续阅读

test.c 用于测试编写的扫雷代码相关功能即对game.c中的函数进行调用
game.h 用于对扫雷代码所涉及的主要函数及参数的声明
game.c 代码的主体,对game.h中声明的函数进行定义

和三子棋(见前篇)一样,应该有个菜单,你觉得怎么好看就怎么设置----test.c

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

本来menu()应该属于game.c但是由于menu()本来就很简单,就没必要和game.c中的大头"争宠"了

布置雷

先看一张图

[c语言]--实现扫雷小游戏

[c语言]--实现扫雷小游戏

 

 

两个问题1.雷该放到哪呢?2.雷该用什么表示呢?

答:1.雷应该放到一个字符数组,不妨设为boom[ROWS][COLS]

2.雷用'1'表示,非雷用'0'表示,注意这里的'1'和'0'是字符1和0. 为什么选择用'1'和'0'?文末总结部分会详细给出

ps:这里的ROWS和COLS对应上图的11行11列,即橙色框.有的小伙伴就会问了我的棋盘不是9*9的嘛,设置成11*11岂不是浪费空间!实则不然,这样设置就可以避免产生数组越界的问题.何出此言?

我们看上面的绿色框,它的作用是去排查(x,y)周围八个格子的字符1(雷)的个数,排查(8,8)当然不会有数组越界,那我要是去排查(1,1) (1,2)...(9,9)也就是最外围的那32个格子,会出现什么情况?绿色框会访问不属于红色框的内容,(不恰当的比喻:你去拿了不该属于你的东西)这当然是不行的.计算机会无情的告诉你error.所以呢,我们应该设置为11*11的,最外围那一圈的元素可以不用,但不能没有!我们只操作中间的9*9就行了.

 排查雷 

 

像上图的绿色框,排查了(8,8),周围有3个雷,排查出来了该怎么办呢?是不是该把这个"3"存起来呀.怎么存呢?当然也是用数组咯.

这时候就有小伙伴跃跃欲试了:int show[ROWS][COLS],创建了一个整形数组;

那我只能说:你这波啊,还在第一层.且往下看

我打算用char show[ROWS][COLS],一个字符类型的数组.为什么要这么设计?这里先卖个关子,在函数定义部分会给出原因.

ok,以上便是我们的思路分析了,真正的大头来了

 

思路程序化

第一步当然是编写我们的test.c了

//游戏测试模块

#define _CRT_SECURE_NO_WARNINGS 1

#include "game.h"

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

void game()
{
	//关于雷的信息存储
	//1. 创建一个数组来存放雷
	char boom[ROWS][COLS] = { 0 };
	//2. 创建一个数组来显示排查出的雷
	char show[ROWS][COLS] = { 0 };
	//初始化两个数组
	InitBoard(boom, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
	//DisplayBoard(boom, ROW, COL);
	DisplayBoard(show, ROW, COL);//
	//布置雷
	Setboom(boom, ROW, COL);
	//DisplayBoard(boom, ROW, COL);
	//扫雷
	Findboom(boom, show, ROW, COL);
}

void test()
{
	int choice = 0;
	srand((unsigned int)time(NULL));//时间戳

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &choice);
		switch (choice)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择!\n");
			break;
		}
	} while (choice);
}

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

 可以发现是不是很精简且简单

main()调用了test(),test()调用了menu()如果输入1,test()又调用了game(),game()又调用了一系列的函数.然后我们才能玩到扫雷游戏.

细心的小伙伴会发现里面有个  #include "game.h"      这其实是头文件名的一种形式,只不过这个是我们自己定义的罢了,不像 #include<stdio.h>  #include<math.h> 这类是vs自带的.你只需要记住:自己定义的头文件要引用的话要加   "  "

函数要"先声明后使用"

我们来看看game.h里面有什么

    1 	//函数声明模块
    2 	//一些数据的全局定义,方便后续修改
    3 	
    4 	
    5 	
    6 	#define ROW 9
    7 	#define COL 9
    8 	
    9 	#define ROWS ROW+2
   10 	#define COLS COL+2
   11 	//雷的数量
   12 	#define BOOM_COUNT 10
   13 	
   14 	#include <stdio.h>
   15 	#include <stdlib.h>
   16 	#include <time.h>
   17 	//函数返回类型   函数名   函数参数
   18 	void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
   19 	void DisplayBoard(char board[ROWS][COLS], int row, int col);
   20 	void Setboom(char board[ROWS][COLS], int row, int col);
   21 	void Findboom(char boom[ROWS][COLS], char show[ROWS][COLS], int row, int col);

18-21行这些函数是game()反复调用的,声明已经有了,还差定义,我们把他们定义在game.c里面

下面一一介绍

InitBoard()

将数组初始化为你想要的内容

InitBoard() 

返回类型 viod

参数  一个char类型数组,两个整形数,一个字符数

还记得上面卖的关子吗,我把show[][]也定义为char类型,现在来告诉你答案,我把show[][]定义为和boom[][]一样的char类型,就是为了,让InitBoard()一个函数操作两个数组,使代码更精简,避免了我要因为初始化show[][]去重新定义另一个函数

下面的DisplayBoard()也是同样的道理

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)

{

        int i = 0;

        int j = 0;

        for (i = 0; i < rows; i++)

        {

                for (j = 0; j < cols; j++)

                {

                        board[i][j] = set;

                }

        }

}

DisplayBoard()

            打印数组内容

void DisplayBoard(char board[ROWS][COLS], int row, int col)

{

        int i = 0;

        int j = 0;

        //打印列号

        for (i = 0; i <= col; i++)

        {

                printf("%d ", i);

        }

        printf("\n");

        for (i = 0; i <= col; i++)

        {

                printf("--");

        }

        printf("\n");

        //打印行号

        for (i = 1; i <= row; i++)

        {

                printf("%d", i);

                printf("|");//紧接着打印数组内容

                for (j = 1; j <= col; j++)

                {

                        printf("%c ", board[i][j]);

                }

                printf("\n");

        }

}

Setboom()

布置雷->把boom[][]里对应坐标换成字符1 '1'

随机设置雷的位置

void Setboom(char board[ROWS][COLS], int row, int col)

{

        int count = BOOM_COUNT;

        while (count)

        {

                int x = rand() % row + 1;//让x范围是1-9

                int y = rand() % col + 1;//让y范围是1-9

                if (board[x][y] == '0')

                {

                        board[x][y] = '1';

                        count--;

                }

        }

}

Findboom()

找出boom[x][y]周围的字符1 '1'   并把找到的'1'的个数存到show[x][y]

//去找我们想排查的那个坐标周围的8个格子中雷的数量                      

int get_boom_count(char boom[ROWS][COLS], int x, int y)

{

       

        return     boom[x - 1][y] +

                   boom[x - 1][y - 1] +

                   boom[x][y - 1] +

                   boom[x + 1][y - 1] +

                   boom[x + 1][y] +

                   boom[x + 1][y + 1] +

                   boom[x][y + 1] +

                   boom[x - 1][y + 1] - 8 * '0';

}

void Findboom(char boom[ROWS][COLS], char show[ROWS][COLS], int row, int col)//两个数组,相同的坐标,一个表示雷,一个表示坐标周围的雷的个数

{

        int x = 0;

        int y = 0;

        int rest_blank = 0;//用来表示剩余的不是雷的格子的个数

        

        while (rest_blank < row * col - BOOM_COUNT)

        {

                printf("请输入要排查的坐标:>");

                scanf("%d%d", &x, &y);

                //这个是时候应该先判断坐标合法性,即要排查的坐标应该在9*9的格子的范围

                if (x >= 1 && x <= row && y >= 1 && y <= col)

                {

                        //坐标合法

                        //1. 踩雷

                        if (boom[x][y] == '1')

                        {

                                printf("很遗憾,你被炸死了\n");

                                //起码要让玩家"死得明白"

                                DisplayBoard(boom, row, col);//展示一下雷的分布

                                break;

                        }

                        else //不是雷

                        {

                                //计算x,y坐标周围有几个雷

                                int count = get_boom_count(boom, x, y);

                                //把找到的雷的个数存到show数组里

                                show[x][y] = count + '0';//一个整数转换为对应的字符加上'0' 如 3+'0'=='3'

                                DisplayBoard(show, row, col);

                                //找到一个雷rest_blank就自增

                               rest_blank++;

                        }

                }

                else

                {

                        printf("输入坐标非法,请重新输入!\n");

                }

        }

        //判断一下是否等于所有非雷的格子的数量

        if (rest_blank == row * col - BOOM_COUNT)

        {

                printf("恭喜你,排雷成功\n");

                DisplayBoard(boom, row, col);

        }

}

最后来看一下game.c的内容其实就是上面几个函数的汇总

//函数定义模块
#define _CRT_SECURE_NO_WARNINGS 1

#include "game.h"

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			board[i][j] = set;
		}
	}
}

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	//打印列号
	for (i = 0; i <= col; i++)
	{
		printf("%d ", i);
	}
	printf("\n");

	for (i = 0; i <= col; i++)
	{
		printf("--");
	}
	printf("\n");
	//打印行号
	for (i = 1; i <= row; i++)
	{
		printf("%d", i);
		printf("|");//紧接着打印数组内容
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}
                                                                  
void Setboom(char board[ROWS][COLS], int row, int col)
{
	int count = BOOM_COUNT;
	while (count)
	{
		int x = rand() % row + 1;//让x范围是1-9
		int y = rand() % col + 1;//让y范围是1-9
		if (board[x][y] == '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

//去找我们想看的那个坐标周围的8个格子中雷的数量                       
int get_boom_count(char boom[ROWS][COLS], int x, int y)
{
	
	return boom[x - 1][y] +
		   boom[x - 1][y - 1] +
		   boom[x][y - 1] +
		   boom[x + 1][y - 1] +
		   boom[x + 1][y] +
	   	   boom[x + 1][y + 1] +
		   boom[x][y + 1] +
		   boom[x - 1][y + 1] - 8 * '0';
}
//
void Findboom(char boom[ROWS][COLS], char show[ROWS][COLS], int row, int col)//两个数组,相同的坐标,一个表示雷,一个表示坐标周围的雷的个数
{
	int x = 0;
	int y = 0;
	int rest_blank = 0;//用来表示剩余的不是雷的格子的个数
	
	while (rest_blank < row * col - BOOM_COUNT)
	{
		printf("请输入要排查的坐标:>");
		scanf("%d%d", &x, &y);
		//这个是时候应该先判断坐标合法性,即要排查的坐标应该在9*9的格子的范围
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//坐标合法
			//1. 踩雷
			if (boom[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				//起码要让玩家"死得明白"
				DisplayBoard(boom, row, col);//展示一下雷的分布
				break;
			}
			else //不是雷
			{
				//计算x,y坐标周围有几个雷
				int count = get_boom_count(boom, x, y);
				//把找到的雷的个数存到show数组里
				show[x][y] = count + '0';
				DisplayBoard(show, row, col);
				//找到一个雷rest_blank就自增
				rest_blank++;
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入!\n");
		}
	}
	//判断一下是否等于所有非雷的格子的数量
	if (rest_blank == row * col - BOOM_COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(boom, row, col);
	}
}

ok,到这大头部分就讲完了

总结注意事项

#define _CRT_SECURE_NO_WARNINGS 1

这其实是为了让编译器在编译scanf时不报错,因为一些编译器(比如我自用的VS2019)会认为scanf不安全,建议你用scanf_s.但是我不听它的建议,我就要用scanf,就在代码前面加上上面那句就行了

关于雷的设置问题

充分利用数字的属性, '1'表示雷, 2表示周围有2个雷 这是实实在在的2,

'2'可以作为数组元素被打印出来,让我们在视觉上认为周围有两个雷,这是字符'2',却有了数字的属性.

可能有点绕,哈哈哈,但应该理解的过来

关于数组类型设置的问题

其实一开始我也没想到把show[][]设置为char类型,我是在编写InitBoard()的时候,突然想到的.我去初始化两个数组,能不能用一个函数啊,但是一个函数只能初始化一个类型的数组,且初始化对象和初始化内容必须是同一种类型(我不想强制类型转换嗷)

初始化 char 类型的boom[][]为 '0' (开始的时候应该没有雷) 要一个函数

初始话 int 类型的show[][] 为 '*' (一开始展示给我们看的应该全部是*,要把雷的位置遮起来,就不让你看,哼) 又有点行不通

所以就干脆把show[][]也定义为char类型,这样一来,数组类型和初始化内容的类型不就统一了嘛.

而且DiaplayBoard()也可以不用为类型发愁

游戏中的不足之处

1.在数周围8个格子的时候,get_boom_count()可以用循环,我为什么不用呢,因为直观,小伙伴们更容易理解

2.在排查雷的时候,不能展开一片.感兴趣的小伙伴可以用递归

排查boom[x][y]周围8个格子--> 1.是雷,就计数存到show[x][y] 2.不是雷,排查它周围的八个格子

到外围的一圈都是雷为止(其实我也不太懂扫雷的游戏规则).为了避免一些格子的重复排查,可以定义一个数组专门来表示某个格子是否被排查过.数组初始化为0,只要排查过,对应坐标就赋值为1.

3.用递归求斐波那契额数列,体现了上述思想,可以看我写的另一篇博客(算是波小广告啦)https://blog.csdn.net/HandsomeDog_L/article/details/120256097

小tips

建议小伙伴在写这种类型的代码时能有一个清晰的思路,怎么才能办到呢-------思维导图

当然大佬除外

上一篇写的三子棋也是有思维导图的,忘发出来了,我把它放到三子棋评论区吧,感兴趣的小伙伴可以去看看

由于编者水平实在有限,有什么错误之处望指正,非常感谢您的反馈

回见啦!

上一篇:基于C语言的简单小游戏-(扫雷)


下一篇:球面墨卡托(Spherical Mercator)