扫雷游戏的具体实现!

前置

函数的声明与定义

单个文件

我们初学者平常写代码的时候需要一个(.c)文件就够用了

#include <stido.h>
//判断⼀年是不是闰年
int is_leap_year(int y)
{
 if(((y%4==0)&&(y%100!=0)) || (y%400==0))
 return 1;
 else
 return 0;
}
int main()
{
 int y = 0;
 scanf("%d", &y);
 int r = is_leap_year(y);
 if(r == 1)
 printf("闰年\n");
 else
 printf("⾮闰年\n");
 return 0;
}

比如判断是否为闰年的情况,函数的定义在函数调⽤之前,确实没啥问题,可以运行。但是一旦将判断闰年函数放在主函数的后面,那么编译器会报警告

为什么?我们可以这样理解,程序的主函数是入口,如果主函数前面没有一个“说明”,当我们在主函数里突然发现这个函数,编译器不认识,太突然了!如果在主函数前面加一个说明(声明),那么相当于提前给编译器打好招呼,熟悉之后就方便办事,因此,上述判断闰年函数在声明后就可以使用!

多个文件

多个文件。多了什么,为什么要多?

⼀般情况下,函数的声明、类型的声明放在头⽂件(.h)中,函数的实现是放在源⽂件(.c)⽂件中。

game.h

主要放头文件,函数的声明,以及一些宏定义。

game.c

game.c与game.h这两个文件是配套的!game.c里面放的是函数的核心内容

test.c

里面放主函数,在主函数中引用我们所写的函数。

这样一个判断闰年的功能就很轻松的写完了。

注意一个细节,由于我在game.h里包含了stdio.h这个头文件,所以我在test.c里直接用game.h就能顺带使用stdio.h这个头文件里的函数。

扫雷游戏game.h

#pragma once

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

/*实际游戏要用的数组范围*/
#define ROW 9//行
#define COL 9//列

/*为了显示边界位置周围雷的个数而扩大的数组*/
#define ROWS 11//行
#define COLS 11//列
#define MineCount 10

/*初始化棋盘*/
void setmine(char board[ROWS][COLS], int rows, int lows, char set);

/*布置棋盘*/
void SetMine(char board[ROWS][COLS], int row, int low);

/*打印棋盘*/
void PrintBoard(char board[ROWS][COLS], int row, int low);

/*排查雷*/
void InvestigateMine(char show[ROWS][COLS], char mine[ROWS][COLS], int cow, int low);

扫雷游戏test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "game.h"

void menu()
{
	printf(" 请输入数据 》:\n");
	printf("**************\n");
	printf("****1.play****\n");
	printf("****0.play****\n");
	printf("**************\n");
}
void game()
{
	char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };

	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');

	/*PrintBoard(mine, ROW, LOW);*/
	PrintBoard(show, ROW, COL);

	SetMine(mine, ROW, COL);

	InvestigateMine(show, mine, ROW, COL);

}

int main()
{
	int input = 0;
	srand((unsigned)time(NULL));
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			break;
		default:
			printf(" 请重新输入数据!");
		}
	} while (input);
	return 0;
}

扫雷游戏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;
	for (i = 0; i < rows; i++)
	{
		int j = 0;
		for (j = 0; j < cols; j++)
		{
			board[i][j] = set;
		}
	}
}

/*布置棋盘*/
void SetMine(char board[ROWS][COLS], int row, int col)
{
	int count = MineCount;
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		for (j = 0; j < col; j++)
		{
			if (board[x][y] == '0')
			{
				board[x][y] = '1';
			}
			count--;
		}
		if (count == 0)
		{
			break;
		}
	}
}

/*打印棋盘*/
void PrintBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("--------扫雷游戏-------\n");
	for (i = 0; i <= col; i++)
	{
		printf("%-2d", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%-2d", i);
		int j = 0;
		for (j = 1; j <= col; j++)
		{
			printf("%-2c", board[i][j]);
		}
		printf("\n");
	}
}

int CountMine(char mine[ROWS][COLS], int x, int y)
{
	int a = mine[x - 1][y] + mine[x][y - 1] + mine[x - 1][y - 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] + mine[x + 1][y - 1] + mine[x][y + 1] + mine[x + 1][y] - 8 * '0';
	return a;
}

/*排查雷*/
void InvestigateMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
	int win = 0;
	int x = 0;
	int y = 0;
	while (win < row * col - MineCount)
	{
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("踩到雷了!");
				PrintBoard(mine, ROW, COL);
				break;
			}
			else
			{
				int count = CountMine(mine, x, y);
				show[x][y] = count + '0';
				PrintBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("请重新输入坐标!\n");
		}
		if (win == row * col - MineCount)
		{
			printf("恭喜,排雷成功!\n");
			PrintBoard(show, ROW, COL);
		}
	}
}

多个文件的目的,是为了提升代码设计的效率,所以我们以后会经常涉及这个知识。

扫雷游戏分析和设计

• 使⽤控制台实现经典的扫雷游戏

• 游戏可以通过菜单实现继续玩或者退出游戏

• 扫雷的棋盘是9*9的格⼦ • 默认随机布置10个雷

• 可以排查雷 ◦ 如果位置不是雷,就显⽰周围有⼏个雷 ◦ 如果位置是雷,就炸死游戏结束 ◦ 把除10个雷之外的所有⾮雷都找出来,排雷成功,游戏结束。

这是程序设计的要求!!

菜单打印

要求

1.玩家可以通过选择1进入游戏,0退出游戏。

2.选错的话提醒玩家,重新选择。

我们的要求是不管程序是否顺利执行都要显示菜单,所以我们很自然的想到用do~while循环

void menu()
{
	printf(" 请输入数据 》:\n");
	printf("**************\n");
	printf("****1.play****\n");
	printf("****0.play****\n");
	printf("**************\n");
}

int main()
{
	int input = 0;
	srand((unsigned)time(NULL));
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			break;
		default:
			printf(" 请重新输入数据!");
		}
	} while (input);
	return 0;
}

解释

将写好的menu函数放到主函数里的do~while循环里就好。注意:这里的scanf函数必须要放到menu函数后面(因为编译器读到scanf时就会停下,等待用户输入),因此就不会先打印菜单了!

在主函数里放入switch~case语句,输入1进入游戏,输入0退出游戏(在退出switch语句的同时退出循环语句 ,这一点设计思路很重要但不难想)

初始化棋盘

要求

1. mine棋盘初始化为全‘0’。

2. show棋盘初始化为全‘1’。

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

解释

这个代码就是通过两个for循环讲棋盘规模进行搭建,之后给set不同的值,来实现初始化

我们都明白,开始要有一个专门的展示棋盘,输入数据后,我们要知道这个地方是不是雷,以及这个输入数据的位置周围有几个雷。如果只用一个棋盘,是不是不可能实现?

因此我们想到用两个数组。

一个是show数组

我们用 '*' 来当作展示给玩家的棋盘,玩家输入对应的坐标后便能显示出所需信息。

另一个是mine数组

这个数组内部随机生成10个雷,其全部内容只有在玩家排雷不幸遇到雷,并退出游戏时,才会展现给大家。

看到game.h时,本来我们需要9*9的棋盘,但是为什么我们还宏定义一个ROWS和COLS?

这是因为如果我们排雷时,输入棋盘边上的坐标时,会导致一部分位置出界,如果我们在额外在上下各开辟一行,左右各开辟一列,那么就变成11*11,将11*11-9*9的部分都初始化成字符0,就不会影响9*9的棋盘某个坐标周围雷的个数的信息。

最后的效果

有朋友很好奇这个随机数是怎么实现的,首先我们是吧mine数组里先全初始化为字符0,然后在通过之后的布置雷函数来实现,下面会详细说明。 

布置棋盘

void SetMine(char board[ROWS][COLS], int row, int col)
{
	int count = MineCount;
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		for (j = 0; j < col; j++)
		{
			if (board[x][y] == '0')
			{
				board[x][y] = '1';
			}
			count--;
		}
		if (count == 0)
		{
			break;
		}
	}
}

解释

MineCount是我们宏定义雷的个数,可以后期方便修改。

因为是棋盘内随机值,每一个数组内的元素都有可能是雷,所以我们定义x和y,将随机值的结果给x和y,每一次成功排雷,就减一个count,当10个雷布置完后 结束布置雷函数,从而实现布置雷。

if语句里的意思是要不同的10个雷位置,万一有两次随机数相等的情况,就会少布置一个雷。

随机数的实现知识

rand

C语言提供了一个函数叫rand,这函数是可以生成随机数的,函数原型如下:


 int rand (void);

rand函数会返回一个伪随机数,这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的大小是依赖编译器上实现的但是大部分编译器上是32767。 rand函数的使用需要包含⼀个头文件是:stdlib.h

正因为这种伪随机数是依据算法来实现的,因此我们需要真正的随机数。

怎么做?

rand函数是对⼀个叫“种子”的基准值进行运算生成的随机数,rand函数生成随机数的默认种子是1。如果要生成不同的随机数,就要让种子是变化的。

srand

C语言中又提供了一个函数叫srand,用来初始化随机数的生成器的,srand的原型如下

void srand (unsigned int seed);

程序中在调用rand函数之前先调用srand函数,通过srand函数的参数seed来设置rand函数生成随机数的时候的种子,只要种子在变化,每次生成的随机数序列也就变化起来了。 那也就是说给srand的种子是如果是随机的,rand就能生成随机数;在生成随机数的时候又需要⼀个随机数,这就矛盾了。

所以这个时候我们要借助一个time函数。

time

在程序中我们一般是使用程序运行的时间作为种子的,因为时间时刻在发生变化的。 在C语言中有一个函数叫time,就可以获得这个时间,time函数原型如下:

time_t time (time_t* timer);

time函数会返回当前的日历时间,其实返回的是1970年1月1日0时0分0秒到现在程序运行时间之间的差值,单位是秒。返回的类型是time_t类型的,time_t类型本质上其实就是32位或者64位的整型类型。 time函数的参数timer如果是非NULL的指针的话,函数也会将这个返回的差值放在timer指向的内存 中带回去。 如果timer是NULL,就只返回这个时间的差值。time函数返回的这个时间差也被叫做:时间戳。 time函数的时候需要包含头文件:time.h

如果只是让time函数返回时间戳,我们就可以这样写:

 time(NULL);//调⽤time函数返回时间戳

这个也是比较简单的,我们了解到这里就好

所以这三个头文件在随机数中经常用到

打印棋盘

void PrintBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("--------扫雷游戏-------\n");
	for (i = 0; i <= col; i++)
	{
		printf("%-2d", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%-2d", i);
		int j = 0;
		for (j = 1; j <= col; j++)
		{
			printf("%-2c", board[i][j]);
		}
		printf("\n");
	}
}

这个没什么好说的,可能有时候不能一次性打印出自己想要的结果,可以边打印边修改。

强调一点,我们初始化用的是字符,因此我们要用%c来打印棋盘。

这个是我想要的打印效果,先把横纵坐标打印出来,再打印棋盘。

排查雷

要求

1.保证玩家输入的值合法在9*9范围内,包括边界。

2.标记时,统计正确标记雷的个数。

3.排雷时,玩家输入要排除的坐标,如果是雷,炸死,则游戏结束;如果不是雷,则统计周围雷的个数,如果排了71(row*col-MineCount)次还没炸,那么游戏结束。

int CountMine(char mine[ROWS][COLS], int x, int y)
{
	int a = mine[x - 1][y] + mine[x][y - 1] + mine[x - 1][y - 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] + mine[x + 1][y - 1] + mine[x][y + 1] + mine[x + 1][y] - 8 * '0';
	return a;
}

void InvestigateMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
	int win = 0;
	int x = 0;
	int y = 0;
	while (win < row * col - MineCount)
	{
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("踩到雷了!");
				PrintBoard(mine, ROW, COL);
				break;
			}
			else
			{
				int count = CountMine(mine, x, y);
				show[x][y] = count + '0';
				PrintBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("请重新输入坐标!\n");
		}
		if (win == row * col - MineCount)
		{
			printf("恭喜,排雷成功!\n");
			PrintBoard(show, ROW, COL);
		}
	}
}

解释

while循环里的条件:先定义一个win变量,表示成功排雷的次数,只要小于71且没踩到雷就可以继续排雷。

第一个if就是保证坐标合法性在9*9之间,如果是非法坐标则提示重新输入。

重新输入坐标后,此时仍然满足循环条件不变,所以还可以进入第一个if里嵌套程序,如果踩雷,直接炸死,退出游戏;如果不是雷,则win加一次统计次数,并打印出这个位置周围雷的情况。只要在循环条件内可以一直排下去!!!

如果71一次仍然安全说明剩余位置全是雷,直接游戏胜利,退出游戏!

重点

这个计算周围信息的特别重要

第一种方法
int CountMine(char mine[ROWS][COLS], int x, int y)
{
	int a = mine[x - 1][y] + mine[x][y - 1] + mine[x - 1][y - 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] + mine[x + 1][y - 1] + mine[x][y + 1] + mine[x + 1][y] - 8 * '0';
	return a;
}

定义一个CountMine函数。不妨设输入排查的坐标为 x y ,将其周围一圈的八个数都加起来再减去8个字符0,就是对应的数字,又因为x y处不可能是雷,如果是雷就已经结束游戏了。

第二种方法
int CountMine(char mine[ROWS][COLS], int x, int y)
{
	int i = 0;
	int con = 0;
	for (i = x - 1; i <= x + 1; i++)
	{
		int j = 0;
		for (j = y - 1; j <= y + 1; j++)
		{
			con += (mine[i][j] - '0');
		}
	}
	return con;
}

利用for循环遍历x y周围包括x y处的9个数,然后每一次这个被遍历的数都减去字符0,然后将九次的结果相加,就是周围雷的个数。

结语

总之这个扫雷游戏不算很简单但是只要好好理解,还是可以掌握的!

                                               

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值