扫雷游戏的程序设计

前言

扫雷游戏是一种益智类游戏,目标是通过揭示方块出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。

游戏规则

扫雷游戏规则如下:

(1).输入坐标位置是雷,游戏失败。(2).如果不是雷,则在该位置标记出周围一圈雷的个数,玩家继续输入,
(3).如果不是雷,而且周围一圈都没有雷(即显示数字0),那么,将显示周围一片周围雷数都为0的区域只到遇到非零。
(4).当排除所有不是雷的地方(即只剩下有雷的地方未显示)游戏胜利。

游戏结构的分析

扫雷过程中,布置的雷,排查的雷的信息都需要存储,所以我们需要一定的数据结构来存储这些信息。

这个时候我们会想到一个9*9的矩阵。(10颗雷)

我们将有雷的地方设置为‘1’,将没雷的设置为‘0’。

示例

0123456789

1

00100000

0

201000000

1

3000001000
4100000000
5000100010
6000000000
7010000000
8000010000
9000001000

假设我排查(7,3),那么在(7,3)周围一圈有雷的话就会被记下来,并且在(7,3)处显示有几个雷,打印到屏幕上。如果我们要排除(8,9)或者(9,6)时,我的访问就会越界,控制台就会乱码,为了解决这个问题,我们会将棋盘设计成[rows+2][cols+2]的数组。而多加的两行的目的是防止越界,所以不需要布置雷。

如图所示

继续分析,当我们排查到一个雷时,需要将雷的信息储存起来,然后打印,作为排雷的重要参考信息,那么这个雷的个数信息应该存放在哪呢?因为存放雷的信息和雷的个数信息会在打印上出现困难。

为了清晰的将棋盘上有雷、非雷的信息和排查出雷的个数在棋盘上展示出来,我们采用另外一个方案,我们专门给一个棋盘(对应一个数组mine)存放布置好的雷的信息,再给另外一个棋盘(对应另外一个数组show)排查出的雷的信息。这样问题就解决了,把雷布置的到mine数组,在mine数组中排查雷,排查出的数据存放在show数组中,并且打印首位数组的信息到后期排查使用。

对于show数组就是将所有的'1','0'换成'*'。

游戏的程序设计

一、创建项目

这里为了提高代码的组织性、可维护性、可重用性和扩展性,提高编译效率和测试效率,我们使用多文件的编程方式来设计扫雷的程序代码

首先我们创建项目准备一个头文件game.h用来做函数的声明,一个源文件game.c用来写函数的实现,最后用一个源文件test.c来作主函数的测试运行。

二、菜单的创作

框架(test.c)

我们写一个基础的框架,将头文件都放在game.h里面,就需要包含头文件。

注意,需要在game.ctext.c中包含一下头文件,要注意自己写的头文件要用双引号引用如下:

接着定义一个主函数main,为了避免代码的混乱,我不想把所以函数写到一个主函数里,所以我定义一个test函数来完成游戏的测试逻辑,在函数test中实现。

//主函数
int main() {
	test();
	return 0;
}

void test()
{
     //游戏逻辑的实现在test函数中完成,避免主函数的代码混乱
}

为了让玩家有好的游戏体验,我们得建立一个游戏菜单供玩家选择是否开始游戏,所以打印菜单menu(),让玩家进行选择。

void menu()
{
	printf("*******扫雷游戏*******\n");
	printf("****** 1. play *******\n");
	printf("****** 0. exit *******\n");
	printf("**********************\n");
	system("color 71");
}

为了美化游戏体验,我们将游戏页面控制台的文字和背景给点颜色,需要包含以下头文件,用来写一个color()函数来设置控制台的颜色。

#include<stdlib.h>

 为了方便大家了解color()函数对控制台颜色的整理,于是用以下代码来实现颜色的整理

#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
void color(int k)
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), k);
}
int main()
{
	for (int i = 1; i <= 255; i++)
	{
		color(i);
		printf("%3d ", i);
		if (i%15==0)
			printf("\n");
	}
	color(15);
	return 0;
}

以上代码的运行结果如下

C语言字体颜色教学可点击此处 ,学习大佬的博客

接着函数进来是直接用do-whlie进行打印菜单,然后可以玩家进行选择,在控制台输入'1'则进入游戏game()函数,输入'0',则退出游戏,如果输入其他数则提示错误,需要重新输入。 

void test()
{
	int input = 0;
	srand((unsigned int)time(NULL));//调用srand(),用于下面rand()产生随机数时的设置随机种子数
	do
	{
		menu();
		printf("请选择是否开始游戏:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

三、棋盘制作以及初始化

接下来为了实现游戏的功能,我们需要设计和制作雷盘(以下统称棋盘);

回到我们的设计目标,我们需要设计的是一个9*9,的棋盘,为了防止越界,需要对棋盘进行扩大成11*11的棋盘,而为了不把棋盘设定的太死板,所以我在头文件game.h中来定义棋盘的行列和纵列,故我定义变量ROW和COL为棋盘的行列和纵列。定义ROWS和COLS为扩大后的棋盘。

#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2

那么我们需要制作两个棋盘。

第一个用来存放布置好的雷的信息(数组mine[ROWS][COLS])

第二个用来存放排查出的雷的信息(数组show[ROWS][COLS])

接着我们的整体思路是将数组初始化,即将棋盘初始化,首先将棋盘1全部初始化为字符'0',然后将棋盘2全部初始化为'*'。

为了同时将棋盘1和棋盘2初始化,我们先定义一个函数为InitBoard(),用于全部初始化棋盘,而二者所要初始化的字符不同,所以我们定义一个变量set用来储存初始化的值。

为了使用InitBoard()函数,我们需要在game.c头文件中声明函数,对其声明同时,我们定义三个参数,rows、cols、set分别储存行列数和初始值,并传给test.c,在game.c中实现。

game.h

#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//声明函数
//初始化棋盘,将数据传给test.c,在game.c中实现
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);

 test.c

char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的个数信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');

game.c

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;
		}
	}
}

到这里,我们的棋盘的制作和初始化便已经完成了,那么我们可以将棋盘打印出来,检查是否是我们想要的。

四、雷盘的打印

为了打印我们的雷盘,和上述所说的一样,我们定义一个函数DisplayBoard();用于检查雷盘的准确性。

game.h

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

test.c

void game()
{
	char mine[ROWS][COLS];//存放布置好的雷
	char show[ROWS][COLS];//存放排查出的雷的个数信息
	//初始化棋盘
	//1. mine数组最开始是全'0'
	//2. show数组最开始是全'*'
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
    DisplayBoard(mine, ROW, COL);
    DisplayBoard(show, ROW, COL);
}

game.c 

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("--------扫雷游戏-------\n");
    //打印列号
	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 ", board[i][j]);
		}
		printf("\n");
	}
}

运行结果

检测运行无误后,为了游戏逻辑的连贯性和后续游戏的体验,我们应该将测试代码注释掉。

void game()
{
	char mine[ROWS][COLS];//存放布置好的雷
	char show[ROWS][COLS];//存放排查出的雷的个数信息
	//初始化棋盘
	//1. mine数组最开始是全'0'
	//2. show数组最开始是全'*'
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
    //DisplayBoard(mine, ROW, COL);
    //DisplayBoard(show, ROW, COL);
}

做完前期雷盘的编排和布置后我们接下来就是需要确保游戏可以玩,所以我们需要开始设计埋雷的代码。

五、埋雷的设计

目的:在9*9的棋盘上随机布置10颗雷,布置在9*9的棋盘上

我们创建SetMine()函数,用来随机布置10颗雷

首先,我们需要定义雷的个数,我将雷定义为EASY_COUNT,令雷的个数为10,并且声明SetMine()函数。

game.h

#define EASY_COUNT 10

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

test.c

#include <stdlib.h>
#include <time.h>
SetMine(mine, ROW, COL);

 注意:mine()数组是11*11,所以传过去的数也必须是11*11,不能因为你仅仅操作11*11格子里面的9*9格子而传9*9,所以声明函数时用ROWS和COLS,而不是ROWS和COLS。

 接着声明和定义完成后,我们需要在game.c中设计SetMine()函数,因为布雷是随机的,所以就要用到一个随机数,所以用到随机数函数rand(),这里需要包含头文件<stdlib.h><time.h>。(在头文件中引入即可)

在调用rand()函数之前,可以使用srand()函数设置随机数种子,如果没有设置随机数种子,rand()函数在调用时,自动设计随机数种子为1。随机种子相同,每次产生的随机数也会相同。

srand()用来设置rand()产生随机数时的随机数种子。

上文已调用,如下图所示

因为不需要time函数的参数,所以传入一个空指针NULL,并强制返回类型为unsigned int

game.c

int count = EASY_COUNT;
 while (count)
 {
 int x = rand() % row + 1;//row=9,rand函数的使用:rand()%row+1表示产生1到9以内的随机整数
 int y = rand() % col + 1;//col=9,rand函数的使用:rand()%col+1表示产生1到9以内的随机整数
//所以此时的[x][y]是一个随机坐标
//布置雷,如果board[x][y]坐标上没有雷('0'),则在这个坐标生成一个雷,然后总雷数count减1
//如果有雷('1'),则重新循环,直到count=0
 if (board[x][y] == '0')
 {
 board[x][y] = '1';
 count--;
 }
 }

 如果想对随机数函数rand()有更加深入的了解,可以参考以下链接

https://blog.csdn.net/chikey/article/details/66970397

再次注意time函数和rand函数需要包含头文件<stdlib.h>和<time.h>,不要忘记了!!!

此时我们布置雷的信息已经设计结束,我们可以测试以下所写代码是否符合我们的想法和是否复制成功,我们可以打印将布置的mine雷盘打印出来,我们使用函数DisplayBoard()

void game()
{
	char mine[ROWS][COLS];//存放布置好的雷
	char show[ROWS][COLS];//存放排查出的雷的个数信息
	//初始化棋盘
	//1. mine数组最开始是全'0'
	//2. show数组最开始是全'*'
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');

	//打印棋盘
	//DisplayBoard(mine, ROW, COL);
	//DisplayBoard(show, ROW, COL);

	// 布置雷
	SetMine(mine, ROW, COL);
	DisplayBoard(mine, ROW, COL);//用来检测,在正式使用游戏时需要注释掉

}

运行结果

以上10颗雷已经布置完毕,说明我们的代码正确

六、排查雷的设计

目的:将排查的雷的周围剩余的雷数

在排查雷之前我们需要打印雷盘,一开始雷盘还没开始排查所以,每个格子都是 '*' 

test.c

void game()
{
	char mine[ROWS][COLS];//存放布置好的雷
	char show[ROWS][COLS];//存放排查出的雷的个数信息
	//初始化棋盘
	//1. mine数组最开始是全'0'
	//2. show数组最开始是全'*'
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
	//DisplayBoard(mine, ROW, COL);
	//DisplayBoard(show, ROW, COL);

	// 布置雷
	SetMine(mine, ROW, COL);
	//DisplayBoard(mine, ROW, COL);
    
    //打印雷盘 
    DisplayBoard(show, ROW, COL);

}

我们创建FindMine()函数,用于排查雷,在mine数组中排查,然后将排查的信息放入show数组中,因此在传参的时候会同时涉及到mine数组和show数组,所以两个数组都需要传

test.c

void game()
{
	char mine[ROWS][COLS];//存放布置好的雷
	char show[ROWS][COLS];//存放排查出的雷的个数信息
	//初始化棋盘
	//1. mine数组最开始是全'0'
	//2. show数组最开始是全'*'
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
	//DisplayBoard(mine, ROW, COL);
	//DisplayBoard(show, ROW, COL);

	// 布置雷
	SetMine(mine, ROW, COL);
	//DisplayBoard(mine, ROW, COL);
    
    //打印雷盘 
    DisplayBoard(show, ROW, COL);

    //排查雷
    FindMine(mine, show, ROW, COL);
}

game.h

//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

函数的实现

game.c

//此函数用与统计排查坐标周围函数有几个雷
static int GetMineCount(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-1]     +
		    mine[x + 1][y + 1] + 
            mine[x][y + 1]     + 
            mine[x - 1][y + 1] - 
            8 * '0');
}//‘1’的值是49;'0'的值是48;‘1’-‘0’=1是个整数。我们将周边的数都加起来-8*‘0’就能得到雷的数。

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while (win < row * col - EASY_COUNT)
	{
		printf("请输入要排查的坐标:");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine, ROW, COL);
				break;
			}
			else
			{
				//该位置不是雷,就统计这个坐标周围有⼏个雷
				int count = GetMineCount(mine, x, y);
				show[x][y] = count + '0';
				DisplayBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("坐标⾮法,重新输⼊\n");
		}
	}
	if (win == row * col - EASY_COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

不希望别人看到,static是静态的意思,静态函数只能在声明它的文件中可见,其他文件不能引用该函数。

到这里我们的扫雷设计就已经全部完成了,以下是所有代码的整合

game.h

#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 EASY_COUNT 10
//声明函数
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);//将数据传给test.c,在game.c中实现
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col);
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
#include <stdlib.h>
#include <windows.h>

//游戏逻辑的测试,包含游戏菜单的打印,游戏设计的基本逻辑的展示。
void menu()
{
	printf("*******扫雷游戏*******\n");
	printf("****** 1. play *******\n");
	printf("****** 0. exit *******\n");
	printf("**********************\n");
	system("color 71");
}
void game()
{
	char mine[ROWS][COLS];//存放布置好的雷
	char show[ROWS][COLS];//存放排查出的雷的个数信息
	//初始化棋盘
	//1. mine数组最开始是全'0'
	//2. show数组最开始是全'*'
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
	//DisplayBoard(mine, ROW, COL);
	//DisplayBoard(show, ROW, COL);

	// 布置雷
	SetMine(mine, ROW, COL);
	//DisplayBoard(mine, ROW, COL);

	//打印雷盘 
	DisplayBoard(show, ROW, COL);

	//排查雷
	FindMine(mine, show, ROW, COL);
}

void test()
{
	int input = 0;
	srand((unsigned int)time(NULL));//播种由函数使用的随机数发生器
	do
	{
		menu();
		printf("请选择是否开始游戏:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

int main() {
	test();
	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 DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("--------扫雷游戏-------\n");
	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 ", board[i][j]);
		}
		printf("\n");
	}
}

void SetMine(char board[ROWS][COLS], int row, int col)
{
	//布置10个雷
	//⽣成随机的坐标,布置雷
	srand((unsigned int)time(NULL));//随机种子数
	/*int count = EASY_COUNT;*/
	/*for (int i = 1; i <= count; i++)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (board[x][y] == '1')
		{
			count--;
			continue;
		}
		board[x][y] = '1';
	}*/
	int count = EASY_COUNT;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (board[x][y] == '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

//此函数用与统计排查坐标周围函数有几个雷
 static int GetMineCount(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-1]+
		mine[x + 1][y + 1] + mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0');
}

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while (win < row * col - EASY_COUNT)
	{
		printf("请输入要排查的坐标:");
		scanf("%d %d", &x, &y);
		//判断坐标的有效性
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine, ROW, COL);
				break;
			}
			else
			{
				//该位置不是雷,就统计这个坐标周围有⼏个雷
				int count = GetMineCount(mine, x, y);
				show[x][y] = count + '0';
				DisplayBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("坐标⾮法,重新输⼊\n");
		}
	}
	if (win == row * col - EASY_COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

扫雷游戏的扩展

• 是否可以选择游戏难度

    ◦ 简单 9*9 棋盘,10个雷

    ◦ 中等 16*16棋盘,40个雷

    ◦ 困难 30*16棋盘,99个雷

• 如果排查位置不是雷,周围也没有雷,可以展开周围的一片

• 是否可以标记雷

• 是否可以加上排雷的时间显示

扩展加紧更新中。。。。。。

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值