C语言实现扫雷小游戏 纯小白 非黑窗口

扫雷

扫雷是我接触最早的一款游戏,小时候一直以为这是一个碰运气瞎点的游戏,长大后才逐渐会玩,到了今天,我才有能力将他用我所学的C语言实现。这里是我编写的扫雷的一些效果图:
扫雷
扫雷

主要功能

关于扫雷相信大家并不陌生,因为能力有限,我只做了如下几个功能,还希望有大佬可以指点:
1、左键点击展开
2、右键单击插旗子
3、右键双击显示问号
4、左键单击数字进行区域展开(若区域内有错误的旗子则直接游戏结束)
5、统计雷的数量
6、显示游戏结束画面

1、创建一个图形界面

我们知道,在C语言的编译器调试程序时只显示一个黑方框。但是扫雷是一个图形窗口,所以要实现扫雷,我们首先要创建一个图形窗口。这里我们需要引入一个头文件graphics.h,如果你使用的是vs或者是vc那你需要去下载EASYX 这个软件,这里是官网 easyX。下载好后,开始创建我们的图形窗口,代码如下。

// An highlighted block
#include <graphics.h>

HWND scanmine = initgraph(ROW * SIZE + 100, COL * SIZE);

initgraph这个函数的作用就是创建一个图形窗口,而不是一个普通的黑窗口,其中的两个参数分别是窗口的长与宽。当然,此时你运行这段代码的话是无法看到黑窗口的,如果你想在图形窗口调试的时候看到黑窗口中的数据的话,可以这样写

// An highlighted block
#include <graphics.h>

HWND scanmine = initgraph(ROW * SIZE + 100, COL * SIZE, SHOWCONSOLE);

2、了解扫雷游戏的原理

我们在运行扫雷时,会发现每一次雷出现的位置都不同,所以我们需要随机生成这些雷,生成之后,我们还会发现,在这些雷周围的的八个块上面的数字会加一,我们也根据这些数字来完成“扫雷”这一目标。这就是扫雷的基本原理。

3、随机生成雷的位置

首先如果我们看到扫雷这种2D游戏时,应该首先想到用二维数组实现他它,所以我们在这边定义一个二维数组,并将其中的数据初始化为0。我们现在默认雷的位置为-1,没有雷的位置为0,那么将数组的列定义为col,将数组的行定义为row,并为他们在内存中开辟空间。这时我们只需要让随机数函数生成的随机数值赋给我们的行与列就可以了。
注意 既然随机数生成的时候是随机的,那么他就有可能生成相同的随机数,从而导致我们游戏中的雷数与预期不符。因此,我们需要在这边加一个限制条件:首先判断所生成的雷的位置的数组数值是否为0,如果为0则将他改为-1,如果已经是-1,则重新生成一个随机位置。

4、为整个数组加密,并在雷周围的位置加一

我们在玩扫雷的时候,只有左键点开才会看到格子下面的的雷或者数字,说明这个游戏是由两层图片组成的,那么要实现这一功能,我们就需要将第一层进行加密,我们为每一个格子加20,就完成了加密,在后面鼠标点击是,只需要一个获取鼠标信息的函数,将20减掉即可。除此以外我们再将雷周围的数组数据加一即可。
下面是这两步的代码:

//生成雷
int gameInit()
{
	srand((unsigned)time(NULL));
	//初始化格子
	for (int row = 0; row < ROW + 2; row++)
	{
		for (int col = 0; col < COL + 2; col++)
		{
			map[row][col] = 0;
		}
	}
	//生成雷
	for (int n = 0; n < MINE_NUM;)
	{
		int row = rand() % ROW+1;
		int col = rand() % COL+1;
		if (map[row][col] == 0)
		{
			map[row][col] = -1;
			n++;
		}
	}
	//遍历数组,找空格
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			if (map[i][j] != -1)
			{
				for (int m = i - 1; m <= i + 1; m++)
				{
					for (int n = j - 1; n <= j + 1; n++)
					{
						if (map[m][n] == -1)
						{
							map[i][j]++;
						}
					}
				}
			}
		} 
	}
	//加密
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			map[i][j] += 20;
		}
	}
}

5、导入图片并在图形区打印整个游戏

整个游戏的数据构建好之后,我们开始准备我们的图形游戏区域,利用"graphics.h"中的loadimage()函数,将素材中的空白、1~8、雷,以及红旗和问号全部导入程序文件中,以便使用时调用。这里我用了sprintf(),可以将每一张图片按顺序导入程序

//加载图片
void image()
{
	for (int i = 0; i < IMAGE_NUM; i++)
	{
		char fileName[20] = "";
		sprintf(fileName, "./image/%d.gif", i);
		loadimage(&img[i], fileName, SIZE, SIZE);
	}
}  

下面是根据每一种数据的范围所贴的图片:

//打印游戏区
void gameDraw()
{
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			printf("%3d", map[i][j]);
			//贴雷
			if (map[i][j] == -1)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[11]);
			}
			//贴数字
			else if (map[i][j] >= 0 && map[i][j] <= 8)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[map[i][j]]);
			}
			//贴空白
			else if (map[i][j] >= 19 && map[i][j] <= 28)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[9]);
			}
			//标记
			else if (map[i][j] >= 39 && map[i][j] <= 48)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[10]);
			}
			//贴问号
			else if (map[i][j] >= 59 && map[i][j] <= 68)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[12]);
			}
			//贴炸雷
			else if (map[i][j] == -2)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[13]);
			}
		}
		printf("\n");
	}
}

6、开玩!

以上是扫雷的一些最基础的部分,下面我们开始进行鼠标信息部分的编写。
用到最多的函数是GetMouseMsg(),这个函数里面包含了一些最基本的鼠标信息反馈函数,方便我们调用。回忆一下扫雷的玩法:左键点击展开,右键一次插红旗,右键两次红旗变成问号。所以在这里左右键点击的功能主要是对上面我们已经进行加密的格子进行解密,说白了就是对数字进行加减,从而达到点开格子的功能。下面是代码:

//鼠标点击开玩
int play()
{
	MOUSEMSG msg = { 0 };
	int r, c;
	while (1) 
	{
		msg = GetMouseMsg();
		switch (msg.uMsg)
		{
		//翻开
		case WM_LBUTTONDOWN:
			r = msg.x / SIZE + 1;
			c = msg.y / SIZE + 1;
			if (map[r][c] >= 19 && map[r][c] <= 28)
			{
				if (map[r][c]==20)    				//如果是空白就翻开
				{
					blankOpen(r,c);   				//这个函数我在下面的拓展功能会讲到,是一个连续展开空白的功能。
					return map[r][c];
				}
				else                  				//如果不是空白就翻开格子,并将翻开的格子数加一			
				{
					map[r][c] -= 20;
					count++;
					return map[r][c];
				}
			}
			break;
		//插旗子,拔旗子
		case WM_RBUTTONDOWN:
			r = msg.x / SIZE + 1;
			c = msg.y / SIZE + 1;
			if (map[r][c] >= 19 && map[r][c] <= 28)  //是未翻开的格子就插旗子
			{
				map[r][c] += 20;
			}
			else if (map[r][c]>=39 && map[r][c]<=48) //点击两次旗子变成问号。
			{
				map[r][c] += 20;
			}
			else if (map[r][c]>=59 && map[r][c]<=68) //点击三次问号消失,变成未翻开的样子。
			{
				map[r][c] -= 40;
			}
			return map[r][c];
			break;
		}
	}
}

一些附加功能

做完了上面的步骤之后一个简单的扫雷就基本完成了。但是要想实现我们小时候的扫雷还差一点。所以我在下面加了一些功能

1、点击到空白格子时自动展开

这个功能就是我上面的blankOpen,先看代码,我来慢慢解释:

//连续展开
void blankOpen(int r,int c)
{
//打开格子
	map[r][c] -= 20;
	count++;
	//点开后遍历九宫格
	for (int m=r-1;m<=r+1;m++)
	{
		for (int n=c-1;n<=c+1;n++)
		{
			if (m >= 1 && m <= ROW && n >= 1 && n <= COL)			//保证是游戏区
			{
				if (map[m][n] >= 19 && map[m][n] <= 28)				//必须为空白格
				{
					if (map[m][n]!=20)
					{
						map[m][n] -= 20;
						count++;
					}
					else
					{
						blankOpen(m,n);
					}
				}
			}
		}
	}
}

基本的原理是这样的,如果鼠标信息知道了我点击的格子是一个空白格子,那么将他翻开后开始对周围的八个格子进行遍历,如果遍历到空白格子那么就进行下一次遍历,算是一个递归函数。那么根据这个原理,想实现点击数字并对周围进行展开也就不是很难了:

//左键点击已经点开的块,遍历周围的块
int open(int r,int c)
{
	int flag = 0;
	for (int m = r - 1; m <= r + 1; m++)
	{
		for (int n = c - 1; n <= c + 1; n++)
		{
			if (m >= 1 && m <= ROW && n >= 1 && n <= COL)			//保证是游戏区
			{
				if (map[m][n] == 19)
				{
					flag = 1;
				}
			}
		}
	}
	if (flag == 0)
	{
		for (int m = r - 1; m <= r + 1; m++)
		{
			for (int n = c - 1; n <= c + 1; n++)
			{
				if (map[m][n] >= 19 && map[m][n] <= 28)
				{
					map[m][n] -= 20;
					blankOpen(r, c);
				}

			}
		}
	}
	else if (flag == 1)
	{
		for (int m = r - 1; m <= r + 1; m++)
		{
			for (int n = c - 1; n <= c + 1; n++)
			{
				if (map[m][n] > 39 && map[m][n] <= 48)
				{
					return -1;
				}
			}
		}
	}
}

2、显示剩余的雷数

我们需要知道进行游戏时整张地图上还剩下多少雷,以此作为一个扫雷的判断的依据,下面的的代码可以实现这个功能,原理是:每当左键点击一次过后,对整张地图进行遍历,把未解密的雷数显示出来(说实话我觉得这里写的有一点啰嗦,希望有高人指点)。上代码:

//显示剩余雷数
int print()
{
	char num[MINE_NUM];
	int n = 0;
	for (int r = 1; r <= ROW; r++)
	{
		for (int c = 1; c <= COL; c++)
		{
			if (map[r][c] == 19 || map[r][c] == -1)
			{
				n++;
			}
		}
	}
	outtextxy(770, 200, "剩余的雷:");//这个函数可以将文字显示在图形窗口上。
	sprintf(num, "%02d", n);
	outtextxy(790, 230, num);
	printf("\n\n\n");
	printf("%02d",n);
}

3、输赢的判断

要想真正开玩,我们必须加上输赢判断的功能。输这个条件我们都知道,点击到雷,或者是标记错误的同时点击了数字对周围的格子进行了遍历都会输。那么什么条件下我们才算赢呢???试想:整个数组的格子数一共有ROWCOL个,而雷数有MINE_NUM个,所以我们只需要判断点开雷以外的格子数是否等于ROWCOL-MINE_NUM这个数值即可。这部分代码我直接会在主函数中体现。

真正开玩!

下面是整个程序的代码:

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

#define COL 16//宽
#define ROW 30//长
#define MINE_NUM 60//雷数
#define SIZE 25//图片大小
#define IMAGE_NUM 14//图片数量

int map[ROW+2][COL+2];//+2是添加了辅助区,用来防止数组越界。
int count = 0;
IMAGE img[IMAGE_NUM];


//生成雷
int gameInit() 
{
	srand((unsigned)time(NULL));
	//初始化格子
	for (int row = 0; row < ROW + 2; row++)
	{
		for (int col = 0; col < COL + 2; col++)
		{
			map[row][col] = 0;
		}
	}
	//生成雷
	for (int n = 0; n < MINE_NUM;)
	{
		int row = rand() % ROW+1;
		int col = rand() % COL+1;
		if (map[row][col] == 0)
		{
			map[row][col] = -1;
			n++;
		}
	}
	//遍历数组,找空格
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			if (map[i][j] != -1)
			{
				for (int m = i - 1; m <= i + 1; m++)
				{
					for (int n = j - 1; n <= j + 1; n++)
					{
						if (map[m][n] == -1)
						{
							map[i][j]++;
						}
					}
				}
			}
		} 
	}
	//加密
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			map[i][j] += 20;
		}
	}
}
//打印游戏区
void gameDraw()
{
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			printf("%3d", map[i][j]);
			//贴雷
			if (map[i][j] == -1)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[11]);
			}
			//贴数字
			else if (map[i][j] >= 0 && map[i][j] <= 8)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[map[i][j]]);
			}
			//贴空白
			else if (map[i][j] >= 19 && map[i][j] <= 28)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[9]);
			}
			//标记
			else if (map[i][j] >= 39 && map[i][j] <= 48)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[10]);
			}
			//贴问号
			else if (map[i][j] >= 59 && map[i][j] <= 68)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[12]);
			}
			//贴炸雷
			else if (map[i][j] == -2)
			{
				putimage((i - 1) * SIZE, (j - 1) * SIZE, &img[13]);
			}
		}
		printf("\n");
	}
}
//加载图片
void image()
{
	for (int i = 0; i < IMAGE_NUM; i++)
	{
		char fileName[20] = "";
		sprintf(fileName, "./image/%d.gif", i);
		loadimage(&img[i], fileName, SIZE, SIZE);
	}
}  
//连续展开
void blankOpen(int r,int c)
{
	//打开格子
	map[r][c] -= 20;
	count++;
	//点开后遍历九宫格
	for (int m=r-1;m<=r+1;m++)
	{
		for (int n=c-1;n<=c+1;n++)
		{
			if (m >= 1 && m <= ROW && n >= 1 && n <= COL)			//保证是游戏区
			{
				if (map[m][n] >= 19 && map[m][n] <= 28)				//必须为空白格
				{
					if (map[m][n]!=20)
					{
						map[m][n] -= 20;
						count++;
					}
					else
					{
						blankOpen(m,n);
					}
				}
			}
		}
	}
}
//左键点击已经点开的块,遍历周围的块
int open(int r,int c)
{
	int flag = 0;
	for (int m = r - 1; m <= r + 1; m++)
	{
		for (int n = c - 1; n <= c + 1; n++)
		{
			if (m >= 1 && m <= ROW && n >= 1 && n <= COL)			//保证是游戏区
			{
				if (map[m][n] == 19)
				{
					flag = 1;
				}
			}
		}
	}
	if (flag == 0)
	{
		for (int m = r - 1; m <= r + 1; m++)
		{
			for (int n = c - 1; n <= c + 1; n++)
			{
				if (map[m][n] >= 19 && map[m][n] <= 28)
				{
					map[m][n] -= 20;
					blankOpen(r, c);
				}

			}
		}
	}
	else if (flag == 1)
	{
		for (int m = r - 1; m <= r + 1; m++)
		{
			for (int n = c - 1; n <= c + 1; n++)
			{
				if (map[m][n] > 39 && map[m][n] <= 48)
				{
					return -1;
				}
			}
		}
	}
}
//输的时候显示所有雷
void boom()
{
	for (int r = 1; r <= ROW; r++)
	{
		for (int c = 1; c <= COL; c++)
		{
			if (map[r][c]==19)
			{
				map[r][c] -= 21;
			}
			else if (map[r][c] == -1)
			{
				map[r][c] -= 1;
			}
		}
	}
}
//显示剩余雷数
int print()
{
	char num[MINE_NUM];
	int n = 0;
	for (int r = 1; r <= ROW; r++)
	{
		for (int c = 1; c <= COL; c++)
		{
			if (map[r][c] == 19 || map[r][c] == -1)
			{
				n++;
			}
		}
	}
	outtextxy(770, 200, "剩余的雷:");
	sprintf(num, "%02d", n);
	outtextxy(790, 230, num);
	printf("\n\n\n");
	printf("%02d",n);
}
//鼠标点击开玩
int play()
{
	MOUSEMSG msg = { 0 };
	int r, c;
	while (1) 
	{
		msg = GetMouseMsg();
		switch (msg.uMsg)
		{
		//翻开
		case WM_LBUTTONDOWN:
			r = msg.x / SIZE + 1;
			c = msg.y / SIZE + 1;
			if (map[r][c] >= 19 && map[r][c] <= 28)
			{
				if (map[r][c]==20)
				{
					blankOpen(r,c);
					return map[r][c];
				}
				else
				{
					map[r][c] -= 20;
					count++;
					return map[r][c];
				}
			}
			else if (map[r][c] >= 0 && map[r][c] <= 8)
			{
				open(r, c);
				if (open(r,c)==-1)
				{
					return -1;
				}
				else 
				{
					return map[r][c];
				}
			}
			break;
		//插旗子,拔旗子
		case WM_RBUTTONDOWN:
			r = msg.x / SIZE + 1;
			c = msg.y / SIZE + 1;
			if (map[r][c] >= 19 && map[r][c] <= 28)
			{
				map[r][c] += 20;
			}
			else if (map[r][c]>=39 && map[r][c]<=48)
			{
				map[r][c] += 20;
			}
			else if (map[r][c]>=59 && map[r][c]<=68)
			{
				map[r][c] -= 40;
			}
			return map[r][c];
			break;
		}
	}
}
//游戏主函数
int main()
{
	HWND scanmine = initgraph(ROW * SIZE + 100, COL * SIZE);
	image();
	gameInit();
	while (1)
	{
		gameDraw();
		print();
		if (play() == -1)
		{
			boom();
			gameDraw();
			MessageBox(scanmine, "你输了", "", MB_OK);
			break;
		}
		if (ROW * COL - MINE_NUM == count)
		{
			MessageBox(scanmine, "你赢了", "", MB_OK);
			break;
		}
	}
	closegraph();
	return 0;
}

总结

这是大一的我写的第一个比较复杂的程序,中间也参考了许多大佬的版本,最终整合出了我这个版本,一定有不完美的地方,希望大家看到后可以在下方留言或者私信我,帮我修改完善这个扫雷的小程序,帮我提升自己。总体过程是快乐的,很享受解决每一个问题的过程,我盼望着有一天可以像那些大佬一样可以完全自己设计程序,也希望每一个大一的程序员可以在编程中找到乐趣,爱上编程,永远不头秃!

图片素材我会传到我的个人主页上! 这里是链接:扫雷图片素材

  • 95
    点赞
  • 373
    收藏
    觉得还不错? 一键收藏
  • 33
    评论
评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值