扫雷游戏:函数和数组的综合运用

今天我们来讲讲C语言初阶较为重要的一个小实验:扫雷游戏。这个游戏运用了很多的函数以及循环分支语句,因此对于新手训练C语言是非常友好的。下面我们就一起来看看吧!

我们先看看程序构成。我们所要写出来的程序一定要根据功能来写,因此,我们按照如下思维导图的递进一点点的完成这段代码。 

 首先最重要的就是程序主体,我们再写程序的时候一定不能上来就写我们需要的函数,而是在程序主体的完善过程中一点点理解我们需要什么函数,需要什么功能,再去完成函数。为了方便写主程序以及函数,我们同时建立test.c、game.c和game.h三个文件。game.c用来编写我们的函数代码,game.h用来声明函数,而主程序test.c包含game.h程序即可使用我们自己定义的函数库。

为了实现这个游戏,我们需要两个字符数组。一个用来存储我们的雷(mine),另外一个展示给游玩者(show)。因此我们需要将show数组全部统一初始化,在游玩者游玩时,根据mine数组来确定周围雷的个数,再替换掉show数组相应的元素即可是先扫雷过程。

介于游戏规则,我们会在游戏时扫描坐标周围九宫格内雷的个数,但是当坐标位于上下左右边界时,没有完整的九宫格会产生越界,因此我们可以将大小换到11*11,但是有雷的地方仅仅限制于9*9的范围内。

话不多说,我们开始:

一、test.c文件

(一)程序主体设计

作为一个游戏,菜单当然是必不可少的,因此这必然会出现在我们的主程序中。对于C语言初学者来说,想要实现鼠标控制并不容易,因此我们使用键盘输入,也就是scanf函数来输入菜单选项。因此我们可以把play选项设置为1,exit选项设置为0。对于我们不同的输入,我们要给出不同的判断,因此我们在菜单后使用分支语句来判断。由于我们所需输入的只有数字,因此我们可以使用较为简单的switch语句,并且在其中的play分支后要运行我们的游戏程序。因此,一个大框架出来了,如下:

int main()
{	int play_or_not = 0;
	do
	{
		menu();//这里是菜单函数
		printf("请选择");
		scanf("%d", &play_or_not);
		switch (play_or_not)
		{
		case 1:
			printf("--------扫雷--------\n");
			play();//这里是游戏函数
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (play_or_not);
    return 0;
}

这段一定是写在test.c文件中作为我们的主程序的,我们的主程序中存在play()和menu()两个函数,那么这两个函数我们也放在主程序中,其他一律写在我们自己的函数库,即game.c文件中。

 (二)menu函数

菜单界面我们只需要打印文字即可,因为在我们的主程序中已经有了scanf函数,这里边不需要输入了,只需要打印说明就行,因此我们直接这样写:

void menu()//这里我们只需要打印,不需要返回值,所以void
{
	printf("******************\n");
	printf("******1.play******\n");
	printf("******2.exit******\n");
	printf("******************\n");
}

这样我们的菜单就做好了,当然仅供参考,友友们有更好看的图案也可以加上去!

(三)play函数

play函数就是我们的游戏函数,是最重要的函数,当然了,我们不可能一口气把这个函数里面所有的函数写完,例如数组,初始化,打印,扫雷,再打印,我们一定会分多个模块完成,因此我们可以先给出play函数的主体:

void play()
{
	char show[][] = { 0 };
	char mine[][] = { 0 };
	chushihua();
	chushihua();
	buzhi();
    dayin();
	saolei();
	//dayin();
}

为了大家看得懂,我直接把函数命名为了汉语拼音。mine数组是我们的雷盘数组,是不需要展示给游戏者看的,但是大家可以加在后面看看雷盘是否设置成功了,使用后再注释掉即可。上图中左右的黄色字体均是我们自己编写的函数,分别是:

  • chushihua:初始化数组,将棋盘中的字符统一
  • buzhi:即布置棋盘,把随机的雷埋入我们的数组中,原理就是替换数组中的数字
  • dayin:即打印,打印出我们的雷盘,让游玩者选择
  • saolei:即扫雷,是play函数中的最重要的主体函数

下面我们就可以分段设计我们所需要的函数了。


二、game.c文件

game.c文件作为我们的函数库的定义文件,我们需要讲上文的四个函数写入其中。

(一)一些说明

  • 在我们的程序中,我们会用到一些数,例如二维数组的行、列,以及雷的个数,如果我们将其写入程序中,当我们需要修改数值时便需要从头到尾修改,非常的不方便。因此我们把我们需要的熟悉定义好,作为全局变量代入程序中,因此我们可以将我们的变量放在game.h文件中。我们定义ROW和COL作为棋盘的大小,ROWS和COLS作为数组的列和行,具体原因上问题到过,这里就不再赘述。同时,雷的个数也是可变量,我们设为leidegeshu,也放入game.h文件中。
  • 为什么不用整型数组而用字符数组,是因为我们所需展现出来的棋盘数字形式不易游玩,换成一些符号更容易些。

(二)chushihua函数

要想把我们的数组初始化,在现阶段我们只能用for循环来解决,因此show数组可以这样初始化:

void chushihua(char arr[ROWS][COLS])
{
	for (int i = 0;i < ROWS;i++)
	{
		for (int j = 0;j < COLS;j++)
		{
			arr[i][j] = '*';
		}
	}
}

但是问题来了,我们需要把mine数组初始化成字符0、1,而这个函数只能初始化成“*”,那我们为什么不把初始化的形式变为变量呢?如下:

void chushihua(char arr[ROWS][COLS],char set)
{
	for (int i = 0;i < ROWS;i++)
	{
		for (int j = 0;j < COLS;j++)
		{
			arr[i][j] = set;
		}
	}
}

这样两个数组我们都可以初始化,这时候我们的play函数已经完成了一部分了:

void play()
{
	char show[ROWS][COLS] = { 0 };
	char mine[ROWS][COLS] = { 0 };
	chushihua(show, '*');
	chushihua(mine, '0');
	buzhi();
    dayin();
	saolei();
}

(三)buzhi函数

既然是随机的布置雷,我们肯定要用到随机数,而随机数的产生最简单的就是利用时间指针。因此我们可以以时间为种子产生随机数,每布置一个雷,计数就减少一个,因此我们可以用do-while循环。如下:

void buzhi(char arr[ROWS][COLS],int row, int col)
{
	srand((unsigned int)time(NULL));
	int count = leidegeshu;
	do
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (arr[x][y] = '0')
		{
			arr[x][y] = '1';
			count--;
		}
	} while (count);
}

(四)dayin函数

这一步的目的是将棋盘打印在屏幕上,供游玩者读取。我们需要挨个打印,同时需要注意换行

这样我们就可以写出来以下代码:

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

但是再看看我们的运行结果,确实打印出来了但是没有行号和列号,这让游玩者寻找坐标很困难。因此我们可以加上行号和列号:

void dayin(char arr[ROWS][COLS],int row,int col)
{
	int i = 0;
	for (i = 0;i <= col;i++)
	{
		printf("%d ", i);//打印列
	}
	printf("\n");
	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");
	}
}

这样我们的打印函数就完成了。

(五)saolei函数

即扫雷函数。当已经输入的坐标数量小于不是雷的坐标数量时,就可以进入扫雷程序,因此我们需要一个if语句:

void saolei(char mine[ROWS][COLS], char show[ROWS][COLS])
{
	int x = 0;
	int y = 0;
	int z = 0;
	while (z < ROW * COL - leidegeshu)
	{
		printf("请输入坐标:\n");
		scanf("%d %d", &x, &y);
		if (x <= ROW && x > 0 && y <= COL && y > 0)
		{
			if (mine[x][y] == '1')
			{
				printf("很抱歉你被炸死了\n");
				dayin(mine, ROW, COL);
				break;
			}
			else
			{
				int count = geshu(mine, x, y);
				show[x][y] = count + '0';
				dayin(show, ROW, COL);
				z++;
			}
		}
		else
		{
			printf("非法的坐标,请重新输入\n");
		}
	}
}

那如果真的出现了一个很厉害的人,把所有的雷都排除掉了,这个时候我们就需要另外一种可能了:

void saolei(char mine[ROWS][COLS], char show[ROWS][COLS])
{
	int x = 0;
	int y = 0;
	int z = 0;
	while (z < ROW * COL - leidegeshu)
	{
		printf("请输入坐标:\n");
		scanf("%d %d", &x, &y);
		if (x <= ROW && x > 0 && y <= COL && y > 0)
		{
			if (mine[x][y] == '1')
			{
				printf("很抱歉你被炸死了\n");
				dayin(mine, ROW, COL);
				break;
			}
			else
			{
				int count = geshu(mine, x, y);
				show[x][y] = count + '0';
				dayin(show, ROW, COL);
				z++;
			}
		}
		else
		{
			printf("非法的坐标,请重新输入\n");
		}
	}
	if (z == ROW * COL - leidegeshu)
	{
		printf("恭喜你,排雷成功\n");
		dayin(mine, ROW, COL);
	}

这样我们可以提醒他排雷成功了,那如果玩家玩到一半想退出怎么办,那这里我们可以给出另一个分支:

void saolei(char mine[ROWS][COLS], char show[ROWS][COLS])
{
	int x = 0;
	int y = 0;
	int z = 0;
	while (z < ROW * COL - leidegeshu)
	{
		printf("请输入坐标:\n");
		scanf("%d %d", &x, &y);
		if (x <= ROW && x > 0 && y <= COL && y > 0)
		{
			if (mine[x][y] == '1')
			{
				printf("很抱歉你被炸死了\n");
				dayin(mine, ROW, COL);
				break;
			}
			else
			{
				int count = geshu(mine, x, y);//统计雷的个数函数
				show[x][y] = count + '0';
				dayin(show, ROW, COL);
				printf("输入00可提前结束哦!\n");
				z++;
			}
		}
		else if (x == 0 && y == 0)
		{
			printf("游戏提前结束\n");
			break;
		}
		else
		{
			printf("非法的坐标,请重新输入\n");
		}
	}
	if (z == ROW * COL - leidegeshu)
	{
		printf("恭喜你,排雷成功\n");
		dayin(mine, ROW, COL);
	}

那么这就是我们saolei函数的完整代码了,是不是很简单呢?在这个函数中还有一个geshu函数,图中已标出,这个函数用来统计这个坐标一圈的九宫格里有多少雷,因此我们还需要写一个geshu函数。

(六)geshu函数

由于数组是字符数组,直接相加并不可取。但是注意到,字符1的ASCII码值是50,字符0的ASCII码值是49,二者想减正好是1。因此我们可以将九宫格内的每个字符都与字符0想减,所得的值再相加就是最终的雷的个数。如下:

int geshu(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');
}

当然,这里也可以用for循环,留给小伙伴们自己考虑,这里便不再赘述。至此,我们的代码已经基本上全部完成了。


三、game.h文件

我们在game.c中写的函数定义需要在game.h文件中声明才可以使用,加上上文所说的全局变量,我们的game.h文件:

#define _CRT_SECURE_NO_WARNINGS 1
#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 leidegeshu 10
void chushihua(char arr[ROWS][COLS], char set);
void dayin(char arr[ROWS][COLS], int row, int col);
void buzhi(char arr[ROWS][COLS], int row, int col);
int geshu(char arr[ROWS][COLS], int x, int y);
void saolei(char arr1[ROWS][COLS], char arr2[ROWS][COLS]);

同时,由于我们在game.h文件中包含了一些头文件,因此test.c文件中便不再需要包含该头文件。此外还要注意我们自己编写的函数库需要用双引号引用。

四、成文

(一)test.c文件

#include"game.h"
void play()
{
	char show[ROWS][COLS] = { 0 };
	char mine[ROWS][COLS] = { 0 };
	chushihua(show, '*');
	chushihua(mine, '0');
	buzhi(mine, ROW, COL);
    dayin(show, ROW, COL);
	saolei(mine,show);
}
void menu()
{
	printf("******************\n");
	printf("******1.play******\n");
	printf("******2.exit******\n");
	printf("******************\n");
}
int main()
{	int play_or_not = 0;
	do
	{
		menu();//这里是菜单函数
		printf("请选择");
		scanf("%d", &play_or_not);
		switch (play_or_not)
		{
		case 1:
			printf("--------扫雷--------\n");
			play();//这里是游戏函数
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (play_or_not);
    return 0;
}

(二)game.c文件

#include"game.h"
//数组初始化
void chushihua(char arr[ROWS][COLS],char set)
{
	for (int i = 0;i < ROWS;i++)
	{
		for (int j = 0;j < COLS;j++)
		{
			arr[i][j] = set;
		}
	}
}

//打印棋盘
void dayin(char arr[ROWS][COLS],int row,int col)
{
	int i = 0;
	for (i = 0;i <= col;i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	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 buzhi(char arr[ROWS][COLS],int row, int col)
{
	srand((unsigned int)time(NULL));
	int count = leidegeshu;
	do
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (arr[x][y] = '0')
		{
			arr[x][y] = '1';
			count--;
		}
	} while (count);
}

//扫雷
void saolei(char mine[ROWS][COLS], char show[ROWS][COLS])
{
	int x = 0;
	int y = 0;
	int z = 0;
	while (z < ROW * COL - leidegeshu)
	{
		printf("请输入坐标:\n");
		scanf("%d %d", &x, &y);
		if (x <= ROW && x > 0 && y <= COL && y > 0)
		{
			if (mine[x][y] == '1')
			{
				printf("很抱歉你被炸死了\n");
				dayin(mine, ROW, COL);
				break;
			}
			else
			{
				int count = geshu(mine, x, y);
				show[x][y] = count + '0';
				dayin(show, ROW, COL);
				printf("输入00可提前结束哦!\n");
				z++;
			}
		}
		else if (x == 0 && y == 0)
		{
			printf("游戏提前结束\n");
			break;
		}
		else
		{
			printf("非法的坐标,请重新输入\n");
		}
	}
	if (z == ROW * COL - leidegeshu)
	{
		printf("恭喜你,排雷成功\n");
		dayin(mine, ROW, COL);
	}
}

//统计雷的个数
int geshu(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');
}

(三)game.h文件

#define _CRT_SECURE_NO_WARNINGS 1
#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 leidegeshu 10
void chushihua(char arr[ROWS][COLS], char set);
void dayin(char arr[ROWS][COLS], int row, int col);
void buzhi(char arr[ROWS][COLS], int row, int col);
int geshu(char arr[ROWS][COLS], int x, int y);
void saolei(char arr1[ROWS][COLS], char arr2[ROWS][COLS]);

好了,我们本期的分享到这里就结束了,友友们都学会了吗?没能理解也不要紧,大家加油!我们下期再见!

附上扫雷的网页版:扫雷游戏网页版 - Minesweeper

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值