C语言实现扫雷游戏

C语言实现扫雷游戏

使用easyx实现可视化编程


1.目标功能

  • 非控制台的游戏界面显示
  • 基本游戏规则

扫雷规则

  • 区分探测未探测格子的颜色
  • 插旗
  • 实时显示游戏中未探测到雷的个数
  • 用表情包显示出目前扫雷状态(有扫雷中,成功,失败三种表情)
  • 保证玩家第一次点击时不会被雷炸到
  • 一点一大片(当一个格子周围没有雷时,该格子周围的格子会被自动探测,该格子周围的空白格会引发连锁反应)
  • 计时器
  • 每轮游戏结束后可以自己选择退出或重开

2.游戏实现

核心思路:玩家操作会改变数组数值,利用数组的数值变化,对不同数值的格子进行贴图,从而控制格子呈现不同状态

格子数值对应状态及图片左击对应操作和在数组中的体现右击对应操作和在数组中的体现
-110(雷)
0-80-8(数字)
19-289(空白格)-20 翻开格子+20 插旗
>3011(旗子)-20 复原插旗处

(这张表格里的内容在接下来会进一步解释)

具体过程:

  • 游戏初始化+随机布雷

    • 定义地图行列,目前难度的雷数

    • 创建数组,全部初始化为0

    • 随机挑选10个位置改变数值为-1,即通过不同数值实现雷与普通格子的区分(若挑选中的位置数值已经为-1,则重新选择另一位置改变数值)

      # define ROW 10
      
      # define COL 10
      
      # define EASYCOUNT 10
      
      # define SIZE 40
      
      int Mine = EASYCOUNT;
      int i, j, flag;
      int k = 0;
      int click = 0;
      int playtime = 0;
      clock_t start=0;
      clock_t end = 0;
      int map[ROW][COL];
      IMAGE img[16];
      
      void initMap()
      {
      	Mine = EASYCOUNT;
      	playtime = 0;
      	k = 0;//每次重开时计时变量k都初始化为0
      	click = 0;//每次重开时计次变量click都初始化为0
      	//数组初始化
      	for (i = 0; i < ROW; i++)
      	{
      		for (j = 0; j < COL; j++)
      		{
      			map[i][j] = 0;
      		}
      	}
          //随机布雷
      	srand((unsigned)time(NULL));
      	int count = EASYCOUNT;
      	while (count)
      	{
      		int m = rand() % ROW;
      		int n = rand() % COL;
      		if (map[m][n] == 0)
      		{
      			map[m][n] = -1;
                  count--;
              }
      	}
      
  • 改变数组数值来实现不同状态无雷格子的区分(与上个代码块封装在同一函数中)

    • 寻找出所有有雷格,将其所在九宫格除自身外所有格子数值+1,使得无雷的格子的数值更改为周围雷数
    • +20使得所有格子在初始时进入空白格状态(代表未翻开)
//显示周围格子中雷的个数
for (int i= 0; i< ROW; i++)
{
	for (int j = 0; j < COL; j++)
		if (map[i][j] == -1)
		{
			for (int x = i - 1; x <=i + 1; x++)
			{
				for (int y = j - 1; y <= j + 1; y++)
				{
					if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] != -1)
					{
						map[x][y]++;
					}
				}
			}
		}
}
	//赋值,为进入格子未翻开状态做准备
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL; j++)
		{
			map[i][j] += 20;
		}
	}
}
  • 导入图片

    素材展示

    在这里插入图片描述

for (int i = 0; i <16; i++)
{
	char file[50] = "";
	sprintf(file, "./photo/%d.gif",i);
	loadimage(&img[i], file,SIZE,SIZE);
}
  • 为不同状态的格子贴图,实现可视化
void GameDraw()
{
	cleardevice();
	//为不同状态的格子贴图
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL; j++)
		{
			if (map[i][j] == -1)
			{
				putimage(j * SIZE, i * SIZE, &img[10]);
			}
			else if (map[i][j] >= 0 && map[i][j] <= 8)
			{
				putimage(j * SIZE, i * SIZE, &img[map[i][j]]);
			}
			else if (map[i][j] >= 19 && map[i][j] <= 28)
			{
				putimage(j * SIZE, i * SIZE, &img[9]);
			}
			else if (map[i][j] > 30)
			{
				putimage(j * SIZE, i * SIZE, &img[11]);
			}
		}
	}
}
  • 通过鼠标点击实现玩家操作
    • 翻开格子

    • 插旗

    • 当玩家第一次点击即踩雷时,将原来位置上的雷消去(赋值为0),再随机生成一个雷(此处别忘记更新旧雷和新雷周围格子数值)

    • 设置了计次变量click来检验玩家的第一次鼠标左键点击,click初始值设为0,而每次点击使得click的值+1,由此可得当click变量改变为1时,玩家进行了第一次鼠标左键点击

      效果展示

      在这里插入图片描述

   int MouseControl()
{
	//通过鼠标点击改变数值,从而实现翻开格子,插旗等功能
	if (MouseHit())
	{
		MOUSEMSG msg = GetMouseMsg();
		int row = msg.y/ SIZE;
		int col = msg.x/ SIZE;
		char str[50] = "";
		switch (msg.uMsg)
		{
		case WM_LBUTTONDOWN:
			//翻开格子
			if (map[row][col] > 8)
			{
				map[row][col] -= 20;
				flag++;
				k++;
				click++;
				//当第一次点击即踩雷时改变雷的位置
				if (click == 1 && map[row][col] == -1)
				{
					//将原来位置的雷消去
					map[row][col] = 0;
					//更新原来雷周围数据
					for (int x = row - 1; x <= row + 1; x++)
					{
						for (int y = col - 1; y <= col + 1; y++)
						{
						if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] == 19)
						{
							map[row][col]++;
						}
						if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] != 19&& (x!= row || y!= col))
						{
							map[x][y]--;
						}
					}
				}
				//再随机布雷一次
				int a = rand() % ROW ;
				int b = rand() % COL ;
				if ((a != row || b != col) && map[a][b] != 19)
				{
					map[a][b] = 19;
					//更新新雷周围数据
					for (int m = a - 1; m <= a + 1; m++)
					{
						for (int n = b - 1; n <= b + 1; n++)
						{
							if ((m >= 0 && m <= 9) && (n >= 0 && n <= 9) && map[m][n] != 19)
							{
								map[m][n]++;
							}
						}
					}

				}
			}
			if (k == 1)
			{
				start = clock();
			}
			openNull(row, col);
		}
	case WM_RBUTTONDOWN:
		//未插旗的格子插旗
		if (map[row][col] > 8 && map[row][col] <= 28)
		{
			map[row][col] += 20;
			Mine--;
			k++;
			if (k == 1)
			{
				start = clock();
			}
		}
		//已插旗的格子复原
		else if (map[row][col] > 28)
		{
			map[row][col] -= 20;
			Mine++;
		}
		break;
	}
		return map[row][col];
  }
}	
  • 遍历数值为0的格子(数值为0表示已打开,周围雷数为0)所在九宫格将其全部打开(在数组中体现为-20操作),此时这些格子中可能会再次出现数值为0的格子,则再次调用此函数(openNull)打开其周围格子,直到上一批打开的格子中不再出现数值为0的格子,递归停止
    • 注意此处特意将误插的旗子一并复原(当玩家之前错误地把旗子插在了非雷的空白格上,而该空白格恰好在此次操作中被打开)

void openNull(int row, int col);

void openNull(int row, int col)
{
	//通过递归实现点开一大片功能
	if (map[row][col] == 0)
	{
		for (int x = row - 1; x <= row + 1; x++)
		{
			for (int y = col - 1; y <= col + 1; y++)
			{
				if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && (map[x][y] != 19) && (map[x][y] > 8))
				{
					if (map[x][y] >= 30)
					{
						Mine++;
					}
					map[x][y] -= 20;
					flag++;
					openNull(x, y);
				}
			}
		}
	}
}
  • 调试用的数组显示

    • 为了快速测试我们的代码中的各个功能是否能够正常运行,我们将数组内容打印到屏幕上来,这样可以轻松地找到所有的雷,以便测试踩雷,游戏失败或胜利等一系列操作而不用自己一遍遍玩游戏

    ​ (传说中的开外挂行为

    • 记得调试完后把这个函数屏蔽掉,这部分内容不会呈现给玩家
 void show()
{
	 //显示数组
	for (int i = 0; i< ROW;i++)
	{
		for (int j = 0; j < COL; j++)
		{
			printf("%2d  ", map[i][j]);
		}
		putchar( '\n');
	}
	system("cls");
}
  • 判断是否踩雷或扫雷完毕

    • 如果格子点开后数值为-1(原先数值为19,点开操作-20)即为踩雷,游戏结束

    • 如果插旗数+点开格子数=总的坐标数即为扫雷成功,游戏结束

    • 游戏结束后提供后续选项

      效果展示

      在这里插入图片描述

    void Judge()
    {
    	//踩雷及后续选项提供
    	if (MouseControl() == -1)
    	{
    		for (i = 0; i < ROW; i++)
    		{
    			for (j = 0; j < COL; j++)
    			{
    				if (map[i][j] == -1)
    				{
    					putimage(360, 400, &img[15]);
    					putimage(j * SIZE, i * SIZE, &img[10]);
    				}
    			}
    		}
    		FlushBatchDraw();
    		int isok=MessageBox(GetHWnd(), "扫雷失败,是否重新开始?","提示", MB_OKCANCEL);
    		if (IDOK == isok)
    		{
    			initMap();
    			flag = 0;
    		}
    		else 
    		{
    			exit(1);
    		}
            }
    //获胜及后续选项提供
    if (flag == ROW * COL - EASYCOUNT)
    {
    	putimage(360, 400, &img[14]);
    	FlushBatchDraw();
    	int isok = MessageBox(GetHWnd(), "扫雷成功,是否重新开始","提示", MB_OKCANCEL);
    	if (IDOK == isok)
    	{
    		initMap();
    		flag = 0;
    	}
    	else
    	{
    		exit(1);
    	}
    }
    }
    
  • 游戏中剩余雷数判断

    • 我们在实现插旗相关代码时已经设置了对于“Mine”这个变量的改变,每成功插旗一次雷数减1,每取消插旗一次雷数加1

      if (map[row][col] > 8 && map[row][col] <= 28)
      		{
      			map[row][col] += 20;
      			Mine--;
      			k++;
      			if (k == 1)
      			{
      				start = clock();
      			}
      		}
      		//已插旗的格子复原
      		else if (map[row][col] > 28)
      		{
      			map[row][col] -= 20;
      			Mine++;
      		}
      
    • 注意此处要限制雷的个数不能为负数,即插旗的个数不能超过10

    void mine()
    {
    	settextstyle(25, 10, 0);
    	settextcolor(BLUE);
    	char str[50] = "";
    	if (Mine >= 0)
    	{
    		sprintf(str, "剩余雷数为% d", Mine);
    		outtextxy(10, 410, str);
    	}
    	else
    	{
    		sprintf(str, "插旗超过上限,请减少您的旗子个数");
    		outtextxy(10, 410, str);
    	}
    }
    
  • 计时器的实现

    • 设置了计时变量k用于检测玩家的第一次鼠标点击(此刻开始计时比较符合现实中的游戏场景,玩家进入程序后可能不会第一时间开始游戏,如果一进入程序就开始计时会使得玩家得到的游戏用时有所偏差,与实际不符)

    • k在玩家第一次点击鼠标左键时+1而初始值设为0

    • 结合下面代码中的判断条件则可检测到玩家的第一次鼠标左键点击,此时开始计时,使用clock函数检测到开始时间

      k++;
      if (k == 1)
      {
      				start = clock();
      }
      
    • 使用clock函数实时检测现在时间,计算出游戏时间=现在时间-开始时间并输出在屏幕上

 void timeplay(clock_t start)
   {
   		TCHAR time_text[50];
   		_stprintf_s(time_text, _T("用时:%d"), 0);
   		outtextxy(160, 410, time_text);
   		if (k > 0)
   		{
   			end = clock();
   			playtime = (end - start) / 1000;
   			_stprintf_s(time_text, _T("用时:%d"), playtime);
   			outtextxy(160, 410, time_text);
   		}
   }



3.加上主函数的完整代码展示

#define _CRT_SECURE_NO_WARNINGS 1
#include <graphics.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include<conio.h>
#include <windows.h>
#pragma warning(disable : 4996)

# define ROW 10

# define COL 10

# define EASYCOUNT 10

# define SIZE 40

int Mine = EASYCOUNT;
int i, j, flag;
int k = 0;
int click = 0;
int playtime = 0;
clock_t start=0;
clock_t end = 0;
int map[ROW][COL];
IMAGE img[16];

void initMap()
{
	Mine = EASYCOUNT;
	playtime = 0;
	k = 0;
	click = 0;
	//数组初始化
	for (i = 0; i < ROW; i++)
	{
		for (j = 0; j < COL; j++)
		{
			map[i][j] = 0;
		}
	}
	//随机布雷
	srand((unsigned)time(NULL));
	int count = EASYCOUNT;
	while (count)
	{
		int m = rand() % ROW;
		int n = rand() % COL;
		if (map[m][n] == 0)
		{
			map[m][n] = -1;
			count--;
		}
	}
	//显示周围格子中雷的个数
	for (int i= 0; i< ROW; i++)
	{
		for (int j = 0; j < COL; j++)
			if (map[i][j] == -1)
			{
				for (int x = i - 1; x <=i + 1; x++)
				{
					for (int y = j - 1; y <= j + 1; y++)
					{
						if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] != -1)
						{
							map[x][y]++;
						}
					}
				}
			}
	}
	//图片的导入
	for (int i = 0; i <16; i++)
	{
		char file[50] = "";
		sprintf(file, "./photo/%d.gif",i);
		loadimage(&img[i], file,SIZE,SIZE);
	}
	//赋值,为进入格子未翻开状态做准备
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL; j++)
		{
			map[i][j] += 20;
		}
	}
}


void GameDraw()
{
	cleardevice();
	//为不同状态的格子贴图
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL; j++)
		{
			if (map[i][j] == -1)
			{
				putimage(j * SIZE, i * SIZE, &img[10]);
			}
			else if (map[i][j] >= 0 && map[i][j] <= 8)
			{
				putimage(j * SIZE, i * SIZE, &img[map[i][j]]);
			}
			else if (map[i][j] >= 19 && map[i][j] <= 28)
			{
				putimage(j * SIZE, i * SIZE, &img[9]);
			}
			else if (map[i][j] > 30)
			{
				putimage(j * SIZE, i * SIZE, &img[11]);
			}
		}
	}
}
void openNull(int row, int col);

int MouseControl()
{
	//通过鼠标点击改变数值,从而实现翻开格子,插旗等功能
	if (MouseHit())
	{
		MOUSEMSG msg = GetMouseMsg();
		int row = msg.y/ SIZE;
		int col = msg.x/ SIZE;
		char str[50] = "";
		switch (msg.uMsg)
		{
		case WM_LBUTTONDOWN:
			//翻开格子
			if (map[row][col] > 8)
			{
				map[row][col] -= 20;
				flag++;
				k++;
				click++;
				//当第一次点击即踩雷时改变雷的位置
				if (click == 1 && map[row][col] == -1)
				{
					//将原来位置的雷消去
					map[row][col] = 0;
					//更新原来雷周围数据
					for (int x = row - 1; x <= row + 1; x++)
					{
						for (int y = col - 1; y <= col + 1; y++)
						{
							

							if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] == 19)
							{
								map[row][col]++;
							}
							if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] != 19&& (x!= row || y!= col))
							{
								map[x][y]--;
							}
						}
					}
					//再随机布雷一次
					int a = rand() % ROW ;
					int b = rand() % COL ;
					if ((a != row || b != col) && map[a][b] != 19)
					{
						map[a][b] = 19;
						//更新新雷周围数据
						for (int m = a - 1; m <= a + 1; m++)
						{
							for (int n = b - 1; n <= b + 1; n++)
							{
								if ((m >= 0 && m <= 9) && (n >= 0 && n <= 9) && map[m][n] != 19)
								{
									map[m][n]++;
								}
							}
						}
	
					}
				}
				if (k == 1)
				{
					start = clock();
				}
				openNull(row, col);
			}
		case WM_RBUTTONDOWN:
			//未插旗的格子插旗
			if (map[row][col] > 8 && map[row][col] <= 28)
			{
				map[row][col] += 20;
				Mine--;
				k++;
				if (k == 1)
				{
					start = clock();
				}
			}
			//已插旗的格子复原
			else if (map[row][col] > 28)
			{
				map[row][col] -= 20;
				Mine++;
			}
			break;
		}
			return map[row][col];
	}

}

void openNull(int row, int col)
{
	//通过递归实现点开一大片功能
	if (map[row][col] == 0)
	{
		for (int x = row - 1; x <= row + 1; x++)
		{
			for (int y = col - 1; y <= col + 1; y++)
			{
				if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && (map[x][y] != 19) && (map[x][y] > 8))
				{
					if (map[x][y] >= 30)
					{
						Mine++;
					}
					map[x][y] -= 20;
					flag++;
					openNull(x, y);
				}
			}
		}
	}
}

 void show()
{
	 //显示数组
	for (int i = 0; i< ROW;i++)
	{
		for (int j = 0; j < COL; j++)
		{
			printf("%2d  ", map[i][j]);
		}
		putchar( '\n');
	}
	system("cls");
}

void Judge()
{
	//踩雷及后续选项提供
	if (MouseControl() == -1)
	{
		for (i = 0; i < ROW; i++)
		{
			for (j = 0; j < COL; j++)
			{
				if (map[i][j] == -1)
				{
					putimage(360, 400, &img[15]);
					putimage(j * SIZE, i * SIZE, &img[10]);
				}
			}
		}
		FlushBatchDraw();
		int isok=MessageBox(GetHWnd(), "扫雷失败,是否重新开始?","提示", MB_OKCANCEL);
		if (IDOK == isok)
		{
			initMap();
			flag = 0;
		}
		else 
		{
			exit(1);
		}

	}
	//获胜及后续选项提供
	if (flag == ROW * COL - EASYCOUNT)
	{
		putimage(360, 400, &img[14]);
		FlushBatchDraw();
		int isok = MessageBox(GetHWnd(), "扫雷成功,是否重新开始","提示", MB_OKCANCEL);
		if (IDOK == isok)
		{
			initMap();
			flag = 0;
		}
		else
		{
			exit(1);
		}
	}

}

void mine()
{
	settextstyle(25, 10, 0);
	settextcolor(BLUE);
	char str[50] = "";
	if (Mine >= 0)
	{
		sprintf(str, "剩余雷数为% d", Mine);
		outtextxy(10, 410, str);
	}
	else
	{
		sprintf(str, "插旗超过上限,请减少您的旗子个数");
		outtextxy(10, 410, str);
	}
}

void timeplay(clock_t start)
{
		TCHAR time_text[50];
		_stprintf_s(time_text, _T("用时:%d"), 0);
		outtextxy(160, 410, time_text);
		if (k > 0)
		{
			end = clock();
			playtime = (end - start) / 1000;
			_stprintf_s(time_text, _T("用时:%d"), playtime);
			outtextxy(160, 410, time_text);
		}
}

int main()
{
	initgraph(ROW*SIZE, (COL+1)*SIZE, SHOWCONSOLE);
	setbkcolor(WHITE);
	initMap();
	BeginBatchDraw();
	while (1)
	{
		//show();
		Judge();
		GameDraw();
		putimage(360, 400, &img[13]);
		mine();
		timeplay(start);
		FlushBatchDraw();
	}
	EndBatchDraw();

	getchar();
	return 0;

}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值