我正在参加 码上掘金体验活动,详情:show出你的创意代码块
一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第30天,点击查看活动详情。
前言
我之前介绍过使用【码上掘金】开发js native和react代码。今天,我们来使用petite-vue开发一个简易的扫雷小游戏
在线预览
关于petite-vue
大家可以看一下官方的github地址,我就不赘述了。petite-vue,我们今天基于其状态管理(reactive)来开发扫雷小游戏
初始化准备
首先我们需要引入petite-view的cdn至【码上掘金】,在设置中添加Script源,其cdn地址如下
https://unpkg.com/petite-vue@0.2.2/dist/petite-vue.iife.js
验证是否完成安装
验证代码
我们在MarkUp中添加代码如下
<div id="app">
<div v-scope>
<p>{{ count }}</p>
<p>{{ plusOne }}</p>
<button @click="increment">increment</button>
</div>
</div>
在Script中添加代码如下
const createApp = PetiteVue.createApp;
createApp({
// exposed to all expressions
count: 0,
// getters
get plusOne() {
return this.count + 1;
},
// methods
increment() {
this.count++;
},
})
.mount("#app");
然后我们点击运行按钮,在预览区域中显示如下
我们点击increment时对应的3和4区域会+1
注意点
- 大家一定要注意。我们需要在使用petite-vue编译的地方添加v-scope来指定该区域需要编译
- mount("#app"): 在这里我们定义渲染的地址,相当于petite-vue执行了document.getElementById的找到需要渲染的div 可以简单理解:为mount寻找页面地址,scope指定渲染区域
关于扫雷的代码实现
源码
我咋这里贴出源码来,大家有兴趣可以贴到自己的开发工具中运行一下 MarkUp
<div id="app">
<div v-scope>
<div>
<span id="textSpan">难度:</span>
<select
@change="store.setStage"
id="selectStyle"
:disabled="store.playFlag"
>
<option value="6">初级</option>
<option value="7">中级</option>
<option value="10">高级</option>
<option value="20">宗师级</option>
</select>
<button id="textSpan" v-if="!store.playFlag" @click="store.generateMine">
开始
</button>
</div>
<div v-if="store.mineArrr&&store.mineArrr.length>0">
<span v-for="(item,index) in store.mineArrr" id="table_span">
<span v-if="index%store.stage===0" id="td_div"></span>
<span id="td_span" :disabled="item.open" @click="store.open(index)">
<span v-if="item.open"
>{{item.mine==="M"?"雷":item.mineNum>0?item.mineNum:"无"}}
</span>
<span v-else>?</span>
</span>
</span>
</div>
</div>
</div>
Style
#app {
text-align: center;
}
#selectStyle {
width: 100px;
height: 40px;
font-size: 20px;
}
#textSpan {
font-size: 20px;
height: 40px;
}
#inputSpan {
font-size: 20px;
height: 40px;
width: 60px;
}
#td_span {
display: inline-block;
width: 40px;
height: 40px;
border: 2px solid black;
}
#td_div {
display: block;
}
#table_span {
width: 64px;
}
Script
const reactive = PetiteVue.reactive;
function randomNum(minNum, maxNum) {
switch (arguments.length) {
case 1:
return parseInt(Math.random() * minNum + 1, 10);
break;
case 2:
return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
break;
default:
return 0;
break;
}
}
function incrMineNum(curTd) {
if (curTd && curTd.mine === "E") {
curTd.mineNum = curTd.mineNum + 1;
}
}
const store = reactive({
stage: 6,
playFlag: false,
mineArrr: [],
setStage(e) {
this.mineArrr= []
this.stage = e.target.value;
},
generateMine() {
this.playFlag = true;
let mineCount = this.stage;
let mineArrr = [];
let mineIndexArr = [];
for (let index = 0; index < mineCount; index++) {
let trArrr = [];
let mineIndex = randomNum(1, mineCount);
mineIndex = mineIndex - 1;
for (let j = 0; j < mineCount; j++) {
let jIndex = index * mineCount + j;
if (j === mineIndex) {
mineIndexArr.push(jIndex);
mineArrr.push({
index: jIndex,
mine: "M",
open: false,
mineNum: 0,
});
} else {
mineArrr.push({
index: jIndex,
mine: "E",
open: false,
mineNum: 0,
});
}
}
}
for (let index in mineIndexArr) {
let mineIndex = mineIndexArr[index];
if (mineIndex % mineCount !== 0) {
incrMineNum(mineArrr[mineIndex - 1]);
incrMineNum(mineArrr[mineIndex - mineCount - 1]);
incrMineNum(mineArrr[mineIndex + Number(mineCount) - 1]);
}
if ((mineIndex + 1) % mineCount !== 0) {
incrMineNum(mineArrr[mineIndex + 1]);
incrMineNum(mineArrr[mineIndex - mineCount + 1]);
incrMineNum(mineArrr[mineIndex + Number(mineCount) + 1]);
}
incrMineNum(mineArrr[mineIndex - mineCount]);
incrMineNum(mineArrr[mineIndex + Number(mineCount)]);
}
this.mineArrr = mineArrr;
},
open(index) {
let mineArrr = this.mineArrr;
let mine = mineArrr[index].mine;
if (mine === "M") {
mineArrr.forEach((element) => {
element.open = true;
});
//alert("游戏结束,你输了");
this.mineArrr = mineArrr;
this.playFlag = false;
return;
}
if (mine === "E") {
mineArrr[index].open = true;
}
this.mineArrr = mineArrr;
},
});
PetiteVue.createApp({
store,
}).mount("#app");
实现逻辑
我先简单介绍一下我的游戏逻辑:用户首先选择level(不选择时为初级),然后点击开始,出现棋盘,点击之后显示地雷的数量。如果是地雷则游戏结束 下面我介绍一下我的实现逻辑
1:选择level,开始游戏
开始游戏对应的div是我们红框中的代码,主要有三个地方的逻辑需要说明一下
- 我们将level设置为状态管理器托管数据,在点击之后会重新设值。大家可以看到我在setStage的地方又重置了一个数组。这是因为当我们结束游戏时切换等级时就算用户不点击开始也会触发stage的状态改变,我们的棋盘会重新渲染。所以我设置如果用户结束游戏时重新选择等级时重置数组,防止错误渲染
- 我添加了一个playFlag用来表示是否在游戏中,如果在游戏中,开始按钮不可见,不可以切花等级
- 点击开始按钮生成棋盘数据
2:关于棋盘的算法
在介绍算法之前,我先介绍一下卫为什么我要做一个扫雷游戏。因为我在刷leetcode时,有一个扫雷算法(leetcode-cn.com/problems/mi…) 本来想根据这个算法来开发个小游戏的。但是发现不太适用,所以我就自己写了更简单的算法
棋子数据算法
现在我们的棋盘和雷数是根据level来生成的。例如选择的初级会生成6x6的棋盘,每一行中随机生成一个雷。
我们先看一下生成棋子的算法
两个for循环用来生成行和列,其中index代表行,j代表列。我们使用随机算法算出一个1到列数之间的随机数,然后将它减去1,这个数就是我们当前行的随机雷。我们将当前行的随机数+当前行数x总列数就代表这个随机的雷位于第几个index。
例如:现在是6x6的level,当前在第三行生成随机数为4,则对应的index为2(index从0开始)x6+4=16
相邻的格子的雷数算法
我们在上一步计算出了所有的index,这一步我们根据index来算相邻格子的雷数
简单介绍一下格子雷数的算法
我们以普通的格子为例。要算的相邻格子的范围就是它相邻的8个格子,如下所示
列\行 | 上一列 | 本列 | 下一列 |
---|---|---|---|
上一行 | 1 | 2 | 3 |
本行 | 4 | 5 | 6 |
下一行 | 7 | 8 | 9 |
当前格子为5时需要计算1-4和6-9
最终我们的公式为
- 1对应:index-level-1
- 2对应:index-level
- 3对应:index-level+1
- 4对应:index-1
- 6对应:index+1
- 7对应:index+level-1
- 8对应:index+level
- 9对应:index+level+1 以4行x4列为例假设当前index在7时,需要计算
- 上一行的7-4-1至7-4+1即:2-4
- 本行的7-1和7+1即:6、8
- 下一行的7+4-1至7+4+1即:10-12 到这一步我们一斤可以实现周围周围的格子赋值了,但是我们需要处理特殊的格子
- 在最上侧和最下侧的格子。这两种类型的格子比较好处理,最上层的格子再向上取格子和最下层格子向下取格子时。计算出来的index在数组中是取不出来的。
例如:
计算4x4的index=2时计算2-4-1到2-4+1的所有index都是小于0,在数组中无法取出来。
计算4x4的index=11时计算11+4-1到11+4+1对应的元素大于16,在数组中也无法取出来\ - 在最左侧和最右侧的格子。这两种类型我的格子算起来比较费劲,我介绍一下实现的逻辑吧
最左侧的index%总列数必为0。这个逻辑是我们在生成格子的时候定义的,例如4x4中第一行为0-3、4-7等等,此时拿着最左侧的index和总列数求余必为0
最右侧的(index+1)%总列数必为0。这个和上边的逻辑差不多。假设在4x4中当前index为7。则(7+1)%4=0 所以我们最终的算法如下
\
3:用户点击计算
其实做完算法之后,我们的程序就完成90%+了,最终我们定义用户点击span时触发打开格子的方法。如果时地雷的话所有span不可点击,将所有的格子显示出来。
如果不是地雷的话显示当前格子相邻的地雷书,如果是0个的话就显示为"空"
最后在createApp中传入状态
PetiteVue.createApp({
store,
}).mount("#app");
结语
今天我们使用petite-vue开发了一个扫雷小游戏。一开始的时候我本来想引入element-plus的,但是发现它没有use方法。如果大家有自己的成熟ui框架(例如Bootstrap),想体验使用petite-vue开发的话,大家可以参考本篇文章来实践一下。 今天4月更文活动就结束了,5月份休息一下,补充一下taro跨端开发相关文章中缺失的scss代码。大家相约6月再见吧,欢迎大家多多点赞关注!