C++ · 手把手教你写一个扫雷小游戏

Hello,大家好,我是余同学。这两个月真是太忙了,无暇给大家更新文章…

暑假不是写了个扫雷小游戏吗(Link)?考虑到很多同学对代码没有透彻的理解,那么,这篇文章,我们来详细分析一下代码.

我们分为三个部分来讲:生成雷区,生成雷区数字刷新与判断


Part.1 生成雷区

随机数

首先,我们的雷区不能是定义好的矩阵,肯定得用随机数生成。
用随机数的话,就出现了一个问题:

  • C++有一个生成随机数的奇妙特性(也就是在不写srand(time(NULL));的情况下)

这个特性是什么样的呢?
我们来看下示例:

#include <bits/stdc++.h>
using namespace std;

int main ()
{
	//srand(time(NULL));
	int random=rand()%10;
	cout<<random;
    return 0;
}

运行结果:
第一次:
1.1

第二次:
1.2

可以看到, 虽然是"随机数",但是,程序每次输出的数却是一样的,这是怎么回事?

程序每次生成的数来源于一个随机数种子,如果我们不改变它的话,那么程序就会一直使用这个种子,从而导致每次生成的随机数都一样

因此,我们要使用随机数种子
其具体原理即是利用每次运行的时间互不相同,生成的随机数也不同
srand(time(NULL));

可以使用rand()%x生成随机数,也可以使用宏定义,即使用define

#define random(x) rand()%(x)

那么,我们看看效果吧
第一次输出:
2.1
第二次输出:
2.2
也就是说,如果我们要生成一个从ab区间的随机数,可以用以下指令:

int number=a+rand()%b; //随机数生成区间为:a ~ a+b-1
int number=rand()%b;   //随机数生成区间为:1 ~ b-1

随机数搞定了,接下来,就是考虑生成扫雷矩阵的事情了

生成雷区

现在,我们面临的最大问题就是:在不使用字符串的情况下,如何用0~9这十个数字实现数字与雷的分别

其实,实现很简单,我使用的方法是:1~8作为数字,0作为附近9格无雷标志(其实还是数字),9作为雷

那么,我们的矩阵是 10×10 的,只要通过循环生成 100 个随机数就行了

看代码吧:

#include<bits/stdc++.h>
/*
#include <iostream>
#include <cstdlib>
#include <windows.h>
#include <ctime>
#include <iomanip>
*/
#define random(x) 1+rand()%(x)
using namespace std;
int ui[12][12],b[12][12]; //多开一些没有坏处,原因后面会讲

int main(){
	srand(time(NULL)); //random seed, srand(time(0));
	//system("color 1B");
	system("title MineSweeper");
	int cnt=10; //地雷个数
	
	while(cnt){
		int x=random(10);//a+rand()%b = [a, a+b-1]
		int y=random(10);
		if(!ui[x][y]){//if(ui[x][y]==0){
			ui[x][y]=9;
			cnt--;
		}
	}
	return 0;
}

忘记说了,我设了两个二维数组,其中,ui数组的功能是:存储整个雷区;数组b的功能则是:存储每个方格的状态(详见Part.3

至此,我们的第一部分结束。

如果有的同学疑惑上面的代码没有输出时,那是因为我们只写了生成雷区的代码,并没有写生成雷区旁数字的代码,详细请看Part.2 生成雷区数字


Part. 2 生成雷区数字

要完成这一部分,我们首先得了解地雷旁数字的生成规律:

每个数字代表周围8格内的地雷数量

还是上图片:
3.1
这样,我们的逻辑就清晰了,只要用双重循环遍历二维数组b的每一项,判断这个点是否为地雷,如果是,就跳过这个点,继续向下遍历;否之,计算出该点周围8个方格的总雷数(我们暂且定义为sum),sum就是这个点的数值

来把这一部分的代码实现吧

for(int i=1;i<=10;i++){
	for(int j=1;j<=10;j++){
		int sum=0; //统计周围地雷数量
		if(ui[i][j]!=9){ //该点不为雷
			/*
			下面的这些坐标具体位置详见上图
			*/
			if(ui[i-1][j-1]==9) sum++;//sum=!(a[i-1][j-1]-9)+!(a[i-1][j]-9)...
			if(ui[i-1][j]==9) sum++;
			if(ui[i-1][j+1]==9) sum++;
			if(ui[i][j-1]==9) sum++;
			if(ui[i][j+1]==9) sum++;
			if(ui[i+1][j-1]==9) sum++;
			if(ui[i+1][j]==9) sum++;
			if(ui[i+1][j+1]==9) sum++;
			ui[i][j]=sum;
		}
	}
}

现在,我们就知道了为什么前面的两个二维数组都要开大一点了:

因为二维数组的索引是从0开始的,而我们使用1作为起始索引(具体见上方代码两个for循环中),就是为了避免在边缘上的方块无法获取到周围地雷数,从而导致程序出bug的原因

然后,代码中的8个 if 语句都是统计周围地雷数的,如果你想简单亿点的话,把if语句替换成这一行:

sum=!(ui[i-1][j-1]-9)+!(ui[i-1][j]-9)+!(ui[i-1][j+1]-9)+
!(ui[i][j-1]-9)+!(ui[i][j+1]-9)+!(ui[i+1][j-1]-9)+!(ui[i+1][j]-9)+!(ui[i+1][j+1]-9);

这里我分做两行写,是怕有的同学看不清,大家在实际替换中删掉换行符就可以了

这一部分完整代码:

#include<bits/stdc++.h>
#include "windows.h"
/*
#include <iostream>
#include <cstdlib>
#include <windows.h>
#include <ctime>
#include <iomanip>
*/
#define random(x) 1+rand()%(x)
using namespace std;

int ui[12][12],b[12][12]; 
int main(){
	srand(time(NULL)); //random seed
	//system("color 1B");
	system("title MineSweeper");
	int cnt=10;//cout<<"Booms:";cin>>cnt;
	while(cnt){
		int x=random(10);//a+rand()%b = [a, a+b-1]
		int y=random(10);
		if(!ui[x][y]){//if(ui[x][y]==0){
			ui[x][y]=9;
			cnt--;
		}
	}
	
	for(int i=1;i<=10;i++){
		for(int j=1;j<=10;j++){
			int sum=0;
			if(ui[i][j]!=9){
				if(ui[i-1][j-1]==9) sum++;//sum=!(a[i-1][j-1]-9)+!(a[i-1][j]-9)...
				if(ui[i-1][j]==9) sum++;
				if(ui[i-1][j+1]==9) sum++;
				if(ui[i][j-1]==9) sum++;
				if(ui[i][j+1]==9) sum++;
				if(ui[i+1][j-1]==9) sum++;
				if(ui[i+1][j]==9) sum++;
				if(ui[i+1][j+1]==9) sum++;
				/*sum=!(ui[i-1][j-1]-9)+!(ui[i-1][j]-9)+!(ui[i-1][j+1]-9)+
				!(ui[i][j-1]-9)+!(ui[i][j+1]-9)+!(ui[i+1][j-1]-9)+
				!(ui[i+1][j]-9)+!(ui[i+1][j+1]-9);*/
				ui[i][j]=sum;
				
			}
		}
	}
	
	/*for(int i=1;i<=10;i++){
		for(int j=1;j<=10;j++){
			cout<<ui[i][j]<<' '; //output the numbers
		}
		cout<<endl;
	}*/
	return 0;
}

大家把最后一段的注释符删掉,就可以看到我们的矩阵了
test1

但是,我们到现在才做完整个项目的一半,我们还没有写判断操作符的代码,这就要留到Part 3来讲了


Part. 3 状态更新与判断输入

刷新屏幕

我们先来讲刷新屏幕

为什不先写判断输入的代码呢?
因为我们得让程序先把扫雷的矩阵输出出来

我们看看这张图片
minesweeper
这是原版的扫雷界面,通过观察每个方格,可以发现,每个方格都会出现三种状态:已翻开(包括空格和数字,用1表示)、未翻开(包括数字和地雷)和旗子(用户标雷处)

那么,我们可以用"#"表示未翻开的区域,用"P"表示旗子(🚩),用0~9这十个数字表示已翻开区域

但是,井号(#)和旗子(P)都属于字符串类型,我们开的是数组类型int整型,存储不了字符串类型的字符,所以,我们不能把#P赋值给二维数组ui里的某一项,而是得直接输出

逻辑:双重for循环遍历每个方格,如果该方块状态为未翻开,输出#;如果此方块状态为已翻开,直接输出二维数组ui里的这一项;如果此方块已被插旗,则输出P

看看代码实现吧:

for(int i=1;i<=10;i++){ 
	for(int j=1;j<=10;j++){ //遍历每个方格
		if(b[i][j]==0){ //如果此方块状态为未翻开
				cout<<"#"<<' '; //则输出#
			}else if(b[i][j]==1){ //如果此方块状态为已翻开
				cout<<ui[i][j]<<' '; //直接输出就行
			}else if(b[i][j]==2){ //如果此方块已被插旗
				cout<<"P"<<' '; //输出P(已插旗)
			}
		}cout<<endl;
判断输入

先来定义一下这个游戏的规则:

游戏一开始首先会打印一个10×10的井号(#)方阵,表示扫雷区,接下来让用户输入一个操作命令,再输入横纵坐标,表示要操作的方格。

将雷全部扫完即为胜利,否之,踩到雷即为失败


操作符:

  • q代表翻开坐标处的井号,再输入xy坐标,表示翻开该方格
  • p代表在坐标处插旗,再输入xy坐标,表示在这个方格上插旗(也就是标雷)
  • c代表取消坐标位的旗,再输入xy坐标,表示取消在该位置插旗

坐标判断依据:

上方所述的x,y坐标的具体位置:第x行第y个(从左往右数)
比如这个矩阵(3*3):

  • 5 9 8
  • 2 0 3
  • 8 6 7

x=2y=3 时,表示的数字为3


理解了上面这些逻辑,我们就有了一个大体的思路:
设置状态变量op(char类型)与xy(存储坐标)

  • 如果输入为q,如果该方块为地雷,直接输出You died!;如果为0~8的正常数字,将该方块状态设置为已翻开
if(op=='q'){
	if(ui[x][y]==9){
		cout<<"You died !";
		scanf("%d");
		return 0;
	}
	else{
		b[x][y]=1;
	}
}

然后,再设置变量kk1k为地雷正确位置(客观),k1为用户标雷位置(主观);作用:存储总地雷数,如果k==cnt(总雷数)k==k1的话,说明所有地雷已经扫完,且位置正确(没有把正常方格当成地雷)

  • 如果输入为p,将该方格设为P,状态设为2(标雷),k1自增;如果当前位置有雷,则说明用户判断正确,k自增
else if(op=='p'){
	k1++;
	if(ui[x][y]==9){
		k++;
	}	
	b[x][y]=2;
}
  • 如果输入为c,如果该位置状态为2(插旗),k1自减,如果该位置有地雷,k自减,然后将该格状态设置为0(未翻开);如果该位置没有插旗,用户想撤销(也就是作弊;当然,你也可以删掉这个if,享受当赢家的快乐),直接跳过(continue)
else if(op=='c'){
	if(b[x][y]==2){
		k1--;
		if(ui[x][y]==9){
			k--;
		}
		b[x][y]=0;
	}else{
		continue;
		}
	}
  • 如果所有地雷已标记(扫完),且位置正确,输出You win!
if(k==cnt && k==k1){
	cout<<"You win !";
	scanf("%d");
	return 0;
}
最后,这个判断是要循环实现的。贴上我修改过的完整代码:
#include<bits/stdc++.h>
#include "windows.h"
/*
#include <iostream>
#include <cstdlib>
#include <windows.h>
#include <ctime>
#include <iomanip>
*/
#define random(x) 1+rand()%(x)

using namespace std;

int ui[12][12],b[12][12]; 

int main(){
	srand(time(NULL)); //random seed
	//system("color 1B");
	system("title MineSweeper");
	int cnt=10;
	while(cnt){
		int x=random(10);//a+rand()%b = [a, a+b-1]
		int y=random(10);
		if(!ui[x][y]){//if(ui[x][y]==0){
			ui[x][y]=9;
			cnt--;
		}
	}
	cnt=10;
	for(int i=1;i<=cnt;i++){
		for(int j=1;j<=cnt;j++){
			int sum=0;
			if(ui[i][j]!=9){
				if(ui[i-1][j-1]==9) sum++;//sum=!(a[i-1][j-1]-9)+!(a[i-1][j]-9)...
				if(ui[i-1][j]==9) sum++;
				if(ui[i-1][j+1]==9) sum++;
				if(ui[i][j-1]==9) sum++;
				if(ui[i][j+1]==9) sum++;
				if(ui[i+1][j-1]==9) sum++;
				if(ui[i+1][j]==9) sum++;
				if(ui[i+1][j+1]==9) sum++;
				//sum=!(ui[i-1][j-1]-9)+!(ui[i-1][j]-9)+!(ui[i-1][j+1]-9)+!(ui[i][j-1]-9)+!(ui[i][j+1]-9)+!(ui[i+1][j-1]-9)+!(ui[i+1][j]-9)+!(ui[i+1][j+1]-9);
				ui[i][j]=sum;
				
			}
		}
	}
	
	/*for(int i=1;i<=10;i++){
		for(int j=1;j<=10;j++){
			cout<<ui[i][j]<<' '; //output the numbers
		}
		cout<<endl;
	}*/
	
	int k=0,k1=0;
	
	while(true){
		/*for(int i=1;i<=cnt1;i++){
			for(int j=1;j<=cnt1;j++){
			cout<<ui[i][j]<<' '; //output the numbers,cheat code(作弊代码)
		}
		cout<<endl;
	}cout<<endl; */
	
		for(int i=1;i<=cnt;i++){
			for(int j=1;j<=cnt;j++){
				if(b[i][j]==0){
					cout<<"#"<<' ';
				}else if(b[i][j]==1){
					cout<<ui[i][j]<<' ';
				}else if(b[i][j]==2){
					cout<<"P"<<' ';
				}
			}
			cout<<endl;
		}
		char op;
		cin>>op;
		int x,y;
		cin>>x>>y;
		system("cls");
		if(op=='q'){
			if(ui[x][y]==9){
				cout<<"You died !";
				scanf("%d");
				return 0;
			}
			else{
				b[x][y]=1;
			}
		}
		else if(op=='p'){
			k1++;
			if(ui[x][y]==9){
				k++;
			}	
			b[x][y]=2;
		}
		else if(op=='c'){
			if(b[x][y]==2){
				k1--;
				if(ui[x][y]==9){
					k--;
				}
				b[x][y]=0;
			}else{
				continue;
			}
		}
		if(k==cnt && k==k1){
			cout<<"You win !";
			scanf("%d");
			return 0;
		}
	}
    return 0;
} 

MineSweeper
但是,我们现在还有一个问题没有解决:当翻开的数为0,且周围8格有为0的格子(也就是扫雷中的空格)时,我们应该自动显示出来
下次再解决吧


Update on 13/2/2024
鸽了一个学期,终于更新了

这次增加了一些功能,解决了上次没完成的一个功能(上方斜体字)
但是还有一个功能(第一次点击不会是雷)没实现,试了很多方案,还问了一个编程很厉害的朋友,这是他给出的解决方案:
在这里插入图片描述
我也试过,还是没写出来,在此求大佬指点

ok,开始正题

Part. 4 一些更新

一、自动显示空白

这个比较简单,只需判断当前点击的方格是否为0,是,判断周围8格是否有空白格(0),是,将其翻开

if(b[x][y]==1&&ui[x][y]==0){//当前格已翻开且为0
	if(ui[x-1][y-1]==0) b[x-1][y-1]=1;
	if(ui[x-1][y]==0) b[x-1][y]=1;
	if(ui[x-1][y+1]==0) b[x-1][y+1]=1;
	if(ui[x][y-1]==0) b[x][y-1]=1;
	if(ui[x][y]==0) b[x][y]=1;
	if(ui[x][y+1]==0) b[x][y+1]=1;
	if(ui[x+1][y-1]==0) b[x+1][y-1]=1;
	if(ui[x+1][y]==0) b[x+1][y]=1;
	if(ui[x+1][y+1]==0) b[x+1][y+1]=1;	
}

加在操作符为“翻开”,也就是 q 的判断普通方块(不为地雷9)分支中,大家不用自己添加,后面会放出完整代码

二、功能性更新
1、生命值

这个功能的增加改善了可玩性
在程序前面加一个lives的变量,每次踩到雷减一,并判断生命值是否为0,为0,停止游戏即可
每次生成雷阵后输出生命值
简单代码:

int lives=3;
/*-----中间部分-----*/
if(ui[x][y]==9){
	lives--;
	if(lives==0){
		return 0;
	}
}
2、踩到旗子不掉血&踩到雷自动插旗

用户插下一个旗子,再次踩到时不会掉血,这个在上个版本没有考虑到
自动插旗让游戏更加方便,不用用户再次在雷的位置插旗

if(b[x][y]==2) continue;//anti-blood on a flag
if(ui[x][y]==9){
	b[x][y]=2;
	lives--;
	if(lives==0){
		return 0;
	}
}
3、上方未完成的功能的替代&非法操作符的判断

设置一个bool类型的firstClick变量,开始为true,当为首次点击并踩到雷时,先将其设为false,再自动插旗,直接continue(不扣血)

下方代码为功能性更新中的1,2,3板块的实现:

bool firstClick=true;
/*-----中间部分-----*/
if(op=='q'){
	if(ui[x][y]==9){//losing
		if(firstClick){
			firstClick=false;
			b[x][y]=2;
			continue;
		}
		if(b[x][y]==2) continue;//anti-blood on a flag
		lives--;
		cout<<"oops! you just clicked a mine"<<endl<<endl;
		if(lives==0){
			cout<<"You Lose!";
			return 0;
		}
		b[x][y]=2;//auto-flag after one death
		continue;
		return 0;
	}
	else{
		firstClick=false;
		/*.....*/
	}
}
else if(op=='p'){//flagging
	firstClick=false;
	/*.....*/
}
else{
	cout<<"invalid operator\n";
	continue;
}
4、自定义地图大小、生命值、雷数

定义row col mine_sum变量,然后输入,判断数据合法性
再把后面for循环里面的10,10(上版本预设的地图大小)更改为rowcol即可
放个判断数据合法性的代码:

int row,col,lives,mine_sum;
cin>>row>>col>>lives>>mine_sum;
while(row>=30||row<=0||col>=30||col<=0||
	lives>=(row*col)||lives==0||
	mine_sum>=(row*col)||mine_sum<=0){ //judge input data
		cout<<"failed to process data, please reset map size/health/mine_sum\n";
		cin>>row>>col>>lives>>mine_sum;
}

这里预设了地图大小不能超过30×30,且行数,列数均大于等于0,生命值必须小于 行数×列数(不然可以试出来),雷数必须小于 行数×列数

5、Ai功能

看着像个重头戏,实际上实现很简单
我将Ai功能的操作符设为a,后面接x,y坐标,表示从(1,1)开始到(x,y)结束的矩阵,Ai会在这个区间内查找 ui[x][y]==9 的点并自动标旗,询问是否输出矩阵,是,就输出从(1,1)开始到(x,y)结束的矩阵(后面实现了用户自定义矩阵)

另外,由于程序原因,这里的x,y坐标并不是坐标系中的坐标:
coord
我加了一个变量用以存储Ai找出的雷(新增雷数),并且做了一个用户自定义矩阵:

新增变量x1,y1,用以存储结束点的坐标,这样,第一次输入的坐标(如输入a 1 1)将成为起点坐标,接下来判断数据,x1,y1不能小于等于0,也不能大于行数列数

int x1,y1;
cin>>x1>>y1;
while(x1<=0||y1<=0||x1>row||y1>col){
	cout<<"invalid input\n";
	cin>>x1>>y1;
}

并将搜索范围设置为从x到x1,从y到y1

这部分的代码如下:

else if(op=='a'){
	firstClick=false;
	cout<<"ai-mode enabled"<<endl;
	int x1,y1;
	cout<<"start-point coordinates have been entered\n please enter the end-point coordinates\n";
	cin>>x1>>y1; //input judging
	while(x1<=0||y1<=0||x1>row||y1>col){
		cout<<"invalid input\n";
		cin>>x1>>y1;
	}
	cout<<"looking for mines in: "<<'('<<x<<','<<y<<')'<<'('<<x1<<','<<y1<<')'<<endl; 
	int new_mine=0;
	for(int i=x;i<=x1;i++){
		for(int j=y;j<=y1;j++){
			if(ui[i][j]==9){
				k++;
				k1++;
				if(b[i][j]!=2){
					b[i][j]=2;
					new_mine++;
				}
				cout<<"mine position: "<<i<<' '<<j<<endl;
			}
		}
	}
	cout<<new_mine<<" NEW mine(s) was(were) found\n";
	cout<<"output array?(y/n)"<<' ';
	char ans;
	cin>>ans;
	if(ans=='y'){
		cout<<"outputting..."<<'\n';
		for(int i=x;i<=x1;i++){
			for(int j=y;j<=y1;j++){
				cout<<ui[i][j]<<' ';
			}cout<<endl;
		}cout<<endl;
				
	}
	cout<<endl;
}
6.一些完善(更新于4.30)

①. Ai输入坐标的改进
这个很简单,只需控制双重循环的范围即可
在这里插入图片描述
上图为Ai代码的改进
②. 视觉效果
一、进度条
一开始我选用了这种:

for(int i=0;i<=100;i+=10){
	cout<<i<<"%... ";
	Sleep(random(row*col*7));
}

经过简易的改进,我们可以写出这种:

for(int i=1;i<=10;i++){
	int x=i;
	cout<<"[";
	for(int j=1;j<=x;j++) cout<<"*";
	for(int g=1;g<=(10-x);g++) cout<<' ';
	cout<<"] "<<x*10<<"%"<<'\n';
	Sleep(random(row*col*3));
}

效果如下:
在这里插入图片描述
二、打字效果
这个也简单,只需加入Sleep代码即可

void print(string s){ //100% created originally
	//getline(cin,s);
	char ch[s.size()];
	for(int i=0;i<s.size();i++) ch[i]=s[i];
	for(int i=0;i<s.size();i++){
		cout<<ch[i];
		Sleep(70);
	} 	
}

调用函数,参数为要输出的字符串即可
三、作弊 调试
运算符为h,同样加上x,y坐标,表示要输出的范围

else if(op=='h'){
	firstClick=false;
	for(int i=1;i<=x;i++){
		for(int j=1;j<=y;j++){
			cout<<ui[i][j]<<' ';
		}cout<<endl;
	}cout<<'\n';	
}

游戏的完整代码如下:

//Made by DingDang 2024-2
//UTF-8 Coding
//Compile Mode:C++11 or higher version System: Win7+ 64x
#include<bits/stdc++.h>
#include "windows.h" //for Windows-Client
/*
#include <iostream>
#include <cstdlib>
#include <windows.h>
#include <ctime>
#include <iomanip>
*/
#define random(x) 1+rand()%(x)
/*
Operator description:
1, q means to open the grid at the coordinate, continue to enter x and y, means to open the y row x
2, p indicates that the flag is planted at the coordinates (the coordinates of the user are thunder)
3, c means cancel the flag at the coordinate (if any)
4. a indicates ai automatic demining, and the last two coordinates indicate Ai demining range
*/
using namespace std;

void print(string s){ //100% created originally
	//getline(cin,s);
	char ch[s.size()];
	for(int i=0;i<s.size();i++) ch[i]=s[i];
	for(int i=0;i<s.size();i++){
		cout<<ch[i];
		Sleep(70);
	} 	
}

int ui[105][105]={0},b[105][105]={0}; 
int lives,mine_sum;
bool firstClick=true;
int row,col;
int k=0,k1=0;

int main(){
	srand(time(NULL)); //random seed
	//system("color 1B");
	system("title MineSweeper");//set window title
	//system("mode con cols=50 lines=30");//set window size
	system("echo [console]variables initialization succeeded");
	cout<<'\n';
	Sleep(200);
	cout<<"inputting|format:<HEIGHT> <WIDTH> <HEALTH> <MINE_SUM>\n";//col width row height
	print("example: 10 10 3 10");cout<<'\n';
	
	cin>>row>>col>>lives>>mine_sum;
	
	while(row>=30||row<=0||col>=30||col<=0||
		lives>=(row*col)||lives==0||
		mine_sum>=(row*col)||mine_sum<=0){ //judge input data
			cout<<"failed to process data, please reset map size/health/mine_sum\n";
			cin>>row>>col>>lives>>mine_sum;
	}
	print("generating map...");cout<<'\n';
	/*for(int i=0;i<=100;i+=10){
		cout<<i<<"%... ";
		Sleep(random(row*col*7));
	}*/
	for(int i=1;i<=10;i++){
		int x=i;
		cout<<"[";
		for(int j=1;j<=x;j++) cout<<"*";
		for(int g=1;g<=(10-x);g++) cout<<' ';
		cout<<"] "<<x*10<<"%"<<'\n';
		Sleep(random(row*col*3));
	}
	cout<<'\n'<<'\n';
	int tempCalc=mine_sum;
	while(mine_sum){
		int x=random(row);//a+rand()%b = [a, a+b-1]
		int y=random(col);
		if(!ui[x][y]){//if(ui[x][y]==0){
			ui[x][y]=9;
			mine_sum--;
		}
	}
	for(int i=1;i<=row;i++){
		for(int j=1;j<=col;j++){
			int sum=0;//sum is the current block number
			if(ui[i][j]!=9){
				if(ui[i-1][j-1]==9) sum++;
				if(ui[i-1][j]==9) sum++;//generate number 
				if(ui[i-1][j+1]==9) sum++;
				if(ui[i][j-1]==9) sum++;
				if(ui[i][j+1]==9) sum++;
				if(ui[i+1][j-1]==9) sum++;
				if(ui[i+1][j]==9) sum++;
				if(ui[i+1][j+1]==9) sum++;
				//sum=!(ui[i-1][j-1]-9)+!(ui[i-1][j]-9)+!(ui[i-1][j+1]-9)+!(ui[i][j-1]-9)+!(ui[i][j+1]-9)+!(ui[i+1][j-1]-9)+!(ui[i+1][j]-9)+!(ui[i+1][j+1]-9);
				ui[i][j]=sum;
				
			}
		}
	}
	
	/*for(int i=1;i<=10;i++){
		for(int j=1;j<=10;j++){
			cout<<ui[i][j]<<' '; //output the numbers
		}
		cout<<endl;
	}*/
	
	while(true){
		/*for(int i=1;i<=row;i++){
			for(int j=1;j<=col;j++){
				cout<<ui[i][j]<<' '; //output the numbers
			}
			cout<<endl;
		}cout<<endl; */
	//for debug
	
		for(int i=1;i<=row;i++){
			for(int j=1;j<=col;j++){ //state of b[k][m]: =0 closed | =1 opened | =2 flagged
				if(b[i][j]==0){
					cout<<"#"<<' ';
				}else if(b[i][j]==1){
					cout<<ui[i][j]<<' ';
				}else if(b[i][j]==2){
					cout<<"P"<<' ';
				}
			}cout<<endl;
			//Judge the status of the grid (opened, closed, flag 
		}
		cout<<"Lives: "<<lives<<'\n';
		char op;//input operator
		cin>>op;
		int x,y;//input coord
		cin>>x>>y;
		while(x>row||y>col){
			cout<<"invalid input number"<<endl;
			cin>>op>>x>>y;continue;
		}
		
		
		if(op=='q'){
			if(ui[x][y]==9){//losing
				system("cls");
				if(firstClick){
					firstClick=false;
					b[x][y]=2;
					k++;k1++;
					continue;
				}else{
					if(b[x][y]==2) continue;//anti-blood on a flag
					lives--;
					system("color 47");
					print("oops! you just clicked a mine");cout<<endl<<endl;
					Sleep(200);
					system("color 07");
					if(lives==0){
						print("You Lose!");
						return 0;
					}
					//auto-flag after one death
					b[x][y]=2;
					k++;k1++;
				}
			}
			else{
				firstClick=false;
				system("cls");
				b[x][y]=1;
				if(b[x][y]==1&&ui[x][y]==0){
					if(ui[x-1][y-1]==0) b[x-1][y-1]=1;
					if(ui[x-1][y]==0) b[x-1][y]=1;
					if(ui[x-1][y+1]==0) b[x-1][y+1]=1;
					if(ui[x][y-1]==0) b[x][y-1]=1;
					if(ui[x][y]==0) b[x][y]=1;
					if(ui[x][y+1]==0) b[x][y+1]=1;
					if(ui[x+1][y-1]==0) b[x+1][y-1]=1;
					if(ui[x+1][y]==0) b[x+1][y]=1;
					if(ui[x+1][y+1]==0) b[x+1][y+1]=1;	
				}
				//Determine if there are any blank squares around, and if there are, open them automatically
			}
		}
		else if(op=='p'){//flagging
			firstClick=false; //k stores the correct location of the mine (objective),
			  			      //k1 is the user's marking location (subjective); Function: Store the total number of mines
			system("cls"); 
			k1++;
		 	if(ui[x][y]==9){
				k++;
			}	
			b[x][y]=2;
		}
		else if(op=='c'){//unflagging 
			system("cls");
			if(b[x][y]==2){
				k1--;
				if(ui[x][y]==9){
					k--;
				}
			b[x][y]=0;
			}else{
				system("cls");
				continue;
			}
		}
		else if(op=='a'){
			firstClick=false;
			print("ai-mode enabled");cout<<'\n';
			cout<<"looking for mines in: "<<'('<<x<<','<<y<<')'<<endl; 
			int new_mine=0;
			for(int i=1;i<=x;i++){
				for(int j=1;j<=y;j++){
					if(ui[i][j]==9){
						if(b[i][j]!=2){
							k++;k1++;
							b[i][j]=2;
							new_mine++;
						}
						cout<<"mine position: "<<i<<' '<<j<<endl;
						Sleep(200);
					}
				}
			}
			cout<<new_mine<<" NEW mine(s) was(were) found\n";
			cout<<"output array?(y/n)"<<' ';
			char ans;
			cin>>ans;
			if(ans=='y'){
				cout<<"outputting..."<<'\n';
				Sleep(1000);
				system("cls");
				for(int i=1;i<=x;i++){
					for(int j=1;j<=y;j++){
						cout<<ui[i][j]<<' ';
					}cout<<endl;
				}cout<<endl;
				
			}
			Sleep(1500);
			cout<<endl;
		}else if(op=='h'){
			firstClick=false;
			for(int i=1;i<=x;i++){
				for(int j=1;j<=y;j++){
					cout<<ui[i][j]<<' ';
				}cout<<endl;
			}cout<<'\n';	
		}else{
			print("invalid operator");cout<<'\n';continue;
		}
		if(k==tempCalc && k==k1){//winning
			print("You Win!");
			for(int i=1;i<=2;i++){
				system("color 1a");
				Sleep(100);
				system("color 2b");
				Sleep(100);
				system("color 3c");
				Sleep(100);
				system("color 4d");
				Sleep(100);
				system("color 5e");
			}
			system("color 07");
			scanf("%d");
		}
	}
    return 0;
} 

如果有任何bug,请在评论区指出,感谢各位!

玩法介绍

  • 程序会提示输入:<ROW> <COL> <HEALTH> <MINE_SUM>,分别代表行数,列数(均小于等于30),生命值(小于行数×列数),雷数(小于行数×列数),有数据判断
  • 操作符 q p c a +X,Y分别代表:在 (x,y) ²(见注释) 翻开/插旗/撤销插旗/使用Ai(作为起始点)³,以下为详细介绍
  • q x y: 翻开位于 (x,y) 的方格,如果这是你的首次点击并且踩到雷,程序会自动帮你插旗并不扣血,如果不是,扣一滴血,自动插旗;如果当前格为空白(0),自动帮你翻开周围8格内所有的空白
  • p x y: 在 (x,y) 插旗,表示你认为这里为雷,旗子没有数量限制,但是只有插对地方且数量正确才会胜利
  • c x y: 撤销在 (x,y) 的插旗,如果该位置没有旗则不会操作
  • a x y:在 (x,y) 排雷并插旗
  • h x y: 在 (x,y) 的范围内输出矩阵
  • NOTICE: 只需输入Ai终点,会自动在 (1,1) 到终点坐标排雷,更加便捷

Github开源地址:这里,上不去的同学试试FastGithub

感谢胡老师在我coding过程中给予的帮助和指导
好了,这就是本篇文章的全部内容(截止到2024/4/30),欢迎各位大佬指点!我们下期再见

### 构建任务失败解决方案 当遇到 `Execution failed for task ':app:shrinkReleaseRes'` 错误时,这通常意味着资源压缩过程中出现了问题。此错误可能由多种原因引起,包括但不限于配置不正确、依赖冲突或特定于项目的其他因素。 #### 可能的原因分析 1. **ProGuard 或 R8 配置不当** ProGuard 和 R8 是用于优化和混淆代码以及减少 APK 大小的工具。如果这些工具的配置存在问题,可能会导致资源无法正常处理[^1]。 2. **重复资源** 如果项目中有多个模块定义了相同的资源名称,可能导致冲突并引发该错误。检查是否存在重名的 drawable、string 等资源文件[^2]。 3. **第三方库兼容性** 某些第三方库可能与当前使用的 Gradle 插件版本或其他库存在兼容性问题,从而影响到资源打包过程中的行为[^3]。 4. **Gradle 缓存问题** 有时旧缓存数据会干扰新编译的结果,尝试清理本地仓库和重新同步项目可以帮助排除此类潜在障碍[^4]。 #### 推荐的操作方法 为了有效解决问题,建议按照以下步骤逐一排查: ```bash # 清理项目构建目录 ./gradlew clean # 删除 .gradle 文件夹下的所有内容以清除缓存 rm -rf ~/.gradle/caches/ ``` 调整 `build.gradle` 中的相关设置也是一个重要环节: ```groovy android { ... buildTypes { release { minifyEnabled true // 是否启用代码缩减 shrinkResources true // 是否开启资源压缩 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' // 尝试禁用 shrinkResources 来测试是否为资源压缩引起的错误 // shrinkResources false } } } ``` 此外,在 `proguard-rules.pro` 文件内添加必要的保留规则,防止关键类被意外移除: ```text -keep class com.example.yourpackage.** { *; } # 替换为你自己的包路径 -dontwarn androidx.**,com.google.** # 忽略警告信息 ``` 最后,确保所使用的 Android Studio 版本是最新的稳定版,并且已经应用了所有的补丁更新。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值