扫雷游戏c++

扫雷

由于windows在Win10中就移除了扫雷,于是自己编写一个扫雷
easyx图形库,鼠标键盘监听,提示窗口,优先队列,广度搜素,重载运算符,文件操作,音乐操作,函数跳转,静态编译,窗口程序

easyx图形库准备

如使用vc,可在汉化文档中直接下载,然后为对应的vc版本进行安装

链接:EasyX_20240601 (2024-6-1 更新) - EasyX

作者喜欢用DEV,下载需点击左上角

在这里插入图片描述
点击more更多
在这里插入图片描述
点击MinGW,下载最新的即可

在这里插入图片描述

解压后会有头文件和库文件

找到你的DEV安装路径,将解压的 easyx 压缩包内 include 文件夹下的 easyx.h 和 graphics.h 按DEV的版本拷贝到如下图路径

在这里插入图片描述

同理,压缩包中,lib32/lib64 下的 库文件 也拷贝到 MinGW 库文件中。

最后,新建项目,并为项目参数中添加libeasyx.a 库文件或直接输入

“-leasyx”

初始界面一

扫雷有3个地图,简单难度9x9,共10颗雷,中等难度:16x16,共40颗雷,困难难度:30x16,共99颗雷。设计思路是第一界面直接选择难度即可,通过选择的难度建立相应的地图大小和选择相应难度的排行榜文件。经典的扫雷还有自定义行列和雷数的功能,但本人觉得自定义行列没有什么必要,自定义雷数的功能倒是可以制作,但是雷数的设置应当有一个上下限,放到第二界面进行选择。

于是,确定了第一界面的内容,难度选择和退出游戏的按键。是的,游戏总该有个退出键。

首先确定好需要的全局变量,行与列,雷数,旗帜数,翻开的格子数,地图数组,游戏难度标志。

int ROW = 9, COL = 9;//行与列
int NUM = 0;         //雷数
int mark = 0;         //旗子数
int found = 0;        //翻开格子数,用于判断胜利
int map[18][32] = { 0 };//地图,由于大地图可以包含小地图,所以只用一个
int mode = 1;//游戏难度

第一界面先使用easyx里的initgraph()函数创建一个图形窗口,并且允许鼠标双击操作(需要加入EX_DBLCLKS参数),大小是我自己取的

setbkcolor(RGB(192, 192, 192))函数会按照这个RGB值(灰色)设置该窗口的背景颜色

cleardevice();//紧接着调用其,即可进行背景的上色

setbkmode(TRANSPARENT);//将设置接下来出现在该窗口中的文字都将背景透明

然后制作4个按键,四个按键的大小都一致即可,这样只需要做一个按键,复制4遍修改一下坐标位置即可

按键的制作

setlinecolor(BLACK)设置线条的颜色为黑色

settextstyle(30, 0, “Consolas”); //设置文本的高度为30,宽度0即自适应,字体为Consolas

settextcolor(BLACK)设置文本颜色为黑色

setfillcolor(RGB(225, 225, 225))//设置接下来的染色区域的颜色为亮灰

rectangle(x, y, x + width, y + height); 会创建一个矩形的边框,作为我们按键的边框,颜色由setlinecolor设置

bar(x + 1, y + 1, x + width - 1, y + height - 1);会创建一个矩形的染色区域,颜色由setfillcolor设置

outtextxy(x + 5, y + (height - 20) / 2, text.c_str());会在设定的位置填充text的文本,颜色和字体由settextcolor,settextstyle设置

文本内容由text提供,该函数接收字符串的首地址,string的c_str()可以提供

void init() {
	initgraph(660, 440, EX_DBLCLKS);
	setbkcolor(RGB(192, 192, 192));
	cleardevice();//背景上色
	setbkmode(TRANSPARENT);//文字背景透明
    
	int x = 60, y = 50;
	int width = 240, height = 50;
	setlinecolor(BLACK);//设置按钮
	settextstyle(30, 0, "Consolas"); //
	settextcolor(BLACK);
	setfillcolor(RGB(225, 225, 225)); //

	string text = "简单:9x9-10";
	rectangle(x, y, x + width, y + height); //
	bar(x + 1, y + 1, x + width - 1, y + height - 1); //
	outtextxy(x + 5, y + (height - 20) / 2, text.c_str()); //

	text = "中等:16x16-40";
	rectangle(x, y + 100, x + width, y + height + 100); //
	bar(x + 1, y + 1 + 100, x + width - 1, y + height - 1 + 100); //
	outtextxy(x + 5, y + 100 + (height - 20) / 2, text.c_str()); //

	text = "困难:30x16-99";
	rectangle(x, y + 200, x + width, y + height + 200); //
	bar(x + 1, y + 200 + 1, x + width - 1, y + 200 + height - 1); //
	outtextxy(x + 5, y + 200 + (height - 20) / 2, text.c_str()); //

	text = "退出游戏";
	rectangle(x, y + 300, x + width, y + height + 300); //
	bar(x + 1, y + 300 + 1, x + width - 1, y + 300 + height - 1); //
	outtextxy(x + 5, y + 300 + (height - 20) / 2, text.c_str()); //
	rule();
	Sleep(100);
	while (End == 0) {
		getMouse1();
		Sleep(20);//归还cpu
	}
}

为了便于玩家了解游戏设计哪些功能,我在右边预留的空间来展示规则,由rule函数实现。按键创建完成后就是rule函数,将规则呈现。所用的函数和上面一样不过多介绍了。在rule函数后,就开始调用第一界面鼠标获取的函数getMouse1(),在接下来介绍。目前只需要知道其功能是为了获取鼠标的消息,了解用户按下了哪个按键,进行相应的跳转。但是鼠标消息的获取显然是一个持续性的动作,如果用户不操作,程序需要一直等待鼠标的消息,所以需要在while函数中循环地尝试获取消息,当收到程序应该结束的消息全局变量End=1,就可以结束游戏。

然而在while中一直等待鼠标消息也不是明智的,这样长时间的无操作将使得cpu一直陷入死循环等待,加入一个Sleep(20)使得当鼠标无消息时,程序中断20ms,将cpu归还,让cpu可以有能力去处理其他的事务,而不是一直等待我们的进程。

20ms会不会太短了呢,要知道家用计算机的速度是1ns执行一条指令,等待20ms相当于99.995%的时间都让cpu处于空闲。而人眼的反应速度只有100ms,所以20ms可以适应人的操作速度且不至于让cpu死循环。

void rule() {
	//setbkmode(TRANSPARENT);//文字背景透明
	setlinecolor(WHITE);//边框颜色
	settextstyle(25, 0, "楷体"); //文字设置
	settextcolor(WHITE);
	//排行榜填充颜色
	setfillcolor(RGB(0, 0, 0));
	rectangle(320, 50, 640, 350);
	bar(321, 51, 639, 349);
	string text = "1.左键单击翻开格子";
	outtextxy(325, 55, text.c_str());
	text = "2.遇到空白说明周围无雷";
	outtextxy(325, 100, text.c_str());
	text = "将自动探索";
	outtextxy(325, 135, text.c_str());
	text = "3.数字表示周围一圈雷数";
	outtextxy(325, 190, text.c_str());
	text = "4.右键单击插旗标记它是雷";
	outtextxy(325, 235, text.c_str());
	text = "5.左键双击数字,若周围标";
	outtextxy(325, 280, text.c_str());
	text = "记数与数字一致自动探索";
	outtextxy(325, 315, text.c_str());
}

界面一获取鼠标

在easyx库中有专门获取鼠标和键盘消息的函数,但我们之后再用,这里使用简单的系统函数,定义一个鼠标消息MOUSEMSG msg

GetMouseMsg会将当前的鼠标消息返回,其成员有x,y坐标,事件类型uMsg

显然,当鼠标消息时间为WM_LBUTTONDOWN左键单击,我们就进行相应的跳转。

对应上面设置的4个按键,简单,中等,困难难度的选择,(x,y)处于不同矩形框内就设置相应的难度和地图大小,前三个按键将跳转至第二界面,通过select()实现,第4个按键设计的是退出键,按下时就flushmessage()清空消息队列,closegraph()关闭图形窗口(即刚刚创建的第一界面),End = 1,记得上面的死循环吗,当end设置1了就会结束,那么我们整个进程就走到头了,实现退出程序的功能。

void getMouse1() { //第1界面获取鼠标
	MOUSEMSG msg = GetMouseMsg();
	//x:61-299,y:5199
	//x:61-299,y:151-199
	//x:61-299,y:251-299
	//x:61-299,y:351-399
	if (msg.uMsg == WM_LBUTTONDOWN) {
		if (msg.x > 60 and msg.x < 300) {
			if (msg.y > 50 and msg.y < 100) {
				msg.uMsg = 0;
				mode = 1;
				ROW = 9;
				COL = 9;
				select();
			} else if (msg.y > 150 and msg.y < 200) {
				msg.uMsg = 0;
				mode = 2;
				ROW = 16;
				COL = 16;
				select();
			} else if (msg.y > 250 and msg.y < 300) {
				msg.uMsg = 0;
				mode = 3;
				ROW = 16;
				COL = 30;
				select();
			} else if (msg.y > 350 and msg.y < 400) { //退出
				flushmessage();
				closegraph();
				End = 1;
			}
		}
	}
}

界面二

第二界面设计了三个按键,直接开始游戏,将按照经典扫雷设置的各难度默认的雷数开始游戏;自定义雷数,由用户输入指定数量的雷数再开始游戏;排行榜,一款单击游戏没有排行榜就似乎失去了灵魂。按键设置和上面一样,创建完后同样需要在一个死循环内获取该界面的鼠标消息,所以设计getMouse2()实现。


void select() {//第2界面
	closegraph();
	flushmessage();
	initgraph(360, 440, EX_DBLCLKS);
	setbkcolor(RGB(192, 192, 192));
	cleardevice();//
	setbkmode(TRANSPARENT);//
	setlinecolor(BLACK);//
	settextstyle(40, 0, "Consolas"); //
	settextcolor(BLACK);
	setfillcolor(RGB(225, 225, 225));

	string text = "开始游戏";
	rectangle(60, 70, 300, 120);
	bar(61, 71, 299, 119);
	outtextxy(61, 71, text.c_str());

	text = "自定义雷数";
	rectangle(60, 170, 300, 220);
	bar(61, 171, 299, 219);
	outtextxy(61, 171, text.c_str());

	text = "排行榜";
	rectangle(60, 270, 300, 320);
	bar(61, 271, 299, 319);
	outtextxy(61, 271, text.c_str());

	while (End == 0) {
		getMouse2();
		Sleep(20);
	}
}

界面二获取鼠标

一样地获取鼠标消息,如果左键单击到相应的坐标区域内,就执行那个按键的跳转。

若为开始游戏按键,就根据当前的难度设置默认雷数,然后play()开始游戏。

若为自定义雷数,就调用setNUM()自定义函数

若为排行榜,就根据当前的难度选择排行榜文件

void getMouse2() { //第2界面获取鼠标
	MOUSEMSG msg = GetMouseMsg();
	if (msg.uMsg == WM_LBUTTONDOWN) {
		msg.uMsg = 0;
		if (msg.x > 60 and msg.x < 300) {
			if (msg.y > 70 and msg.y < 120) { //根据难度选模式
				if (mode == 1) { //小
					NUM = 10;
					play();
				} else if (mode == 2) { //
					NUM = 40;
					play();
				} else if (mode == 3) { //
					NUM = 99;
					play();
				}

			} else if (msg.y > 170 and msg.y < 220) { //自定义雷数
				setNUM();
			} else if (msg.y > 270 and msg.y < 320) { //排行榜
				if (mode == 1) { //小排行榜
					Rank();
				} else if (mode == 2) { //
					Rank();
				} else if (mode == 3) { //
					Rank();
				}
			}
		}
	}
}

自定义雷数函数

首先,自定义雷数显然需要接收一个实时的键盘输入,并且可以打印到图形化窗口上,现在先来构造一个创造输入框的函数

像创建一个按钮一样,利用上面的函数构造一个输入框,每次需要刷新输入框的时候传入字符串文本调用该函数即可,刷新即接收到一个数字输入就调用,以达到实时的。

//更新输入文本框
void inputBox(int x, int y, int w, int h, string text) {
	setlinecolor(BLACK);//按钮边框
	rectangle(x, y, x + w, y + h);
	settextstyle(30, 0, "Consolas"); //文本
	settextcolor(BLACK);
	//按钮填充
	setfillcolor(RGB(225, 225, 225));
	bar(x + 1, y + 1, x + w - 1, y + h - 1);
	outtextxy(x + 7, y + (h - 25) / 2, text.c_str());
}

紧接着就是输入的处理函数

通过系统库中GetAsyncKeyState()获得键盘消息,传入一个ascll码,它将返回一个short,

  1. 返回值表示两个内容,一个是最高位bit的值,代表这个键是否被按下,
  2. 一个是最低位bit的值,代表在上次调用GetAsyncKeyState后,这个键是否被按下。

通过和0x8000与操作,则只得到了最高位的信息,也就是这个键是否按下的0/1,结果正好进行if判断

由于雷数显然是个数字,只需要处理0~9的char字符即可

此外,输入框显然还需要处理退格键,和回车键,将这两个也加上即可,其余ascll码统统不理

char getKey() {
	for (char c = '0'; c <= '9'; ++c) { //获取键盘输入,只处理数字
		if (GetAsyncKeyState(c) & 0x8000)
			return c;
	}
	//退格
	if (GetAsyncKeyState(VK_BACK) & 0x8000) {
		return '\b';
	}
	//回车
	if (GetAsyncKeyState(VK_RETURN) & 0x8000) {
		return '\r';
	}
	//无
	return '\0';
}

然后就是调用这俩函数的函数

上面写的输入框函数是要建立在一个窗口上的,在这个函数中我们建立输入框的窗口

窗口设计好后就调用文本框函数,输入text一开始是空

接下来进行循环,以让用户任意地修改,每轮循环休息100ms归还cpu,如此让电脑休息,并且0.1s才是人的反应速度,如果使用20ms,则在用户抬起手指前,会将一个按键重复地接收数次。如果休息时间过长,则经常在用户输入时处于休眠

设立一个bool值quit,记录回车键是否按下,随时退出循环

设立一个num1记录输入的雷数

通过上面写的getKey()函数,获取键盘输入,如果是退格,那么在输入input不空时就删除最后一位,这可以通过string类型的pop_back()函数直接实现

如果是回车,则quit改1,下次循环判定就会终止

如果是数字,就加到input的最后一位上

由于上面的getKey每次只处理一个按键,所以每接到一个输入,就会接下来调用文本框函数显示出来。

现在需要考虑的是,用户的输入应该有个上下限,因为地图大小是有限的,我在这里设置了雷数应该限定在长x宽的1/10~1/2,

接下来就是把存储了用户输入的string类型的input转换成int类型,string类型中的data()获取字符串的首地址

而系统函数atoi()接收一个一个字符串首地址,并将该字符串数组直接硬译成int类型(非数字部分会直接抹去),如此我不必傻不愣登地写一个for循环去一个个地转义。

那么如果这个数过大,调用windows函数MessageBox()建立一个弹窗,文本为提示即可,并且将input直接清零,这样一来,只要用户输入过大就会直接被清空输入MessageBox()的第一个参数使用GetHWnd(),这会使得我们的提示弹窗将永远处于本程序的第一窗口,如果用户不响应该窗口,进程将一直处于中断。

用户输入过大可以在while循环中实时地检测,但是如果用户输入过小呢,由于我们不知道用户会在何时结束输入,所以输入过小的判断要在循环之外,当接收到了回车,代表输入的结束,此时即可判断是否过小,那么也就是while循环结束我们开始判断,过小一样弹出提示窗口,并清空input。接下来,我们需要重新获取用户的输入,通过在while函数先设立好一个折跃点A:,再在输入过小时调用goto A;即可直接将进程跳转至代码A处继续执行。如果你学C没学过goto进行程序跳转有点白学。

如果输入合理,那么清空消息队列,雷数被设为num1,开始游戏play()

void setNUM() { //自定义雷数
	//设置输入文本框
	flushmessage();
	int w = 350, h = 100;
	initgraph(w, h);
	setbkcolor(WHITE);
	cleardevice();
	setbkmode(TRANSPARENT);//
	settextstyle(25, 0, "楷体"); //
	settextcolor(BLACK);
	string text = "请输入雷数,回车开始游戏";
	outtextxy(5, 20, text.c_str());

	int inputBoxX = 10, inputBoxY = 50;
	int inputBoxW = 320, inputBoxH = 40;
	string input;
	//文本框
	inputBox(inputBoxX, inputBoxY, inputBoxW, inputBoxH, input);

	//循环
A:
	bool quit = 0; //是否回车

	int num1 = 0;
	while (!quit) { //未回车

		//获取键盘
		char key = getKey();
		if (key != '\0') { //不是无
			if (key == '\b') { //是退格
				if (!input.empty()) { //就删input的最后一位
					input.pop_back();
				}
			} else if (key == '\r') { //回车
				quit = 1;
			} else {
				input += key; //是数字就写入
			}
		}
		inputBox(inputBoxX, inputBoxY, inputBoxW, inputBoxH, input); //更新文本框

		const char *str = input.data(); //string转char
		num1 = atoi(str); //char转int
		if (num1 > ROW * COL / 2) {
			MessageBox(GetHWnd(), "雷数过大 \n请小于格子数的1/2", "ERROR", MB_OK + 16);
			input.clear();//清空输入
		}
		NUM = num1;//记录雷数
		//每0.1s接受一次键盘输入,如此人可以反应不至于过快
		Sleep(100);
	}
	if (num1 < ROW * COL / 10) {
		MessageBox(GetHWnd(), "雷数过小 \n请大于格子数的1/10", "ERROR", MB_OK + 16);
		input.clear();//
		goto A;
	}

	//清空所有消息
	flushmessage();
	play();
}

排行榜函数

首先创建一个文件指针wj,创建一个char指针file,由它根据不同的难度选择相应的文件名作为char数组,wj打开file字符串名字的文件

打开方式先是a读写模式,当文件不存在时会直接创建,如此确定一定会有这个文件

再次以r只读模式打开,rewind()确保光标处于文件头,开始读取排行榜中记录的东西,排行榜显然设置不宜过大,记录10条信息即可,排行榜还需要根据大小进行排序,显然在相同的地图下,雷数越多越厉害,耗时越小越厉害,需要进行排序,我们采用构造一种新的数据类型number并利用优先队列排序。

从文件中fscanf读取前十条数据到优先队列进行排序,接下来创建一个排行榜的图形窗口。然后重新w只写模式写入文件,把优先队列中的数据一个个fprintf写入即可

同时为了在窗口上可视化,sprintf_s(string,“%d”,int),将会把" "中的内容拷贝到指定字符串上,再通过easyx库的outtextxy文本函数可视化即可

在排行榜可视化结束后,再额外设置两个按键,为排行榜清空和回到主页,清空排行榜很简单,只需要w只写模式打开即可,它是直接清空文件内容再写。

而跳转回主页需要使用函数外跳转函数,上面提到的goto是函数内跳转方式。这两个按键的功能显然需要监听鼠标消息,我们放到另一个函数changeRank里面实现

//读取排行榜
void Rank() {
	FILE *wj;

	char ch = ' ';
	char *file = &ch;//根据难度选文件
	if (mode == 1) {
		char f[20] = "./rank/small.txt";
		file = f;
	} else if (mode == 2) {
		char f[20] = "./rank/middle.txt";
		file = f;
	} else if (mode == 3) {
		char f[20] = "./rank/big.txt";
		file = f;
	}
	wj = fopen(file, "a");
	fclose(wj);
	wj = fopen(file, "r");
	rewind(wj);
	priority_queue<number> p;
	number x;
	int n, m, s;
	char c;
	for (int i = 0; i < 10; ++i) {//将前十条放入大根堆
		while (!feof(wj)) {
			fscanf(wj, "%d", &n);
			fscanf(wj, "%d", &m);
			fscanf(wj, "%c", &c);
			fscanf(wj, "%d\n", &s);
			x.n = n;
			x.m = m;
			x.s = s;
			p.push(x);
		}
	}
	fclose(wj);

	closegraph();
	initgraph(360, 440, EX_DBLCLKS);
	setbkcolor(RGB(192, 192, 192));
	cleardevice();//背景上色
	setbkmode(TRANSPARENT);//文字背景透明
	setlinecolor(WHITE);//边框颜色
	settextstyle(20, 0, "楷体"); //文字设置
	settextcolor(WHITE);
	//排行榜填充颜色
	setfillcolor(RGB(0, 0, 0));
	rectangle(60, 40, 300, 340);
	bar(61, 41, 299, 339);

	wj = fopen(file, "w"); //写排行榜
	int i = 0;
	while (!p.empty()) {
		x = p.top();
		p.pop();
		if (i == 10)
			break;
		if (x.n >= ROW * COL / 10 and x.n <= ROW * COL / 2 and x.m >= 0 and x.m <= 99 and x.s >= 0 and x.s <= 59) {

			fprintf(wj, "%d %d:%d\n", x.n, x.m, x.s);
			string text;
			char f[30];
			sprintf_s(f, "%d. %d颗雷,用时 %d:%d", i + 1, x.n, x.m, x.s);
			for (int j = 0; j < 30; j++) {
				text += f[j];
			}
			outtextxy(70, 41 + i * 30, text.c_str());
			++i;
		}
	}
	fclose(wj);
	setlinecolor(BLACK);//制作两个按钮
	settextstyle(20, 0, "楷体");
	settextcolor(BLACK);

	setfillcolor(RGB(225, 225, 225));

	rectangle(60, 360, 160, 420);
	rectangle(200, 360, 300, 420);
	bar(61, 361, 159, 419);
	bar(201, 361, 299, 419);
	string text = "文件有误";
	outtextxy(61, 365, text.c_str());
	text = "重置排行榜";
	outtextxy(61, 385, text.c_str());
	text = "回到主页";
	outtextxy(205, 365, text.c_str());
	while (1) {
		changeRank();
		Sleep(20);
	}
}

排行榜监听鼠标

这次监听鼠标我们试着使用easyx库内的函数实现,首先定义一个ExMessage消息类型,其成员message回返回消息的类型。通过库内的函数peekmessage()函数,传入需要获取消息的对象msg,消息类型为EX_MOUSE鼠标类型的消息,该函数会把鼠标的消息计入msg,并返回bool值,对象是否有更新。即如果鼠标消息未更新,那么while循环结束,这个函数也结束了,我们回到排行榜函数那里重新等待下一轮监听。

如果左键创建排行榜根据难度选择重建的文档即可,重建后重新调用排行榜函数以刷新排行榜。如果点击的是回到主页,longjmp(buf, 1)函数将跳转到buf被定义的地方

<setjmp.h>库提供函数外跳转函数,我们在游戏初始化前写一句setjmp(buf)会在该位置写一个跳转点buf,这样回到主页就相当于我们的程序重头执行。

void changeRank() {//重建排行榜
	ExMessage msg;
	while (peekmessage(&msg, EX_MOUSE)) {
		if (msg.message == WM_LBUTTONDOWN) {
			//61, 361, 159, 419
			//201, 361, 299, 419
			if (msg.y > 360 and msg.y < 420) {
				if (msg.x > 60 and msg.x < 160) {	//创建排行榜
					FILE *wj;
					char ch = ' ';
					char *file = &ch; //根据难度选择文件
					if (mode == 1) {
						char f[20] = "./rank/small.txt";
						file = f;
					} else if (mode == 2) {
						char f[20] = "./rank/middle.txt";
						file = f;
					} else if (mode == 3) {
						char f[20] = "./rank/big.txt";
						file = f;
					}
					wj = fopen(file, "w");//重建该文件
					fclose(wj);
					Rank();
				} else if (msg.x > 200 and msg.x < 300) {//回到主页
					closegraph();
					longjmp(buf, 1);
				}
			}
		}
	}
}

排行榜元素类型

上面说到的优先队列,是大根堆,需要使用一种基本数据类型来建立,所以需要重新写一种排行榜元素的数据类型

这个数据类型只需要记录雷数,分钟,秒钟即可,布尔值great是为了记录该排名是不是一个新的排名,默认为0

一个自定义的数据类型建立优先队列是需要重载小于号来实现大小的比较的

显然最重要的是雷数,那么先判断雷数,如果不相等,小于号返回的是正常的小于号即可。雷数相等,那么比较分钟,返回相反的小于号,因为越大的分钟数时间排名越靠后。最后比较秒钟,返回相反的小于号。

struct number {//排名元素
	int n;//雷数
	int m;//分
	int s;//秒
	bool great = 0;
	bool operator<(const number &x) const {//重载比较符
		if (n == x.n) {
			if (m == x.m) {
				return s > x.s;
			}
			return m > x.m;
		}
		return  n < x.n; //
	}
};

游戏开始play()

素材准备

#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")

链接上系统库中的音乐播放的库文件,在项目文件夹中准备好需要的音乐,先直接调用音乐的函数参数close为每一次执行前关闭上一次打开的音乐文件,open打开音乐文件,play播放音乐文件。

接下来准备图片

IMAGE img[13];//图片

IMAGE是esayx库中的类型,通过sprintf_s(fileName, “./source/image/%d.jpg”, i); 将" "中的内容传入字符串fileName

loadimage(img + i, fileName, 40, 40) 将该文件名的图片加载进IMAGE图片数组,图片大小设置为40x40,我们的一个格子大小就这样定义吧。

地图初始化

根据难度确定窗口大小

progress()是展示计数板的函数,在计数板展示后,将未打开的格子图片铺满整个地图,在素材里9.jpg是未翻开的格子图片

visit全局bool二维数组是记录已标记的格子,包含已翻开或已插旗,存储一个标记数组的目的是为了后期的探图

checked全局bool二维数组是记录已翻开的格子,visit与其做差即可得到插旗的格子

map全局int二维数组记录该格子周围一圈8格中含有的雷数,若该格子本身是雷,就是-1。素材中0~8号图片都是对应的数字图片,表示周围的雷数,所以当一个格子被翻开时直接应用map数组存储的int信息转为字符串然后调用该名字的图片文件放到指定位置即可

每次play()即新游戏开始,要将visit,checked,map都初始化,由于小地图是可以囊括在大地图中的,所以只需要设置最大难度16x30的地图,当地图是9x9的简单模式时,visit和checked只对该区域重置为0表示未标记,其他的地方都是1即可

随机埋雷

然后就是随机埋雷,调用srand(time(0))即可,设置一个随时间变化的随机数种子,每次打开游戏的随机数都将不一样,随机数对行列取余得到随机的埋雷位置,如果该位置已埋雷就再找,直到达到设定的雷数都埋好结束。并且每次埋1颗雷,该位置设为-1,且该位置周围一圈的数字+1,除非它也是雷,map数组就这样记录了整个地图信息。

第一次必不触雷

为了保证游戏的可玩性,我们不希望玩家第一次点击就触雷,所以第一次点击如果就是雷就要将其再随机移动到别的不是雷的格子去。

做一个鼠标监听的循环,如果左键单击的位置是雷,那么就先将该格子设为0,重新根据周围的雷数,每存在一颗雷,该位置再+1,且周围不是雷的格子-1。再次调用随机数找个无雷格子设为-1,周围不是雷的格子+1,这样保证了第一次绝不是雷。接着不管是不是第一次点击就触雷,都应该将第一个格子翻开了,found全局变量计数翻开数+1,visit、checked设为1,将该格子放入待翻开的格子队列,调用翻开函数check()翻开。

当雷放好后跳出第一次鼠标的监听循环即可,进入游戏正式开始的鼠标监听环节。

正式游戏

循环监听

每次监听鼠标,调用自定义的click()函数,执行对应的鼠标操作,计数板需要记录的信息是"已插旗子数/雷数",已正确排出的雷数显然不能显示,只应显示玩家自己标记的旗子数,由于鼠标操作后就一定会对计数板产生影响,所以它会产生一个信号表示计数板是否需要更新,然后根据计数板更新信号调用progress()进行更新。

showtime()也是自定义的函数,用于显示当前游戏的时间,传入的参数t0表示游戏开始的系统时间,在前面游戏开局的时候被定义。第2个参数s是上次获取的系统时间的秒数,在showtime中我们将再次获取当前系统时间并赋值给s,两个系统时间果不相同说明时间面板需要更新。

最后检测是否游戏胜利,胜利的方法很好判断,全局变量found如果等于 长x宽-雷数,就说明所有不是雷的地方都被翻过,游戏胜利

//游戏开始
void play() {
	mciSendString("close ./source/music/start.mp3", NULL, 0, NULL);
	mciSendString("open ./source/music/start.mp3", NULL, 0, NULL);
	mciSendString("play ./source/music/start.mp3", NULL, 0, NULL);
	Sleep(200);
	int starty = 80;
	char fileName[30];
	flushmessage();//清空鼠标和键盘消息

	for (int i = 0; i < 13; ++i) { //加载图片
		sprintf_s(fileName, "./source/image/%d.jpg", i);
		loadimage(img + i, fileName, 40, 40);
		//putimage(40 * i, 0, img+i);
	}
	//根据难度选择地图
	closegraph();
	if (mode == 1) {
		initgraph(360, 440, EX_DBLCLKS);
	} else if (mode == 2) {
		initgraph(640, 720, EX_DBLCLKS);
	} else if (mode == 3) {
		initgraph(1200, 720, EX_DBLCLKS);
	}

	setbkcolor(RGB(192, 192, 192));
	cleardevice();//背景上色,计数板打开
	progress();
	//地图初始化
	for (int j = 0; j < COL; ++j) {
		for (int i = 0; i < ROW; ++i) {
			putimage(40 * j, starty + 40 * i, img + 9);
		}
	}

	//重置visit,checked,map
	for (int i = 0; i < 18; ++i) {
		for (int j = 0; j < 32; ++j) {
			visit[i][j] = 1;
			checked[i][j] = 1;
			map[i][j] = 0;
		}
	}
	for (int i = 1; i <= ROW; ++i) {
		for (int j = 1; j <= COL; ++j) {
			visit[i][j] = 0;
			checked[i][j] = 0;
		}
	}

	//随机数埋雷
	srand((unsigned)time(0));
	for (int i = 0; i < NUM;) {
		int r = rand() % ROW + 1; //map多留一圈
		int c = rand() % COL + 1;
		if (map[r][c] != -1) {
			map[r][c] = -1;
			++i;
			for (int i = 0; i < 3; ++i) {
				for (int j = 0; j < 3; ++j) {
					if (map[r - 1 + i][c - 1 + j] != -1) {
						map[r - 1 + i][c - 1 + j] += 1;
					}
				}
			}
		}
	}
	t0 = clock();//开始时间
	int s = 0;

	//第一次点击保证不能触雷
	while (1) {
		s = showtime(t0, s);
		ExMessage msg;
		while (peekmessage(&msg, EX_MOUSE)) {
			if (msg.y < 80)
				break;
			int c = msg.x / 40 + 1;
			int r = (msg.y - 80) / 40 + 1;

			if (msg.message == WM_LBUTTONDOWN) {//左键单击?
				mciSendString("close ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("open ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("play ./source/music/click.wav", NULL, 0, NULL);
				if (map[r][c] == -1) { //如果触雷
					map[r][c] = 0; //原位置=0
					for (int i = 0; i < 3; ++i) { //周围不为雷的地方-1
						for (int j = 0; j < 3; ++j) {
							if (i == 1 and j == 1) {
								continue;
							}
							if (map[r - 1 + i][c - 1 + j] == -1) {//周围有一颗雷原位置+1
								++map[r][c];
							} else {
								--map[r - 1 + i][c - 1 + j];
							}
						}
					}
					//随机移动到不是雷的地方
					while (1) {
						int r = rand() % ROW + 1;
						int c = rand() % COL + 1;
						if (map[r][c] != -1) {	//找到不是雷的地方
							map[r][c] = -1; //改为雷
							for (int i = 0; i < 3; ++i) { //周围不是雷的地方+1
								for (int j = 0; j < 3; ++j) {
									if (map[r - 1 + i][c - 1 + j] != -1) {
										map[r - 1 + i][c - 1 + j] += 1;
									}
								}
							}
							break;
						}
					}
				}

				//进行一步翻开
				pair<int, int> p;
				p.first = r;
				p.second = c;
				q.push(p);
				check();//结束后即可跳走开始游戏
				goto B;
			}
		}
		Sleep(20);
	}
B:
	while (1) {
		click();
		if (prog == 1) {//计数板更新信号
			prog = 0;
			progress();
		}
		s = showtime(t0, s); //获取时间
		win();
		Sleep(20);//归还cpu
	}
	End = 1;
}

计数板progress()

计数板记录"旗子数/雷数",直接使用outtextxy()在指定位置展示字符串即可,当然需要先定义一个全局变量mark记录旗子数。这个字符串需要将mark、NUM这两个全局变量进行int转字符串操作

void progress() { //更新计数板
	setbkmode(TRANSPARENT);//文字背景透明
	settextstyle(60, 0, "Consolas"); //
	settextcolor(BLACK);
	setfillcolor(RGB(192, 192, 192));
	bar(0, 0, COL * 40, 79);
	string text;
	char m[4];//int转string
	sprintf_s(m, "%d", mark);
	for (int i = 0; i < 4; ++i) {
		if (m[i] == '\0')
			break;
		text += m[i];
	}
	text += "|";
	char c[4];//int转string
	sprintf_s(c, "%d", NUM);
	for (int i = 0; i < 4; ++i) {
		text += c[i];
	}
	outtextxy(40, 10, text.c_str());
}

展示时间showtime()

获取当前的系统时间减去游戏开始的时间t0得到游戏时间,并转换成 分钟:秒钟,如果分钟数超过99,则游戏时间有点过长了,完全可以直接中断(其实是因为显示不下了)。发布一个窗口提示,并将End设为1,窗口只有当用户点击后才重新开始进程,将会直接跳转至buf处,也就是初始化的地方,然后由于End被设为了1,init()初始化函数将会走完,彻底结束进程。

接着就是判断时间面板是否需要更新,因为我们没有必要时刻地刷新时间板,只需要每次调用该函数的时候,传入上次传出的时间,与本次时间相比较,一致则无需更新。

int  showtime(int t0, int change) {//展示时间
	int t = (clock() - t0) / CLOCKS_PER_SEC;
	int m = t / 60;
	int s = t % 60;
	if (m > 99) {
		MessageBox(GetHWnd(), "时间过长,结束游戏", "DEFEAT", MB_OK + 16);
		closegraph();
		End = 1;
		longjmp(buf, 1);
	}
	//如果秒数与上次调用发生改变就更新时间
	if (s != change) {
		char c[20];
		string text;
		sprintf_s(c, "%d:%d", m, s);//int转string
		for (int i = 0; i < 20; ++i) {
			if (c[i] == '\0')
				break;
			text += c[i];
		}
		progress();
		outtextxy(200, 10, text.c_str());
	}
	return s;
}

鼠标监听click()

扫雷的鼠标操作有3个,左键单击是翻开,右键单击是插旗,左键双击是自动探索

peekmessage()进行实时的获取鼠标消息,如果鼠标无新操作会直接返回假,此时可以sleep让cpu休息。

如果是左键一个未翻开格子,即checked的位置为0,就可以翻开,将该格子坐标作为一个pair放入全局队列,并调用翻开函数check()处理该队列。且播放自己存的点击操作的音乐

如果是右键一个未翻开格子,播放插旗音乐,且没被插旗子,即visit位置为0,就可以插旗子,并且mark旗子数+1,prog即计数板需要更新信号置为1,然后图形化窗口的该位置改为旗子的图片。如果已被插了旗子,则此次右键是取消旗子,mark-1,prog=1,图片改成格子

如果是双击,双击必须是已翻开的数字格或空白(空白即为0),然后进行自动探索,即如果周围的旗子数和格子中的数字相等,则请计算机直接快速地帮我们连续翻开那些我们自认为不是雷的格子。此时需要判断该格子周围的旗子数,将周围一圈的未翻开且被标记的格子计数即可,如果相等,则将周围一圈未翻开且不是旗子的格子加入队列,等待check()处理。

void click() { //鼠标操作
	ExMessage msg;
	while (peekmessage(&msg, EX_MOUSE)) {//当鼠标无新操作时会归还cpu
		if (msg.y < 80)
			break;
		int c = msg.x / 40 + 1; //鼠标坐标转换map坐标
		int r = (msg.y - 80) / 40 + 1;
		//如果没翻开
		if (checked[r][c] == 0) { //左键
			if (msg.message == WM_LBUTTONDOWN) { //翻开
				//将上次播放的音乐文件关闭,再打开,再播放
				mciSendString("close ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("open ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("play ./source/music/click.wav", NULL, 0, NULL);
				pair<int, int> p;
				p.first = r;
				p.second = c;
				q.push(p);
				check();//执行一步翻开操作
			} else if (msg.message == WM_RBUTTONDOWN) { //右键
				mciSendString("close ./source/music/rightClick.wav", NULL, 0, NULL);
				mciSendString("open ./source/music/rightClick.wav", NULL, 0, NULL);
				mciSendString("play ./source/music/rightClick.wav", NULL, 0, NULL);
				if (visit[r][c] == 0) { //如果没插旗子就插
					prog = 1;
					++mark;
					visit[r][c] = 1;
					putimage((c - 1) * 40, (r - 1) * 40 + 80, img + 10);
				} else if (visit[r][c] == 1) { //插了就取消
					prog = 1;
					--mark;
					visit[r][c] = 0;
					putimage((c - 1) * 40, (r - 1) * 40 + 80, img + 9);
				}
			}
		}
		//双击
		else if (map[r][c] > 0 and map[r][c] < 9) {
			if (msg.message == WM_LBUTTONDBLCLK) {
				mciSendString("close ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("open ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("play ./source/music/click.wav", NULL, 0, NULL);
				//只有当已翻开且为数字的格子被双击才自动探索
				//且需要保证周围棋子数等于数字
				int flag = 0;
				for (int i = 0; i < 3; ++i) {
					for (int j = 0; j < 3; ++j) { //数旗子,它是未翻开且被标记的格子
						if (checked[r - 1 + i][c - 1 + j] == 0 and visit[r - 1 + i][c - 1 + j] == 1) {
							++flag;
						}
					}
				}
				if (flag == map[r][c]) {
					for (int i = 0; i < 3; ++i) {
						for (int j = 0; j < 3; ++j) {
							if (visit[r - 1 + i][c - 1 + j] == 0) {//如果它没被标记,翻开
								pair<int, int> p;
								p.first = r - 1 + i;
								p.second = c - 1 + j;
								q.push(p);
								check();
							}
						}
					}
				}
			}
		}
	}
}

翻开操作check()

翻开操作显然是需要进行遍历探索的,当周围安全时,应该交由计算机帮我们直接把周围格子也继续进行翻开,使用广度搜索是好的,

所以会需要全局的队列存下接下来需要翻开的格子,同样也需要一个标记数组visit表示该格子已被访问(已翻开)或无需访问(已插旗)

优先判断触雷,触雷就播放炸弹音乐,进入defeat失败函数

如果可以继续,则found已翻开数+1,checked和visit都置为1,图形化窗口该位置放上相应的图片。同样的,由于翻开会得到该格子周围的雷数信息,那么计算机也应该帮我们继续探索安全的区域,即如果周围旗子数等于该格数字就将周围一圈也加入探索队列,当然,如果玩家的旗子插错了,自以为是地认为一些地方是雷插错了旗子,那么自动探索就会翻开触雷的格子。真的安全的话,就会将许多格子加入队列,然后继续在while循环一个个翻开,直到队列为空

自动探索每次触发都应该在探索结束后播放音乐,游戏效果嘛,所以播放音乐不能在每次探索时都播放,一但触发了自动探索就需要发出一个音乐应播放的信号,在while循环之外再播放一次即可。

bool searchMusic = 0;
void check() { //这是一步翻开操作,如果遇到空白将接着翻开四周
	//使用广度搜索
	while (!q.empty()) {	//如果还有格子需要探索
		pair<int, int> p = q.front();
		q.pop();
		int r = p.first;
		int c = p.second;
		//如果已标记,无需处理
		if (visit[r][c] == 1) {
			continue;
		}

		//优先处理触雷
		if (map[r][c] == -1) {
			mciSendString("close ./source/music/boom.wav", NULL, 0, NULL);
			mciSendString("open ./source/music/boom.wav", NULL, 0, NULL);
			mciSendString("play ./source/music/boom.wav", NULL, 0, NULL);
			visit[r][c] = 1;
			defeat(r, c);
			return;
		}
		//翻开
		putimage((c - 1) * 40, (r - 1) * 40 + 80, img + map[r][c]);
		++found;
		visit[r][c] = 1;
		checked[r][c] = 1。
		if (map[r][c] >= 0 and map[r][c] < 9) {//周围探索
			//只有当周围旗子数等于数字才自动探索
			int flag = 0;
			for (int i = 0; i < 3; ++i) {
				for (int j = 0; j < 3; ++j) { //数旗子,它是未翻开且被标记的格子
					if (checked[r - 1 + i][c - 1 + j] == 0 and visit[r - 1 + i][c - 1 + j] == 1) {
						++flag;
					}
				}
			}
			if (flag == map[r][c]) {
				for (int i = 0; i < 3; ++i) {
					for (int j = 0; j < 3; ++j) {
						if (visit[r - 1 + i][c - 1 + j] == 0) {//如果它没被标记,翻开
							pair<int, int> p;
							p.first = r - 1 + i;
							p.second = c - 1 + j;
							q.push(p);
							searchMusic = 1;
						}
					}
				}
			}
		}
	}
	if (searchMusic == 1) {
		mciSendString("close ./source/music/search.wav", NULL, 0, NULL);
		mciSendString("open ./source/music/search.wav", NULL, 0, NULL);
		mciSendString("play ./source/music/search.wav", NULL, 0, NULL);
		searchMusic = 0;
	}
}

游戏失败defeat()

将所有未插旗且为雷的格子换为雷的图片,以显示所有未被找到的雷

然后将触雷的格子换成触雷的图片,提示玩家是在该处触雷

最后发布一个失败的提示窗口,将进程中断

当用户回应时就回到buf处,游戏的主页

void defeat(int r, int c) { //失败
	for (int i = 1; i <= ROW; ++i) {//显示所有未被找到的雷
		for (int j = 1; j <= COL; ++j) {
			if (visit[i][j] == 0 and map[i][j] == -1) {
				putimage((j - 1) * 40, (i - 1) * 40 + 80, img + 11);
			}
		}
	}

	putimage((c - 1) * 40, (r - 1) * 40 + 80, img + 12);
	MessageBox(GetHWnd(), "你输了", "DEFEAT", MB_OK + 16);
	closegraph();
	longjmp(buf, 1);
}

游戏胜利win()

当游戏胜利后,需要调用写排行榜函数wRank()判断是否需要将本次记录写入排行榜

然后发布一个胜利的提示窗口,将进程中断

当用户回应时就回到buf处,游戏的主页

void win() { //获胜
	if (found == ROW * COL - NUM) {
		wRank();
		MessageBox(GetHWnd(), "你赢了", "WIN", MB_OK);
		closegraph();
		longjmp(buf, 1);
	}
}

写排行榜wRank()

获取了游戏时间后先根据难度选择需要打开文件名

a模式确定文件存在,如果不存在会新建,存在则不改写

r模式只读,将其中的记录和本次记录加入优先队列进行排序,并且本次记录number的great成员置为1,接下来写入文件如果写入遇到great为1的记录,则说明本次记录上榜了,可以发布一个窗口提示玩家

w模式只写,清空文件后将优先队列中的记录依次从大到小写入文件,如果great为1,则说明排行榜被更新,可以提示玩家

关闭文件

void wRank() {//胜利后写排行
	int t = (clock() - t0) / CLOCKS_PER_SEC; //获取当前时间
	int mnew = t / 60;
	int snew = t % 60;
	FILE *wj;
	char ch = ' ';
	char *file = &ch;//根据难度选择文件
	if (mode == 1) {
		char f[20] = "./rank/small.txt";
		file = f;
	} else if (mode == 2) {
		char f[20] = "./rank/middle.txt";
		file = f;
	} else if (mode == 3) {
		char f[20] = "./rank/big.txt";
		file = f;
	}

	wj = fopen(file, "a");//确保文件存在
	fclose(wj);
	wj = fopen(file, "r"); //读文件
	rewind(wj);
	int n, m, s;
	char c;
	priority_queue<number> p;
	number x;
	x.n = NUM;
	x.m = mnew;
	x.s = snew;
	x.great = 1;//新记录标记
	p.push(x);
	for (int i = 0; i < 10; ++i) {
		while (!feof(wj)) {//读取最多10个排名
			fscanf(wj, "%d", &n);
			fscanf(wj, "%d", &m);
			fscanf(wj, "%c", &c);
			fscanf(wj, "%d\n", &s);

			x.n = n;
			x.m = m;
			x.s = s;
			x.great = 0;
			p.push(x);
		}
	}
	fclose(wj);
	wj = fopen(file, "w"); //写文件
	int i = 0;
	while (!p.empty()) {//通过大根堆写入最高的10个排名
		if (i == 10)
			break;
		++i;
		x = p.top();
		p.pop();
		fprintf(wj, "%d %d:%d\n", x.n, x.m, x.s);
		if (x.great == 1) {//如果本次排名可以上榜前十
			MessageBox(GetHWnd(), "排行榜已更新", "上榜", MB_OK);
			x.great = 0;
		}
	}
	fclose(wj);
}

静态编译

由于需要可移植性,在未安装这些库的用户电脑上也可以游玩该程序,需要进行静态编译。

在vs中选择MFC并选择Release发布模式

字符集使用多字节字符集

预处理器定义中输入_CRT_SECURE_NO_WARNINGS,用来让程序信任打开文件

链接器中需要在输入的附加依赖项里加上user32.lib系统库,获取鼠标键盘监听的库文件

最后编译运行即可在项目中找到静态编译的可执行文件,在任一一台未配置c语言的电脑都将支持运行

在这里插入图片描述

windows窗口程序

主函数使用窗口程序主函数代替main函数,这样程序执行时将不会调用终端窗口

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {}

附源代码

至此所有的代码都解释完毕,项目非常简单,综合很多c的基本知识,适合练手,附完整代码

#include <graphics.h>
#include <conio.h>
#include <windows.h>
#include <iostream>
#include <time.h>
#include <queue>
using namespace std;

#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")
#include <setjmp.h>

#pragma comment(lib, "Shell32.lib")

static jmp_buf buf;

int ROW = 9, COL = 9;//行与列
int NUM = 0;         //雷数
int mark = 0;         //旗子数
int found = 0;        //翻开格子数,用于判断胜利
bool End = 0;
bool prog = 0; //更新计数板信号
int t0;//开始时间

//简单:9x9-10,中等:16x16-40,困难:30x16-99

int map[18][32] = { 0 };//地图,由于大地图可以包含小地图,所以只用一个
int mode = 1;//游戏难度

bool visit[18][32] = { 0 }; //已标记或翻开
bool checked[18][32] = { 0 }; //已翻开
IMAGE img[13];//图片


void rule() {

	//setbkmode(TRANSPARENT);//文字背景透明
	setlinecolor(WHITE);//边框颜色
	settextstyle(25, 0, "楷体"); //文字设置
	settextcolor(WHITE);
	//排行榜填充颜色
	setfillcolor(RGB(0, 0, 0));
	rectangle(320, 50, 640, 350);
	bar(321, 51, 639, 349);
	string text = "1.左键单击翻开格子";
	outtextxy(325, 55, text.c_str());
	text = "2.遇到空白说明周围无雷";
	outtextxy(325, 100, text.c_str());
	text = "将自动探索";
	outtextxy(325, 135, text.c_str());
	text = "3.数字表示周围一圈雷数";
	outtextxy(325, 190, text.c_str());
	text = "4.右键单击插旗标记它是雷";
	outtextxy(325, 235, text.c_str());
	text = "5.左键双击数字,若周围标";
	outtextxy(325, 280, text.c_str());
	text = "记数与数字一致自动探索";
	outtextxy(325, 315, text.c_str());

}

struct number {//排名元素
	int n;//雷数
	int m;//分
	int s;//秒
	bool great = 0;
	bool operator<(const number &x) const {//重载比较符
		if (n == x.n) {
			if (m == x.m) {
				return s > x.s;
			}
			return m > x.m;
		}
		return  n < x.n; //
	}
};
void Rank();
void changeRank() {//重建排行榜
	ExMessage msg;
	while (peekmessage(&msg, EX_MOUSE)) {
		if (msg.message == WM_LBUTTONDOWN) {
			//61, 361, 159, 419
			//201, 361, 299, 419
			if (msg.y > 360 and msg.y < 420) {
				if (msg.x > 60 and msg.x < 160) {	//创建排行榜
					FILE *wj;
					char ch = ' ';
					char *file = &ch; //根据难度选择文件
					if (mode == 1) {
						char f[20] = "./rank/small.txt";
						file = f;
					} else if (mode == 2) {
						char f[20] = "./rank/middle.txt";
						file = f;
					} else if (mode == 3) {
						char f[20] = "./rank/big.txt";
						file = f;
					}
					wj = fopen(file, "w");//重建该文件
					fclose(wj);
					Rank();
				} else if (msg.x > 200 and msg.x < 300) {//回到主页
					closegraph();
					longjmp(buf, 1);
				}
			}
		}
	}
}

void wRank() {//胜利后写排行
	int t = (clock() - t0) / CLOCKS_PER_SEC; //获取当前时间
	int mnew = t / 60;
	int snew = t % 60;
	FILE *wj;
	char ch = ' ';
	char *file = &ch;//根据难度选择文件
	if (mode == 1) {
		char f[20] = "./rank/small.txt";
		file = f;
	} else if (mode == 2) {
		char f[20] = "./rank/middle.txt";
		file = f;
	} else if (mode == 3) {
		char f[20] = "./rank/big.txt";
		file = f;
	}

	wj = fopen(file, "a");//确保文件存在
	fclose(wj);
	wj = fopen(file, "r"); //读文件
	rewind(wj);
	int n, m, s;
	char c;
	priority_queue<number> p;
	number x;
	x.n = NUM;
	x.m = mnew;
	x.s = snew;
	x.great = 1;//新记录标记
	p.push(x);
	for (int i = 0; i < 10; ++i) {
		while (!feof(wj)) {//读取最多10个排名
			fscanf(wj, "%d", &n);
			fscanf(wj, "%d", &m);
			fscanf(wj, "%c", &c);
			fscanf(wj, "%d\n", &s);

			x.n = n;
			x.m = m;
			x.s = s;
			x.great = 0;
			p.push(x);
		}
	}
	fclose(wj);
	wj = fopen(file, "w"); //写文件
	int i = 0;
	while (!p.empty()) {//通过大根堆写入最高的10个排名
		if (i == 10)
			break;
		++i;
		x = p.top();
		p.pop();
		fprintf(wj, "%d %d:%d\n", x.n, x.m, x.s);
		if (x.great == 1) {//如果本次排名可以上榜前十
			MessageBox(GetHWnd(), "排行榜已更新", "上榜", MB_OK);
			x.great = 0;
		}
	}

	fclose(wj);
}

//读取排行榜
void Rank() {
	FILE *wj;

	char ch = ' ';
	char *file = &ch;//根据难度选文件
	if (mode == 1) {
		char f[20] = "./rank/small.txt";
		file = f;
	} else if (mode == 2) {
		char f[20] = "./rank/middle.txt";
		file = f;
	} else if (mode == 3) {
		char f[20] = "./rank/big.txt";
		file = f;
	}
	wj = fopen(file, "a");
	fclose(wj);
	wj = fopen(file, "r");
	rewind(wj);
	priority_queue<number> p;
	number x;
	int n, m, s;
	char c;
	for (int i = 0; i < 10; ++i) {//将前十条放入大根堆
		while (!feof(wj)) {
			fscanf(wj, "%d", &n);
			fscanf(wj, "%d", &m);
			fscanf(wj, "%c", &c);
			fscanf(wj, "%d\n", &s);
			x.n = n;
			x.m = m;
			x.s = s;
			p.push(x);
		}
	}
	fclose(wj);

	closegraph();
	initgraph(360, 440, EX_DBLCLKS);
	setbkcolor(RGB(192, 192, 192));
	cleardevice();//背景上色
	setbkmode(TRANSPARENT);//文字背景透明
	setlinecolor(WHITE);//边框颜色
	settextstyle(20, 0, "楷体"); //文字设置
	settextcolor(WHITE);
	//排行榜填充颜色
	setfillcolor(RGB(0, 0, 0));
	rectangle(60, 40, 300, 340);
	bar(61, 41, 299, 339);

	wj = fopen(file, "w"); //写排行榜
	int i = 0;
	while (!p.empty()) {
		x = p.top();
		p.pop();
		if (i == 10)
			break;
		if (x.n >= ROW * COL / 10 and x.n <= ROW * COL / 2 and x.m >= 0 and x.m <= 99 and x.s >= 0 and x.s <= 59) {

			fprintf(wj, "%d %d:%d\n", x.n, x.m, x.s);
			string text;
			char f[30];
			sprintf_s(f, "%d. %d颗雷,用时 %d:%d", i + 1, x.n, x.m, x.s);
			for (int j = 0; j < 30; j++) {
				text += f[j];
			}
			outtextxy(70, 41 + i * 30, text.c_str());
			++i;
		}
	}
	fclose(wj);
	setlinecolor(BLACK);//制作两个按钮
	settextstyle(20, 0, "楷体");
	settextcolor(BLACK);

	setfillcolor(RGB(225, 225, 225));

	rectangle(60, 360, 160, 420);
	rectangle(200, 360, 300, 420);
	bar(61, 361, 159, 419);
	bar(201, 361, 299, 419);
	string text = "文件有误";
	outtextxy(61, 365, text.c_str());
	text = "重置排行榜";
	outtextxy(61, 385, text.c_str());
	text = "回到主页";
	outtextxy(205, 365, text.c_str());
	while (1) {
		changeRank();
		Sleep(20);
	}
}

void win() { //获胜

	if (found == ROW * COL - NUM) {
		wRank();
		MessageBox(GetHWnd(), "你赢了", "WIN", MB_OK);
		closegraph();
		longjmp(buf, 1);
	}
}

void defeat(int r, int c) { //失败
	for (int i = 1; i <= ROW; ++i) {//显示所有未被找到的雷
		for (int j = 1; j <= COL; ++j) {
			if (visit[i][j] == 0 and map[i][j] == -1) {
				putimage((j - 1) * 40, (i - 1) * 40 + 80, img + 11);
			}
		}
	}

	putimage((c - 1) * 40, (r - 1) * 40 + 80, img + 12);
	MessageBox(GetHWnd(), "你输了", "DEFEAT", MB_OK + 16);
	closegraph();
	longjmp(buf, 1);
}

queue<pair<int, int>> q;
bool searchMusic = 0;
void check() { //这是一步翻开操作,如果遇到空白将接着翻开四周
	//使用广度搜索
	while (!q.empty()) {	//如果还有格子需要探索
		pair<int, int> p = q.front();
		q.pop();
		int r = p.first;
		int c = p.second;
		//如果已标记,无需处理
		if (visit[r][c] == 1) {
			continue;
		}

		//优先处理触雷
		if (map[r][c] == -1) {
			mciSendString("close ./source/music/boom.wav", NULL, 0, NULL);
			mciSendString("open ./source/music/boom.wav", NULL, 0, NULL);
			mciSendString("play ./source/music/boom.wav", NULL, 0, NULL);
			visit[r][c] = 1;
			defeat(r, c);
			return;
		}
		//翻开
		putimage((c - 1) * 40, (r - 1) * 40 + 80, img + map[r][c]);
		++found;
		visit[r][c] = 1;
		checked[r][c] = 1;
		if (map[r][c] >= 0 and map[r][c] < 9) {//周围探索
			//只有当周围旗子数等于数字才自动探索
			int flag = 0;
			for (int i = 0; i < 3; ++i) {
				for (int j = 0; j < 3; ++j) { //数旗子,它是未翻开且被标记的格子
					if (checked[r - 1 + i][c - 1 + j] == 0 and visit[r - 1 + i][c - 1 + j] == 1) {
						++flag;
					}
				}
			}
			if (flag == map[r][c]) {
				for (int i = 0; i < 3; ++i) {
					for (int j = 0; j < 3; ++j) {
						if (visit[r - 1 + i][c - 1 + j] == 0) {//如果它没被标记,翻开
							pair<int, int> p;
							p.first = r - 1 + i;
							p.second = c - 1 + j;
							q.push(p);
							searchMusic = 1;
						}
					}
				}
			}
		}
	}
	if (searchMusic == 1) {

		mciSendString("close ./source/music/search.wav", NULL, 0, NULL);
		mciSendString("open ./source/music/search.wav", NULL, 0, NULL);
		mciSendString("play ./source/music/search.wav", NULL, 0, NULL);
		searchMusic = 0;
	}
}

void click() { //鼠标操作
	ExMessage msg;
	while (peekmessage(&msg, EX_MOUSE)) {//当鼠标无新操作时会归还cpu
		if (msg.y < 80)
			break;
		int c = msg.x / 40 + 1; //鼠标坐标转换map坐标
		int r = (msg.y - 80) / 40 + 1;
		//如果没翻开
		if (checked[r][c] == 0) { //左键
			if (msg.message == WM_LBUTTONDOWN) { //翻开
				//将上次播放的音乐文件关闭,再打开,再播放
				mciSendString("close ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("open ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("play ./source/music/click.wav", NULL, 0, NULL);
				pair<int, int> p;
				p.first = r;
				p.second = c;
				q.push(p);
				check();//执行一步翻开操作
			} else if (msg.message == WM_RBUTTONDOWN) { //右键
				mciSendString("close ./source/music/rightClick.wav", NULL, 0, NULL);
				mciSendString("open ./source/music/rightClick.wav", NULL, 0, NULL);
				mciSendString("play ./source/music/rightClick.wav", NULL, 0, NULL);
				if (visit[r][c] == 0) { //如果没插旗子就插
					prog = 1;
					++mark;
					visit[r][c] = 1;
					putimage((c - 1) * 40, (r - 1) * 40 + 80, img + 10);
				} else if (visit[r][c] == 1) { //插了就取消
					prog = 1;
					--mark;
					visit[r][c] = 0;
					putimage((c - 1) * 40, (r - 1) * 40 + 80, img + 9);
				}
			}
		}
		//双击
		else if (map[r][c] > 0 and map[r][c] < 9) {
			if (msg.message == WM_LBUTTONDBLCLK) {
				mciSendString("close ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("open ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("play ./source/music/click.wav", NULL, 0, NULL);
				//只有当已翻开且为数字的格子被双击才自动探索
				//且需要保证周围棋子数等于数字
				int flag = 0;
				for (int i = 0; i < 3; ++i) {
					for (int j = 0; j < 3; ++j) { //数旗子,它是未翻开且被标记的格子
						if (checked[r - 1 + i][c - 1 + j] == 0 and visit[r - 1 + i][c - 1 + j] == 1) {
							++flag;
						}
					}
				}
				if (flag == map[r][c]) {
					for (int i = 0; i < 3; ++i) {
						for (int j = 0; j < 3; ++j) {
							if (visit[r - 1 + i][c - 1 + j] == 0) {//如果它没被标记,翻开
								pair<int, int> p;
								p.first = r - 1 + i;
								p.second = c - 1 + j;
								q.push(p);
								check();
							}
						}
					}
				}
			}
		}
	}
}



void progress() { //更新计数板
	setbkmode(TRANSPARENT);//文字背景透明
	settextstyle(60, 0, "Consolas"); //
	settextcolor(BLACK);


	setfillcolor(RGB(192, 192, 192));
	bar(0, 0, COL * 40, 79);
	string text;

	char m[4];//int转string
	sprintf_s(m, "%d", mark);
	for (int i = 0; i < 4; ++i) {
		if (m[i] == '\0')
			break;
		text += m[i];
	}

	text += "|";

	char c[4];//int转string
	sprintf_s(c, "%d", NUM);
	for (int i = 0; i < 4; ++i) {
		text += c[i];
	}

	outtextxy(40, 10, text.c_str());


}

int  showtime(int t0, int change) {//展示时间
	int t = (clock() - t0) / CLOCKS_PER_SEC;
	int m = t / 60;
	int s = t % 60;
	if (m > 99) {
		MessageBox(GetHWnd(), "时间过长,结束游戏", "DEFEAT", MB_OK + 16);
		closegraph();
		End = 1;
		longjmp(buf, 1);
	}
	//如果秒数与上次调用发生改变就更新时间
	if (s != change) {
		char c[20];
		string text;
		sprintf_s(c, "%d:%d", m, s);//int转string
		for (int i = 0; i < 20; ++i) {
			if (c[i] == '\0')
				break;
			text += c[i];
		}
		progress();
		outtextxy(200, 10, text.c_str());
	}
	return s;
}

//游戏开始
void play() {
	mciSendString("close ./source/music/start.mp3", NULL, 0, NULL);
	mciSendString("open ./source/music/start.mp3", NULL, 0, NULL);
	mciSendString("play ./source/music/start.mp3", NULL, 0, NULL);
	Sleep(200);
	int starty = 80;
	char fileName[30];
	flushmessage();//清空鼠标和键盘消息

	for (int i = 0; i < 13; ++i) { //加载图片
		sprintf_s(fileName, "./source/image/%d.jpg", i);
		loadimage(img + i, fileName, 40, 40);
		//putimage(40 * i, 0, img+i);
	}
	//根据难度选择地图
	closegraph();
	if (mode == 1) {
		initgraph(360, 440, EX_DBLCLKS);
	} else if (mode == 2) {
		initgraph(640, 720, EX_DBLCLKS);
	} else if (mode == 3) {
		initgraph(1200, 720, EX_DBLCLKS);
	}

	setbkcolor(RGB(192, 192, 192));
	cleardevice();//背景上色,计数板打开
	progress();
	//地图初始化
	for (int j = 0; j < COL; ++j) {
		for (int i = 0; i < ROW; ++i) {
			putimage(40 * j, starty + 40 * i, img + 9);
		}
	}

	//重置visit,checked,map
	for (int i = 0; i < 18; ++i) {
		for (int j = 0; j < 32; ++j) {
			visit[i][j] = 1;
			checked[i][j] = 1;
			map[i][j] = 0;
		}
	}
	for (int i = 1; i <= ROW; ++i) {
		for (int j = 1; j <= COL; ++j) {
			visit[i][j] = 0;
			checked[i][j] = 0;
		}
	}


	//随机数埋雷
	srand((unsigned)time(0));
	for (int i = 0; i < NUM;) {
		int r = rand() % ROW + 1; //map多留一圈
		int c = rand() % COL + 1;
		if (map[r][c] != -1) {
			map[r][c] = -1;
			++i;
			//测试
			//putimage(40*(c-1),starty+40*(r-1),img+11);

			for (int i = 0; i < 3; ++i) {
				for (int j = 0; j < 3; ++j) {
					if (map[r - 1 + i][c - 1 + j] != -1) {
						map[r - 1 + i][c - 1 + j] += 1;
						//测试
						//putimage(40*(c-2+j),starty+40*(r-2+i),img+map[r-1+i][c-1+j]);

					}
				}
			}
		}
	}
	t0 = clock();//开始时间
	int s = 0;

	//第一次点击保证不能触雷
	while (1) {
		s = showtime(t0, s);
		ExMessage msg;
		while (peekmessage(&msg, EX_MOUSE)) {
			if (msg.y < 80)
				break;
			int c = msg.x / 40 + 1;
			int r = (msg.y - 80) / 40 + 1;

			if (msg.message == WM_LBUTTONDOWN) {//左键单击?
				mciSendString("close ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("open ./source/music/click.wav", NULL, 0, NULL);
				mciSendString("play ./source/music/click.wav", NULL, 0, NULL);
				if (map[r][c] == -1) { //如果触雷
					map[r][c] = 0; //原位置=0
					for (int i = 0; i < 3; ++i) { //周围不为雷的地方-1
						for (int j = 0; j < 3; ++j) {
							if (i == 1 and j == 1) {
								continue;
							}
							if (map[r - 1 + i][c - 1 + j] == -1) {//周围有一颗雷原位置+1
								++map[r][c];
							} else {
								--map[r - 1 + i][c - 1 + j];
							}
						}
					}
					//随机移动到不是雷的地方
					while (1) {
						int r = rand() % ROW + 1;
						int c = rand() % COL + 1;
						if (map[r][c] != -1) {	//找到不是雷的地方
							map[r][c] = -1; //改为雷
							for (int i = 0; i < 3; ++i) { //周围不是雷的地方+1
								for (int j = 0; j < 3; ++j) {
									if (map[r - 1 + i][c - 1 + j] != -1) {
										map[r - 1 + i][c - 1 + j] += 1;
									}
								}
							}
							break;
						}
					}
				}

				//进行一步翻开
				pair<int, int> p;
				p.first = r;
				p.second = c;
				q.push(p);
				check();//结束后即可跳走开始游戏
				goto B;
			}
		}
		Sleep(20);
	}
B:
	while (1) {
		click();
		if (prog == 1) {//计数板更新信号
			prog = 0;
			progress();
		}
		s = showtime(t0, s); //获取时间
		win();
		Sleep(20);//归还cpu
	}

	//检查
	/*
	for(int i=1;i<=ROW;i++){
		for(int j=1;j<=COL;j++){
			if(map[i][j]>=0 and map[i][j]<=8)
			putimage(40*(j-1),80+40*(i-1),img+map[i][j]);
			else if(map[i][j]==-1)
			putimage(40*(j-1),80+40*(i-1),img+11);
		}
	}*/
	End = 1;
}

void setNUM();
void getMouse2() { //第2界面获取鼠标
	MOUSEMSG msg = GetMouseMsg();
	if (msg.uMsg == WM_LBUTTONDOWN) {
		msg.uMsg = 0;
		if (msg.x > 60 and msg.x < 300) {
			if (msg.y > 70 and msg.y < 120) { //根据难度选模式
				if (mode == 1) { //小
					NUM = 10;
					play();
				} else if (mode == 2) { //
					NUM = 40;
					play();
				} else if (mode == 3) { //
					NUM = 99;
					play();
				}

			} else if (msg.y > 170 and msg.y < 220) { //自定义雷数
				setNUM();
			} else if (msg.y > 270 and msg.y < 320) { //排行榜
				if (mode == 1) { //小排行榜
					Rank();
				} else if (mode == 2) { //
					Rank();
				} else if (mode == 3) { //
					Rank();
				}
			}
		}
	}
}

void select() {//第2界面
	closegraph();
	flushmessage();
	initgraph(360, 440, EX_DBLCLKS);
	setbkcolor(RGB(192, 192, 192));
	cleardevice();//
	setbkmode(TRANSPARENT);//
	setlinecolor(BLACK);//
	settextstyle(40, 0, "Consolas"); //
	settextcolor(BLACK);
	//
	setfillcolor(RGB(225, 225, 225));

	//
	string text = "开始游戏";
	rectangle(60, 70, 300, 120);
	bar(61, 71, 299, 119);
	outtextxy(61, 71, text.c_str());

	text = "自定义雷数";
	rectangle(60, 170, 300, 220);
	bar(61, 171, 299, 219);
	outtextxy(61, 171, text.c_str());

	text = "排行榜";
	rectangle(60, 270, 300, 320);
	bar(61, 271, 299, 319);
	outtextxy(61, 271, text.c_str());

	while (End == 0) {
		getMouse2();
		Sleep(20);
	}

}

void getMouse1() { //第1界面获取鼠标

	MOUSEMSG msg = GetMouseMsg();
	//x:61-299,y:5199
	//x:61-299,y:151-199
	//x:61-299,y:251-299
	//x:61-299,y:351-399
	if (msg.uMsg == WM_LBUTTONDOWN) {
		if (msg.x > 60 and msg.x < 300) {
			if (msg.y > 50 and msg.y < 100) {
				msg.uMsg = 0;
				mode = 1;
				ROW = 9;
				COL = 9;
				select();
			} else if (msg.y > 150 and msg.y < 200) {
				msg.uMsg = 0;
				mode = 2;
				ROW = 16;
				COL = 16;
				select();
			} else if (msg.y > 250 and msg.y < 300) {
				msg.uMsg = 0;
				mode = 3;
				ROW = 16;
				COL = 30;
				select();
			} else if (msg.y > 350 and msg.y < 400) { //退出
				flushmessage();
				closegraph();
				End = 1;
			}

		}
	}
}

void init() {
	initgraph(660, 440, EX_DBLCLKS);
	setbkcolor(RGB(192, 192, 192));
	cleardevice();//背景上色
	setbkmode(TRANSPARENT);//文字背景透明



	int x = 60, y = 50;
	int width = 240, height = 50;

	setlinecolor(BLACK);//设置按钮
	settextstyle(30, 0, "Consolas"); //
	settextcolor(BLACK);
	setfillcolor(RGB(225, 225, 225)); //

	string text = "简单:9x9-10";
	rectangle(x, y, x + width, y + height); //
	bar(x + 1, y + 1, x + width - 1, y + height - 1); //
	outtextxy(x + 5, y + (height - 20) / 2, text.c_str()); //

	text = "中等:16x16-40";
	rectangle(x, y + 100, x + width, y + height + 100); //
	bar(x + 1, y + 1 + 100, x + width - 1, y + height - 1 + 100); //
	outtextxy(x + 5, y + 100 + (height - 20) / 2, text.c_str()); //

	text = "困难:30x16-99";
	rectangle(x, y + 200, x + width, y + height + 200); //
	bar(x + 1, y + 200 + 1, x + width - 1, y + 200 + height - 1); //
	outtextxy(x + 5, y + 200 + (height - 20) / 2, text.c_str()); //

	text = "退出游戏";
	rectangle(x, y + 300, x + width, y + height + 300); //
	bar(x + 1, y + 300 + 1, x + width - 1, y + 300 + height - 1); //
	outtextxy(x + 5, y + 300 + (height - 20) / 2, text.c_str()); //

	rule();

	Sleep(100);


	while (End == 0) {
		getMouse1();
		Sleep(20);//归还cpu
	}

}

//更新输入文本框
void inputBox(int x, int y, int w, int h, string text) {
	setlinecolor(BLACK);//按钮边框
	rectangle(x, y, x + w, y + h);
	settextstyle(30, 0, "Consolas"); //文本
	settextcolor(BLACK);
	//按钮填充
	setfillcolor(RGB(225, 225, 225));
	bar(x + 1, y + 1, x + w - 1, y + h - 1);
	outtextxy(x + 7, y + (h - 25) / 2, text.c_str());

}

char getKey() {
	for (char c = '0'; c <= '9'; ++c) { //获取键盘输入,只处理数字
		if (GetAsyncKeyState(c) & 0x8000)
			return c;
	}
	//退格
	if (GetAsyncKeyState(VK_BACK) & 0x8000) {
		return '\b';
	}
	//回车
	if (GetAsyncKeyState(VK_RETURN) & 0x8000) {
		return '\r';
	}
	//无
	return '\0';
}

void setNUM() { //自定义雷数
	//设置输入文本框
	flushmessage();
	int w = 350, h = 100;
	initgraph(w, h);
	setbkcolor(WHITE);
	cleardevice();
	setbkmode(TRANSPARENT);//
	settextstyle(25, 0, "楷体"); //
	settextcolor(BLACK);
	string text = "请输入雷数,回车开始游戏";
	outtextxy(5, 20, text.c_str());

	int inputBoxX = 10, inputBoxY = 50;
	int inputBoxW = 320, inputBoxH = 40;
	string input;
	//文本框
	inputBox(inputBoxX, inputBoxY, inputBoxW, inputBoxH, input);

	//循环
A:
	bool quit = 0; //是否回车

	int num1 = 0;
	while (!quit) { //未回车

		//获取键盘
		char key = getKey();
		if (key != '\0') { //不是无
			if (key == '\b') { //是退格
				if (!input.empty()) { //就删input的最后一位
					input.pop_back();
				}
			} else if (key == '\r') { //回车
				quit = 1;
			} else {
				input += key; //是数字就写入
			}
		}
		inputBox(inputBoxX, inputBoxY, inputBoxW, inputBoxH, input); //更新文本框

		const char *str = input.data(); //string转char
		num1 = atoi(str); //char转int
		if (num1 > ROW * COL / 2) {
			MessageBox(GetHWnd(), "雷数过大 \n请小于格子数的1/2", "ERROR", MB_OK + 16);
			input.clear();//清空输入
		}
		NUM = num1;//记录雷数
		//每0.1s接受一次键盘输入,如此人可以反应不至于过快
		Sleep(100);
	}
	if (num1 < ROW * COL / 10) {
		MessageBox(GetHWnd(), "雷数过小 \n请大于格子数的1/10", "ERROR", MB_OK + 16);
		input.clear();//
		goto A;
	}

	//清空所有消息
	flushmessage();
	play();

}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	setjmp(buf);
	ROW = 9, COL = 9;//地图大小
	NUM = 0;         //雷数
	mark = 0;         //标记数
	found = 0;        //翻开数
	prog = 0;      //计数板需变化
	searchMusic = 0;
	while (!q.empty()) {
		q.pop();
	}
	init();

	return 0;
}
(RGB(225, 225, 225));
	bar(x + 1, y + 1, x + w - 1, y + h - 1);
	outtextxy(x + 7, y + (h - 25) / 2, text.c_str());

}

char getKey() {
	for (char c = '0'; c <= '9'; ++c) { //获取键盘输入,只处理数字
		if (GetAsyncKeyState(c) & 0x8000)
			return c;
	}
	//退格
	if (GetAsyncKeyState(VK_BACK) & 0x8000) {
		return '\b';
	}
	//回车
	if (GetAsyncKeyState(VK_RETURN) & 0x8000) {
		return '\r';
	}
	//无
	return '\0';
}

void setNUM() { //自定义雷数
	//设置输入文本框
	flushmessage();
	int w = 350, h = 100;
	initgraph(w, h);
	setbkcolor(WHITE);
	cleardevice();
	setbkmode(TRANSPARENT);//
	settextstyle(25, 0, "楷体"); //
	settextcolor(BLACK);
	string text = "请输入雷数,回车开始游戏";
	outtextxy(5, 20, text.c_str());

	int inputBoxX = 10, inputBoxY = 50;
	int inputBoxW = 320, inputBoxH = 40;
	string input;
	//文本框
	inputBox(inputBoxX, inputBoxY, inputBoxW, inputBoxH, input);

	//循环
A:
	bool quit = 0; //是否回车

	int num1 = 0;
	while (!quit) { //未回车

		//获取键盘
		char key = getKey();
		if (key != '\0') { //不是无
			if (key == '\b') { //是退格
				if (!input.empty()) { //就删input的最后一位
					input.pop_back();
				}
			} else if (key == '\r') { //回车
				quit = 1;
			} else {
				input += key; //是数字就写入
			}
		}
		inputBox(inputBoxX, inputBoxY, inputBoxW, inputBoxH, input); //更新文本框

		const char *str = input.data(); //string转char
		num1 = atoi(str); //char转int
		if (num1 > ROW * COL / 2) {
			MessageBox(GetHWnd(), "雷数过大 \n请小于格子数的1/2", "ERROR", MB_OK + 16);
			input.clear();//清空输入
		}
		NUM = num1;//记录雷数
		//每0.1s接受一次键盘输入,如此人可以反应不至于过快
		Sleep(100);
	}
	if (num1 < ROW * COL / 10) {
		MessageBox(GetHWnd(), "雷数过小 \n请大于格子数的1/10", "ERROR", MB_OK + 16);
		input.clear();//
		goto A;
	}

	//清空所有消息
	flushmessage();
	play();

}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	setjmp(buf);
	ROW = 9, COL = 9;//地图大小
	NUM = 0;         //雷数
	mark = 0;         //标记数
	found = 0;        //翻开数
	prog = 0;      //计数板需变化
	searchMusic = 0;
	while (!q.empty()) {
		q.pop();
	}
	init();

	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值