Win7扫雷的H5完整复刻实现(一) / js扫雷算法的初次鉴定与地图初始化实现

学校实训最后布置的一个答辩作业,周围同学都是在用资源做简单动画,突然想起大一的时候想用C语言模拟的扫雷,虽然做H5游戏比较少,但是还是较完美的复刻了WIN7扫雷的大部分算法和UI。

简单扫雷本身的实现难度不算太高,若是想要完美复刻win7的扫雷,还是需要费一番功夫的,这份无聊之作(装逼之作)是用两天时间写出来的,某些地方略有缺陷,但无伤大雅。

虽然本作是用JS语言实现逻辑,但是其他语言仍有互通之处,可以作为参考


同时推荐各位注重于代码的构架和算法本身的实现,下面我将讲述我的构架和算法实现过程与想法,希望各位能有自己的想法,不要参考他人过多

这是本作扫雷共计实现的功能和UI

- 首次点击鉴定

- 鼠标左右键同时点击展示周围雷区,并在满足一定条件下展开雷区

- 雷域全随机

- 参照win版扫雷的加载动画(渐变加载)

- 爆炸动画


首先让我们明确扫雷本质到底是什么:

扫雷是一个N*M的矩阵下发生状态转移的过程,每次对矩阵内元素点击的过程,都是一次发生状态转移的判断条件,先通过算法修改矩阵中的状态码,再用可视的界面表示这个矩阵中的内容,其中可以用动画作为过渡,这就是扫雷的本质。

其中,矩阵可以用二维数组表示,可视界面可以用canvans或者div绘制,其区别只在于canvans的动画与div不一样,canvans由于其动画需要清屏的特性,不仅效率不够高,而且想要实现一些UI的算法会变得非常复杂,所以本作选用div块进行模拟。


绘制初始可视界面:

其中sBlock是初始方块,game-ctn($ctn)是方块容器。

for(var i = 0; i < $num; i++) {
	for(var j = 0; j < $num; j++) {
		(function(i, j) {
			var $sBlock = $("
"); $ctn.append($sBlock); $sBlock.animate({ 'opacity': '0.9' }, 50 * (100 - i * 3 - j * 4)); })(i, j); } }

css不再赘述,可以直接参考我git里的代码,注意容器需要留出border的宽度,不然会导致方块溢出。

这里实现了渐变加载,本质是使每一个方块的透明度从0至0.9进行变化,通过设置不同的动画时间,来实现整体的加载动画

这里实现的是从末尾开始两边成三角蔓延加载的动画,通过50*(100-i*3-j*4)的算法,i与j越小的方块显示的速度也就越慢,同时若偏移量相同(设偏移量为N),map[x][0]与map[0][x]的动画加载速度相同(皆是50*(100-Nx)),同时他们连成一条线的方块,如map[x-1][1],加载速度同理相同,这样就造成了三角渐变的视觉效果,而这里 j(y轴)造成的速度偏移量比 i(x轴)大,导致方块在y轴上的加载速度比x轴上快,所以加载动画的渐变效果是倾斜的。

Win7扫雷的H5完整复刻实现(一) / js扫雷算法的初次鉴定与地图初始化实现_第1张图片     Win7扫雷的H5完整复刻实现(一) / js扫雷算法的初次鉴定与地图初始化实现_第2张图片

(加载过程中截图)


首次点击鉴定

为判断是否为第一次点击,可以使用标志变量进行鉴定,向所有Block绑定点击事件,若标志变量为false则执行首次点击算法并设其为true,反之不执行。

明确需求如下:

- 每一局游戏的第一次点击展开的区域大小,样子都不同

- 第一次点击的周围8个方块一定不是雷,不然扫雷游戏将难以进行

- 延展初次禁雷区时不应该通过斜角延展,不然会导致其展开的时候很丑(想象一下两个矩形只有一个角连接,不符合扫雷的UI)

第一次点击后,初始化地图矩阵,首先设置点击处周围的禁雷域,这里用-2的状态码表示禁雷域(本质是空白地区),0表示初始值

	_randomClickArray: function _randomClickArray(numX, numY, arrayNum) {
		var randomNumX = numX;
		var randomNumY = numY;
		var $firstFlag = this.$firstFlag;
		var $squareNum = void 0;
		if(arrayNum < 0) return;
		//越过边界
		if(!(numX < 0 || numX > 17 || numY < 0 || numY > 17)) {
			//重复遍历
			if(map[numX][numY] == -2) $squareNum = arrayNum;
			else {
				map[numX][numY] = -2;
				var $squareNum = arrayNum - 1;
			}
		} else {
			//溯回越界
			if(numX < 0) numX = 0;
			if(numX > 17) numX = 17;
			if(numY < 0) numY = 0;
			if(numY > 17) numY = 17;
			$squareNum = arrayNum;
		}
		//点击处八面不允许出现地雷
		if(!$firstFlag) {
			var i = numX;
			var j = numY;
			if(i != 17 && j == 17 && i != 0) {
				this._checkMineMap(i + 1, j);
				this._checkMineMap(i + 1, j - 1);
				this._checkMineMap(i - 1, j);
				this._checkMineMap(i - 1, j - 1);
			}
			if(j != 17 && i == 17 && j != 0) {
				this._checkMineMap(i, j + 1);
				this._checkMineMap(i - 1, j + 1);
				this._checkMineMap(i, j - 1);
				this._checkMineMap(i - 1, j - 1);
			}
			if(j != 17 && i == 0 && j != 0) {
				this._checkMineMap(i, j + 1);
				this._checkMineMap(i + 1, j + 1);
				this._checkMineMap(i, j - 1);
				this._checkMineMap(i + 1, j - 1);
			}
			if(i != 17 && j == 0 && i != 0) {
				this._checkMineMap(i + 1, j);
				this._checkMineMap(i + 1, j + 1);
				this._checkMineMap(i - 1, j);
				this._checkMineMap(i - 1, j + 1);
			}
			if(i == 17 && j == 17) {
				this._checkMineMap(i, j - 1);
				this._checkMineMap(i - 1, j);
				this._checkMineMap(i - 1, j - 1);
			}
			if(i == 0 && j == 0) {
				this._checkMineMap(i, j + 1);
				this._checkMineMap(i + 1, j + 1);
				this._checkMineMap(i + 1, j);
			}
			if(i == 17 && j == 0) {
				this._checkMineMap(i, j + 1);
				this._checkMineMap(i - 1, j + 1);
				this._checkMineMap(i - 1, j);
			}
			if(i == 0 && j == 17) {
				this._checkMineMap(i, j - 1);
				this._checkMineMap(i + 1, j - 1);
				this._checkMineMap(i + 1, j);
			}
			if(j != 17 && i != 17 && i != 0 && j != 0) {
				this._checkMineMap(i, j + 1);
				this._checkMineMap(i + 1, j);
				this._checkMineMap(i + 1, j + 1);
				this._checkMineMap(i - 1, j + 1);
				this._checkMineMap(i + 1, j - 1);
				this._checkMineMap(i - 1, j);
				this._checkMineMap(i, j - 1);
				this._checkMineMap(i - 1, j - 1);
			}
			this.$firstFlag = true;
		}
		//随机方向
		//		//左
		//		if(map[numX - 1][numY] === -1) coutNum++;
		//		//右
		//		if(map[numX + 1][numY] === -1) coutNum++;
		//		//下
		//		if(map[numX][numY + 1] === -1) coutNum++;
		//		//上
		//		if(map[numX][numY - 1] === -1) coutNum++;
		//		//右上
		//		if(map[numX + 1][numY - 1] === -1) coutNum++;
		//		//右下
		//		if(map[numX + 1][numY + 1] === -1) coutNum++;
		//		//左下
		//		if(map[numX - 1][numY + 1] === -1) coutNum++;
		//		//左上
		//		if(map[numX - 1][numY - 1] === -1) coutNum++;
		var $randomDir = Math.floor(Math.random() * 4);
		if($randomDir == 4) $randomDir = 3;
		switch($randomDir) {
			case 0:
				this._randomClickArray(numX - 1, numY, $squareNum);
				break;
			case 1:
				this._randomClickArray(numX + 1, numY, $squareNum);
				break;
			case 2:
				this._randomClickArray(numX, numY + 1, $squareNum);
				break;
			case 3:
				this._randomClickArray(numX, numY - 1, $squareNum);
				break;
				/*case 4:
					this._randomClickArray(numX + 1, numY - 1, $squareNum);
					break;
				case 5:
					this._randomClickArray(numX + 1, numY + 1, $squareNum);
					break;
				case 6:
					this._randomClickArray(numX - 1, numY - 1, $squareNum);
					break;
				case 7:
					this._randomClickArray(numX - 1, numY + 1, $squareNum);
					break;
				*/
			default:
				return;
				break;
		}

	},
	_checkMineMap: function _checkMineMap(x, y) {
		map[x][y] = -2;
	},

在这里,为了保证每一次点击的空白地区的大小和样子都不同,随机一个值来表示除了首次点击方块周围8个方块以外的多少个方块为-2,即该函数传递的arrayNum值,该函数第一次运行时,将点击处周围的8个方块与自身设置为-2,同时在上下左右四个方块中随机选择一个方块进行递归调用,每当非越界值的矩阵位置被赋值为-2时,都使arrayNum - 1并再次递归调用,当arrayNum为0时表示递归结束,此处为递归出口。其中无视回溯现象和越界现象(虽然在数学上有可能无限的选择边界值或者重复回溯导致递归无限,但是在计算机的运算速度中这种概率接近为0)。

 

初始的-2区域设置后,进行第一次赋值,-2区域是空白区,即在扫雷中属于周围一定不存在雷的区域,这里要用 -3状态码进行一次包围,标记-3是可能展示周围雷数字的区域但不存在有雷的可能性。

	_setMapClickArray: function _setMapClickArray() {
		var $num = this.$num;
		for(var i = 0; i < $num; i++) {
			for(var j = 0; j < $num; j++) {
				if(map[i][j] == -2) {
					try {
						//阻止扩展数组操作
						if(i != 17 && j == 17 && i != 0) {
							this._checkArrayNum(i + 1, j);
							this._checkArrayNum(i + 1, j - 1);
							this._checkArrayNum(i - 1, j);
							this._checkArrayNum(i - 1, j - 1);
						}
						if(j != 17 && i == 17 && j != 0) {
							this._checkArrayNum(i, j + 1);
							this._checkArrayNum(i - 1, j + 1);
							this._checkArrayNum(i, j - 1);
							this._checkArrayNum(i - 1, j - 1);
						}
						if(j != 17 && i == 0 && j != 0) {
							this._checkArrayNum(i, j + 1);
							this._checkArrayNum(i + 1, j + 1);
							this._checkArrayNum(i, j - 1);
							this._checkArrayNum(i + 1, j - 1);
						}
						if(i != 17 && j == 0 && i != 0) {
							this._checkArrayNum(i + 1, j);
							this._checkArrayNum(i + 1, j + 1);
							this._checkArrayNum(i - 1, j);
							this._checkArrayNum(i - 1, j + 1);
						}
						if(i == 17 && j == 17) {
							this._checkArrayNum(i, j - 1);
							this._checkArrayNum(i - 1, j);
							this._checkArrayNum(i - 1, j - 1);
						}
						if(i == 0 && j == 0) {
							this._checkArrayNum(i, j + 1);
							this._checkArrayNum(i + 1, j + 1);
							this._checkArrayNum(i + 1, j);
						}
						if(i == 17 && j == 0) {
							this._checkArrayNum(i, j + 1);
							this._checkArrayNum(i - 1, j + 1);
							this._checkArrayNum(i - 1, j);
						}
						if(i == 0 && j == 17) {
							this._checkArrayNum(i, j - 1);
							this._checkArrayNum(i + 1, j - 1);
							this._checkArrayNum(i + 1, j);
						}
						if(j != 17 && i != 17 && i != 0 && j != 0) {
							this._checkArrayNum(i, j + 1);
							this._checkArrayNum(i + 1, j);
							this._checkArrayNum(i + 1, j + 1);
							this._checkArrayNum(i - 1, j + 1);
							this._checkArrayNum(i + 1, j - 1);
							this._checkArrayNum(i - 1, j);
							this._checkArrayNum(i, j - 1);
							this._checkArrayNum(i - 1, j - 1);
						}
					} catch(e) {
						//无视越界操作
					}
				}
			}
		}
	},
	_checkArrayNum: function _checkArrayNum(numX, numY) {
		if(map[numX][numY] == -2) return;
		else map[numX][numY] = -3;
	},

同样,这里是遍历数组,找出每一个-2的位置,通过边界值鉴定使它的八方都为 -3,注意一定要边界值鉴定,js的特性不会使数组越界,而是扩展数组本身,这有一定可能性污染地图数组(虽然实际上我每一次遍历都是来自于初始化时设置的$num值,而非array.length,不会存在污染的可能性)。

 

包边处理后,进行第二次随机,即将雷随机分配到非雷域(0值区中),这里用 -1 表示雷的状态码

//调用方式
var randomNumX = parseInt(Math.random() * 17);
var randomNumY = parseInt(Math.random() * 17);
for(var i = 0; i < $Mnum; i++) {
	this._randomArray(randomNumX, randomNumY);
}


_randomArray: function _randomArray(numX, numY) {
		var randomNumX = numX;
		var randomNumY = numY;
		//console.log(map);
		if(randomNumX === 18) randomNumX = 17;
		if(randomNumY === 18) randomNumY = 17;
		if(map[randomNumX][randomNumY] == 0) {
			map[randomNumX][randomNumY] = -1;
			return;
		} else {
			randomNumX = Math.floor(Math.random() * 18);
			randomNumY = Math.floor(Math.random() * 18);
			this._randomArray(randomNumX, randomNumY);
		}
	},

这里初始化设置了一个$Mnum表示雷的总个数,每设置一个雷都会调用一次_randomArray函数,每次都会随机一个位置作为设置雷的位置,若这个位置的状态码不为0,则递归至随机寻找到一个为0的数组位置,保证了雷的个数。

 

随机设置了雷的位置后,进行第二次赋值,这一次将在 -3(被标记为可为展示周围数字但不能为雷的方块)与0(初始方块)中演算其八方的雷的个数,并将个数值赋给数组。

//二次赋值
//雷域
for(var i = 0; i < $num; i++) {
	for(var j = 0; j < $num; j++) {
		if(map[i][j] == 0 || map[i][j] == -3) {
		this._setMap(i, j);
	    }	
    }		
}
_setMap: function _setMap(numX, numY) {
		if(map[numX][numY] != 0 && map[numX][numY] != -3) return;
		map[numX][numY] = 0;
		var coutNum = 0;
		//八向寻值
		//边框判定
		//边框值 numX17 numY17 numX0 numY0
		if(numX == 0 && numY != 0 && numY != 17) {
			//右
			if(map[numX + 1][numY] === -1) coutNum++;
			//下
			if(map[numX][numY + 1] === -1) coutNum++;
			//上
			if(map[numX][numY - 1] === -1) coutNum++;
			//右上
			if(map[numX + 1][numY - 1] === -1) coutNum++;
			//右下
			if(map[numX + 1][numY + 1] === -1) coutNum++;
		} else if(numX == 0 && numY == 0) {
			//右
			if(map[numX + 1][numY] === -1) coutNum++;
			//右下
			if(map[numX + 1][numY + 1] === -1) coutNum++;
			//下
			if(map[numX][numY + 1] === -1) coutNum++;
		} else if(numX != 0 && numY == 0 && numX != 17) {
			//左下
			if(map[numX - 1][numY + 1] === -1) coutNum++;
			//左
			if(map[numX - 1][numY] === -1) coutNum++;
			//右
			if(map[numX + 1][numY] === -1) coutNum++;
			//右下
			if(map[numX + 1][numY + 1] === -1) coutNum++;
			//下
			if(map[numX][numY + 1] === -1) coutNum++;
		} else if(numX == 17 && numY == 17) {
			//上
			if(map[numX][numY - 1] === -1) coutNum++;
			//左
			if(map[numX - 1][numY] === -1) coutNum++;
			//左上
			if(map[numX - 1][numY - 1] === -1) coutNum++;
		} else if(numX == 17 && numY == 0) {
			//左
			if(map[numX - 1][numY] === -1) coutNum++;
			//下
			if(map[numX][numY + 1] === -1) coutNum++;
			//左下
			if(map[numX - 1][numY + 1] === -1) coutNum++;
		} else if(numX == 0 && numY == 17) {
			//右
			if(map[numX + 1][numY] === -1) coutNum++;
			//上
			if(map[numX][numY - 1] === -1) coutNum++;
			//右上
			if(map[numX + 1][numY - 1] === -1) coutNum++;
		} else if(numY == 17 && numX != 0 && numX != 17) {
			//左
			if(map[numX - 1][numY] === -1) coutNum++;
			//上左
			if(map[numX - 1][numY - 1] === -1) coutNum++;
			//右
			if(map[numX + 1][numY] === -1) coutNum++;
			//上
			if(map[numX][numY - 1] === -1) coutNum++;
			//右上
			if(map[numX + 1][numY - 1] === -1) coutNum++;
		} else if(numY != 0 && numY != 17 && numX == 17) {
			//左
			if(map[numX - 1][numY] === -1) coutNum++;
			//左下
			if(map[numX - 1][numY + 1] === -1) coutNum++;
			//左上
			if(map[numX - 1][numY - 1] === -1) coutNum++;
			//下
			if(map[numX][numY + 1] === -1) coutNum++;
			//上
			if(map[numX][numY - 1] === -1) coutNum++;
		} else {
			//左
			if(map[numX - 1][numY] === -1) coutNum++;
			//右
			if(map[numX + 1][numY] === -1) coutNum++;
			//下
			if(map[numX][numY + 1] === -1) coutNum++;
			//上
			if(map[numX][numY - 1] === -1) coutNum++;
			//右上
			if(map[numX + 1][numY - 1] === -1) coutNum++;
			//右下
			if(map[numX + 1][numY + 1] === -1) coutNum++;
			//左下
			if(map[numX - 1][numY + 1] === -1) coutNum++;
			//左上
			if(map[numX - 1][numY - 1] === -1) coutNum++;
		}
		map[numX][numY] = coutNum;
	},

至此初次的鉴定与矩阵的初始化完成,算法部分实现了大部分,以后用户的每一次操作都是在对这个矩阵进行状态演变。


扫雷完整项目github地址    https://github.com/xxx407410849/MinesSweeper

若本文对您有帮助请给我的git项目加个星星哦

你可能感兴趣的:(前端,javascrpit,H5游戏,算法)