学校实训最后布置的一个答辩作业,周围同学都是在用资源做简单动画,突然想起大一的时候想用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轴上快,所以加载动画的渐变效果是倾斜的。
(加载过程中截图)
为判断是否为第一次点击,可以使用标志变量进行鉴定,向所有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项目加个星星哦