介绍
大家好,我是大胆的番茄。本文是「挑战 100 款小游戏」的第二款游戏:「扫雷」的总结与分享。
演示地址:https://wanghaida.com/demo/2202-minesweeper/index.html
Github 仓库:https://github.com/wanghaida/games/tree/master/2202-minesweeper
一个游戏要想好玩,除了玩法以外,设计也是必须的。但我不会设计怎么办?那就把 Microsoft Store 里的 Microsoft Minesweeper 下载下来翻翻目录,就发现了个这个:
嚯~,这不是我们熟悉的雪碧图吗?虽然不是全部的,但基本的都在。爱了爱了~
地图
雪碧图上每个方块正好 84x84 像素,为了高清屏,除以 2,每个方块是 42x42。游戏有 9x9、16x16、30x16 三种难度,按 30x16 来算,游戏区域大小达到了 1260x672,再包括一些小间距的话,小屏幕稍显吃力,所以除了 9x9 采用 84/2 = 42 像素以外,其他的用 84/2.4 = 35 像素。
用网格简单的把架子搭一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| :root { --base: 2;
--row: 9; --col: 9; }
#game { display: grid; grid-template-rows: repeat(var(--row), calc(84px / var(--base))); grid-template-columns: repeat(var(--col), calc(84px / var(--base))); gap: 2px;
div { background: url('./images/sprite_state.png') calc(-84px / var(--base)) 0 / calc(1344px / var(--base)) calc(420px / var(--base)) no-repeat; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const oGame = document.getElementById('game');
const minesweeper = {
initMap(row = 9, col = 9, mines = 10) { oGame.innerHTML = '';
document.documentElement.style.setProperty('--base', row > 9 ? '2.4' : '2');
document.documentElement.style.setProperty('--row', row); document.documentElement.style.setProperty('--col', col);
const oFragment = document.createDocumentFragment();
for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { const oDiv = document.createElement('div');
oFragment.appendChild(oDiv); } }
oGame.appendChild(oFragment); }, };
minesweeper.initMap();
|
得到下面一张图:
CSS 上使用了三个变量,--base
用来缩放每个方块大小的,--row
表示行,--col
表示列。还是咱们熟悉的网格系统,通过 repeat()
函数来画出行和列。
直接出来可不行,人家是有加载动效的,我准备加个关键帧动画和定时器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #game { .state-loading { background-position: calc(-84px / var(--base)) 0; animation: state-loading 0.2s steps(1); } .state-closed { background-position: calc(-84px / var(--base)) 0; } }
@keyframes state-loading { 0% { background-position: calc(-504px / var(--base)) calc(-84px / var(--base)); } 25% { background-position: calc(-672px / var(--base)) calc(-84px / var(--base)); } 50% { background-position: calc(-840px / var(--base)) calc(-84px / var(--base)); } 75% { background-position: calc(-924px / var(--base)) calc(-84px / var(--base)); } 100% { background-position: calc(-1260px / var(--base)) calc(-84px / var(--base)); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| const minesweeper = { mapCount: 0, mapTimer: null, initMap(row = 9, col = 9, mines = 10) { ...
document.documentElement.style.setProperty('--col', col);
const oFragment = document.createDocumentFragment();
for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { const oDiv = document.createElement('div');
oFragment.appendChild(oDiv); } }
oGame.appendChild(oFragment);
clearInterval(this.mapTimer); this.mapCount = 0; this.mapTimer = setInterval(() => { if (this.mapCount >= row) { return clearInterval(this.mapTimer); }
for (let i = 0; i < col; i++) { oDiv = oGame.children[this.mapCount * col + i];
oDiv.className = 'state-loading'; oDiv.addEventListener('animationend', function fn() { oDiv.className = 'state-closed'; oDiv.removeEventListener('animationend', fn); }); }
this.mapCount++; }, 100); }, };
|
理论上是没有问题的,但可能是 dom 的原因,在方块数量过多的时候,会有明显的从左向右加载的延迟,所以我由原来的先添加 div 进游戏区再给每行添加样式改成了每次添加一行 div 进游戏区同时添加样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| const minesweeper = { mapCount: 0, mapTimer: null, initMap(row = 9, col = 9, mines = 10) { ...
document.documentElement.style.setProperty('--col', col);
clearInterval(this.mapTimer); this.mapCount = 0; this.mapTimer = setInterval(() => { if (this.mapCount >= row) { return clearInterval(this.mapTimer); }
const oFragment = document.createDocumentFragment();
for (let i = 0; i < col; i++) { const oDiv = document.createElement('div');
oDiv.className = 'state-loading'; oDiv.addEventListener('animationend', function fn() { oDiv.className = 'state-closed'; oDiv.removeEventListener('animationend', fn); });
oFragment.appendChild(oDiv); }
oGame.appendChild(oFragment);
this.mapCount++; }, 100); }, };
|
为了方便操作方块,也为了性能稍微快一点,咱们用个 map 来存一下方块对应状态,dom 上只存数据对应坐标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| const minesweeper = {
map: [],
state: 'loading',
row: 9, col: 9, mines: 10, initMap(row = 9, col = 9, mines = 10) { this.map = []; this.row = row; this.col = col; this.mines = row * col === 256 ? 40 : row * col === 480 ? 99 : mines;
this.state = 'loading'; document.getElementById('mines').innerHTML = this.mines;
...
this.mapTimer = setInterval(() => { if (this.mapCount >= row) { this.state = 'loaded'; return clearInterval(this.mapTimer); }
const oFragment = document.createDocumentFragment();
const mapTemp = []; for (let i = 0; i < col; i++) { const oDiv = document.createElement('div');
...
oDiv.pos = [this.mapCount, i]; mapTemp.push({ isOpen: false, isCheck: false, isExplode: false, sign: 'normal', type: 0, });
oFragment.appendChild(oDiv); } this.map.push(mapTemp);
oGame.appendChild(oFragment);
this.mapCount++; }, 100); }, };
|
大体 ok:
基础动效
再把基础的空格、数字、旗子、问号效果弄出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
| .state-flag-down { background-position: calc(-1260px / var(--base)) calc(-168px / var(--base)); animation: state-flag-down 0.1s steps(1); } .state-flag-up { background-position: calc(-252px / var(--base)) calc(-252px / var(--base)); animation: state-flag-up 0.1s steps(1); } .state-normal-down { background-position: calc(-588px / var(--base)) calc(-168px / var(--base)); animation: state-normal-down 0.1s steps(1); } .state-normal-up { background-position: calc(-924px / var(--base)) calc(-168px / var(--base)); animation: state-normal-up 0.1s steps(1); } .state-question-down { background-position: calc(-588px / var(--base)) calc(-252px / var(--base)); animation: state-question-down 0.1s steps(1); } .state-question-up { background-position: calc(-924px / var(--base)) calc(-252px / var(--base)); animation: state-question-up 0.1s steps(1); }
.state-0 { background-position: 0 0; } .state-1 { background-position: calc(-504px / var(--base)) 0; } .state-2 { background-position: calc(-588px / var(--base)) 0; } .state-3 { background-position: calc(-672px / var(--base)) 0; } .state-4 { background-position: calc(-756px / var(--base)) 0; } .state-5 { background-position: calc(-840px / var(--base)) 0; } .state-6 { background-position: calc(-924px / var(--base)) 0; } .state-7 { background-position: calc(-1008px / var(--base)) 0; } .state-8 { background-position: calc(-1092px / var(--base)) 0; } .state-9 { background-position: calc(-336px / var(--base)) 0; }
@keyframes state-flag-down { 0% { background-position: calc(-1008px / var(--base)) calc(-168px / var(--base)); } 33.33% { background-position: calc(-1092px / var(--base)) calc(-168px / var(--base)); } 66.66% { background-position: calc(-1176px / var(--base)) calc(-168px / var(--base)); } 100% { background-position: calc(-1260px / var(--base)) calc(-168px / var(--base)); } } @keyframes state-flag-up { 0% { background-position: 0 calc(-252px / var(--base)); } 33.33% { background-position: calc(-84px / var(--base)) calc(-252px / var(--base)); } 66.66% { background-position: calc(-168px / var(--base)) calc(-252px / var(--base)); } 100% { background-position: calc(-252px / var(--base)) calc(-252px / var(--base)); } } @keyframes state-normal-down { 0% { background-position: calc(-336px / var(--base)) calc(-168px / var(--base)); } 33.33% { background-position: calc(-420px / var(--base)) calc(-168px / var(--base)); } 66.66% { background-position: calc(-504px / var(--base)) calc(-168px / var(--base)); } 100% { background-position: calc(-588px / var(--base)) calc(-168px / var(--base)); } } @keyframes state-normal-up { 0% { background-position: calc(-672px / var(--base)) calc(-168px / var(--base)); } 33.33% { background-position: calc(-756px / var(--base)) calc(-168px / var(--base)); } 66.66% { background-position: calc(-840px / var(--base)) calc(-168px / var(--base)); } 100% { background-position: calc(-924px / var(--base)) calc(-168px / var(--base)); } } @keyframes state-question-down { 0% { background-position: calc(-336px / var(--base)) calc(-252px / var(--base)); } 33.33% { background-position: calc(-420px / var(--base)) calc(-252px / var(--base)); } 66.66% { background-position: calc(-504px / var(--base)) calc(-252px / var(--base)); } 100% { background-position: calc(-588px / var(--base)) calc(-252px / var(--base)); } } @keyframes state-question-up { 0% { background-position: calc(-672px / var(--base)) calc(-252px / var(--base)); } 33.33% { background-position: calc(-756px / var(--base)) calc(-252px / var(--base)); } 66.66% { background-position: calc(-840px / var(--base)) calc(-252px / var(--base)); } 100% { background-position: calc(-924px / var(--base)) calc(-252px / var(--base)); } }
|
效果如下:
随机生成地雷
随机生成地雷比较简单,在 0 - row * col
生成 N 个不重复数字,再转换为二维坐标即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| const minesweeper = { initMap(row = 9, col = 9, mines = 10) { ... this.mapTimer = setInterval(() => { if (this.mapCount >= row) { this.state = 'loaded'; this.generateMines(); return clearInterval(this.mapTimer); } }, 100); },
generateMines() { let pos = []; while (pos.length !== this.mines) { pos = [...new Set([...pos, Math.floor(Math.random() * this.row * this.col)])]; }
for (let i = 0; i < pos.length; i++) { const x = Math.floor(pos[i] / this.col); const y = pos[i] % this.col;
pos[i] = [x, y]; this.map[x][y].type = 9; }
for (let i = 0; i < pos.length; i++) { const around = this.findPos(pos[i]); for (let j = 0; j < around.length; j++) { const grid = this.map[around[j][0]][around[j][1]]; if (grid.type !== 9) { grid.type++; } } } }, };
|
查找周围坐标
扫雷的数字标注是指示周围 8 格里的地雷数量,所以上面用到了个 findPos
方法就是用来返回周围坐标的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| const minesweeper = {
findPos([x, y]) { const pos = [ [x - 1, y - 1], [x - 1, y], [x - 1, y + 1], [x, y - 1], [x, y + 1], [x + 1, y - 1], [x + 1, y], [x + 1, y + 1], ]; return pos.filter(([x, y]) => !(x < 0 || y < 0 || x >= this.row || y >= this.col)); }, findPosUDLR([x, y]) { const pos = [ [x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1], ]; return pos.filter(([x, y]) => !(x < 0 || y < 0 || x >= this.row || y >= this.col)); }, };
|
而 findPosUDLR
用来返回上下左右 4 格,主要用来游戏结束后的爆炸效果,要不显得有点呆。
游戏逻辑分析
鼠标事件有按下、抬起、移动和双击。
这里为啥不把按下和抬起合并成单击事件呢?因为鼠标在一个方块左键按下不松手,移出当前方块的则需要进行恢复,主要是防止误点。而右键按下时再抬起时,需要进行标记变换(正常->旗子->问号->正常),所以不能简单的处理成单击事件,而且还得缓存一下按下的方块,在移动后、抬起时判定还是不是原来的方块。
至于双击事件,主要存在于双击数字时快速将未标记旗子的方块进行打开操作。
鼠标按下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| let oTemp = null;
oGame.addEventListener('mousedown', (ev) => { if (oGame === ev.target || ['loading', 'over'].includes(minesweeper.state)) return;
const [x, y] = ev.target.pos; if (false === minesweeper.map[x][y].isOpen) { oTemp = ev.target; oTemp.className = 'state-' + minesweeper.map[x][y].sign + '-down'; } });
|
如果没有点中方块,或者游戏加载中/游戏已结束则不再进行逻辑处理。其实这里有个 loading 时单击直接完成的效果,我这里也懒得写了,方法就是清掉 initMap
里的定时器,然后拿到 mapCount
直接进行剩下的 div.state-closed
填充。
先判断当前这个方块没有打开,然后缓存当前点击的方块,给方块添加一个按下的效果。
鼠标移动
1 2 3 4 5 6 7 8 9 10 11 12 13
| oGame.addEventListener('mousemove', (ev) => { if (oTemp && oTemp !== ev.target) { if (oTemp.className.match(/state\-.+\-down/)) { oTemp.className = oTemp.className.replace('-down', '-up'); } oTemp = null; } });
|
移动就是判定缓存 dom 存在且和当前元素不相等,且有按下的样式,就把按下样式变为了抬起。
鼠标抬起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| oGame.addEventListener('mouseup', (ev) => { if (oTemp !== ev.target) { oTemp = null; return; }
const [x, y] = ev.target.pos;
if (ev.button === 0) { if (minesweeper.map[x][y].sign === 'normal') { minesweeper.handleClick(oTemp); } else { oTemp.className = 'state-' + minesweeper.map[x][y].sign + '-up'; } } if (ev.button === 2) { minesweeper.map[x][y].sign = { flag: 'question', normal: 'flag', question: 'normal', }[minesweeper.map[x][y].sign];
if (minesweeper.map[x][y].sign === 'flag') { minesweeper.mines -= 1; } if (minesweeper.map[x][y].sign === 'question') { minesweeper.mines += 1; } document.getElementById('mines').innerHTML = minesweeper.mines;
oTemp.className = 'state-' + minesweeper.map[x][y].sign + '-up'; }
oTemp = null; });
|
先看单击事件,如果标记为 normal
,则进行逻辑处理,如果是 flag
或者 question
,则添加抬起事件不做任何处理。
右击事件,先将方块标记状态变更,添加抬起样式。如果变成了旗子就把地雷数量减一,旗子变成其他就把地雷数量加一。
鼠标双击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| oGame.addEventListener('dblclick', (ev) => { if (oGame === ev.target) return;
const [x, y] = ev.target.pos; const grid = minesweeper.map[x][y];
if (grid.isOpen && grid.type > 0 && grid.type < 9) { minesweeper.handleNumber([x, y], grid.type);
minesweeper.judgeVictory(); } });
|
游戏逻辑处理
这里我们看看具体的 handleClick
方法和 handleNumber
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| const minesweeper = {
handleClick(dom) { if (this.state !== 'ongoing') { this.state = 'ongoing'; this.startTime = +new Date(); this.startInterval(); }
const grid = this.map[dom.pos[0]][dom.pos[1]];
grid.isOpen = true; grid.isCheck = true; dom.className = 'state-' + grid.type;
if (grid.type === 0) { this.handleSpace(dom.pos); } else if (grid.type === 9) { this.handleMines([dom.pos]); }
this.judgeVictory(); }, };
|
首先就是判定状态了,如果当前不是游戏状态,就修改状态并开启计时。
拿到方块 grid
对应数据后,修改它的打开状态、递归状态和方块样式。
处理空白方块
如果是一个空白方块,那么除了它自身变化以外,还要向外扩展将所有空白方块和相邻的数字方块展示出来。
点击红点之后,判断红色方框内所有方格,碰到数字则跳过(1/2/3/9),碰到空白则递归(4/6/7)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| const minesweeper = {
handleSpace(pos) { const around = this.findPos(pos); for (let i = 0; i < around.length; i++) { const [x, y] = around[i]; const grid = this.map[x][y];
if (false === grid.isCheck && 'normal' === grid.sign) { grid.isOpen = true; grid.isCheck = true;
const oDiv = oGame.children[x * this.col + y]; oDiv.className = 'state-' + grid.sign + '-down'; oDiv.addEventListener('animationend', function fn() { oDiv.className = 'state-' + grid.type; oDiv.removeEventListener('animationend', fn); });
if (grid.type > 0 && grid.type < 9) { continue; } if (grid.type === 0) { this.handleSpace(around[i]); } } } }, };
|
处理地雷方块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| const minesweeper = {
handleMines(pos) { this.state = 'over'; oGame.className = 'fail'; clearInterval(this.startTimer);
for (let i = 0; i < this.map.length; i++) { for (let j = 0; j < this.map[i].length; j++) { if (this.map[i][j].type === 9 && this.map[i][j].sign !== 'flag') { oGame.children[i * this.col + j].className = 'state-9'; } if (this.map[i][j].type !== 9 && this.map[i][j].sign === 'flag') { oGame.children[i * this.col + j].className = 'state-flag-error'; } } }
for (let i = 0; i < pos.length; i++) { const [x, y] = pos[i]; const grid = this.map[x][y];
grid.isOpen = true; grid.isExplode = true;
const oDiv = oGame.children[x * this.col + y]; oDiv.className = 'state-over'; oDiv.addEventListener('animationend', function fn() { minesweeper.explodeMines(pos[i]); oDiv.removeEventListener('animationend', fn); }); } }, };
|
触碰到地雷肯定就 over 了,先标记出所有没有标记旗子的地雷,和所有错误的旗子。这个方法传递的坐标数组,因为双击数字触发有猜错多个地雷的可能,所以循环位置,给地雷添加打开、爆炸状态,并添加爆炸动画。
话说雪碧图里没有爆炸动画,哪来的素材呢?MSN 有这个游戏的 canvas 版本😉
我自己把素材保存后用 ps 弄了张雪碧图:
当爆炸动画结束后执行游戏结束动画 explodeMines
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| const minesweeper = {
explodeMines(pos) { setTimeout(() => { const around = this.findPosUDLR(pos); for (let i = 0; i < around.length; i++) { const [x, y] = around[i]; const grid = this.map[x][y];
if (grid.isExplode === false && grid.sign !== 'flag') { grid.isExplode = true;
const oDiv = oGame.children[x * this.col + y];
if (grid.type === 9) { grid.isOpen = true; oDiv.className = 'state-over'; } else if (!grid.isOpen) { oDiv.className = 'state-explode'; oDiv.addEventListener('animationend', function fn() { oDiv.className = 'state-closed'; oDiv.removeEventListener('animationend', fn); }); }
this.explodeMines(around[i]); } } }, 100); }, };
|
通过 findPosUDLR
来进行菱形扩散,碰到 normal
标记就变红一下,碰到地雷就执行爆炸效果,递归完成所有方块的遍历。
呆呆的 findPos
方法:
处理数字方块
还记得双击中进行了数字方块的处理吗?原本我也是放到 handleClick
中了,但刚一翻开就会进行逻辑处理很明显是个 bug 啊!
那数字模块都要处理什么?我们先看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| const minesweeper = {
handleNumber(pos, type) { const around = this.findPos(pos);
let flag = 0; const flags = []; const mines = [];
for (let i = 0; i < around.length; i++) { const [x, y] = around[i]; const grid = this.map[x][y];
if (grid.sign === 'flag') { flag++; flags.push({ ...grid, pos: around[i] }); } if (grid.type === 9) { mines.push({ ...grid, pos: around[i] }); } }
if (flag >= type) { if (JSON.stringify(flags) === JSON.stringify(mines)) { this.handleSpace(pos); } else { for (let i = 0; i < flags.length; i++) { if (flags[i].type === 9) continue;
oGame.children[flags[i].pos[0] * this.col + flags[i].pos[1]].className = 'state-flag-error'; } this.handleMines(mines.filter((item) => item.sign !== 'flag').map((item) => item.pos)); } } }, };
|
首先拿到数字的坐标 pos
和内容 type
,其实内容通过 pos
在 map
中取也行,不过外面我查到了,就顺手传进来了。
循环查找标记的数量,当旗子 flag
>= 数字 type
时,才能将剩余 normal
标记做点击处理。
先判断标记是否正确,因为都是一个顺序 push,所以可以简单的转字符串后比对,对比成功则将当前数字方块当一个空白方块处理。
标记错误就将错误的小旗子显示出来,将错误的地雷坐标扔到处理地雷方块的逻辑里去。
判断游戏胜利
胜利的条件是 未打开的方块 === 旗子的数量 + 地雷的数量
,所以单击一个方块和双击一个数字方块都应进行游戏胜利的判定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const minesweeper = {
judgeVictory() { let count = 0; let flags = 0;
for (let i = 0; i < this.map.length; i++) { for (let j = 0; j < this.map[i].length; j++) { if (this.map[i][j].isOpen === false) { count++; } if (this.map[i][j].sign === 'flag') { flags++; } } }
if (count === flags + this.mines) { this.state = 'over'; oGame.className = 'success'; clearInterval(this.startTimer); } }, };
|
结尾
其实还有亿点点细节我觉得简单就没有在文章当中展示,感兴趣可以去 github 翻翻源码,对比着看会更好理解一点。
包括花园主题和所有音频其实也在游戏文件夹中,可以自己添加。这里再放一张花园主题的雪碧图:
方法都一样,就是 css 的关键帧多写一些。
以上。