前言
- 这次使用了 vue 来编写 2048,主要目的是温习一下 vue。
- 但是好像没有用到太多 vue 的东西,==! 估计可能习惯了不用框架吧
- 之前由于时间关系没有对实现过程详细讲解,本次会详细讲解下比较绕的函数
- 由于篇幅问题简单的函数就不做详解了
- 代码地址: https://github.com/yhtx1997/S...
实现功能
- 数字合并
- 当前总分计算
- 没有可移动的数字时不进行任何操作
- 没有可移动,可合并的数字,并且不能新建时游戏失败
- 达到 2048 结束游戏
用到的知识
- ES6
- vue 部分模板语法
- vue 生命周期
-
数组方法
- reverse()
- push()
- unshift()
- some()
- forEach()
- reduceRight()
-
数学方法
- Math.abs()
- Math.floor()
具体实现
- 是否需要将上下操作转换为左右操作
- 数据初始化
- 合并数字
- 判断操作是否无效
- 渲染到页面
- 随机创建数字
- 计算总分
- 判断成功
- 判断失败
总体流程如下所示
command (keyCode) { // 总部
this.WhetherToRotate(keyCode) // 是否需要将上下操作转换为左右操作
this.Init() // 数据初始化 合并数字
this.IfInvalid() // 判断是否无效
this.Rendering(keyCode) // 渲染到页面
}
初始化
首先先将基本的 HTML 标签跟 CSS 样式写出来
由于用的 vue ,所以渲染 html 部分的代码不用我们去手写
<template>
<div id='app'>
<div class='total'>总分: {{this.total}} 分</div> // {{}} 这个中间表示 JavaScript 表达式
<div class='main'>
<div class='row' v-for='(items,index) of arr' :key='index'> // v-for表示循环渲染当前元素,具体渲染次数为 arr.length
<div
:class='`c-${item} item`'
v-for='(item,index) of items'
:key='index'
>{{item>0?item:''}}</div> // :class= 表示将 JavaScript 变量作为类名
</div>
</div>
<footer>
<h2>玩法说明:</h2>
<p>1.用键盘上下左右键控制数字走向</p>
<p>2.当点击了一个方向时,格子中的数字会全部往那个方向移动,直到不能再移动,如果有相同的数字则会合并</p>
<p>3.当格子中不再有可移动和可合并的数字时,游戏结束</p>
</footer>
</div>
</template>
css由于太长就不放了跟之前基本没有太多区别
接下来是数据的初始化
data () {
return {
arr: [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // 与页面绑定的数组
Copyarr: [[], [], [], []], // 用来数据操作的数组
initData: [], // 包含数字详细坐标的数组
haveGrouping: false, // 有可以合并的数字
itIsLeft: false, // 是否为向左合并,默认不是向左合并
endGap: true, // 判断最边上有没有空隙 默认有空隙
middleGap: true, // 真 为某行中间有空隙
haveZero: true, // 当前页面有没有 0
total: 0, // 总分数
itIs2048: false, // 是否成功
max: 2048 // 最高分数
}
}
添加事件监听
在 mounted 添加事件监听
为什么在 mounted 添加事件?
我们先了解下vue的生命周期
- beforeCreate 实例创建之前 在这个阶段我们写的代码还没有被运行
- created 实例创建之后 在这个阶段我们写的代码已经运行了但是还没有将 HTML 渲染到页面
- mounted 挂载之后 在这个阶段 html 渲染到页面了,可以取到 dom 节点
- beforeUpdate 数据更新前 在我们需要重新渲染 html 前调用 类似执行 warp.innerHTML = html; 之前
- updated 数据更新后 在重新渲染 HTML 后调用
- destroyed 实例销毁后调用 将我们写的代码丢弃掉后调用
- errorCaptured 当捕获一个来自子孙组件的错误时被调用 2.5.0+ 新增
- 注:我说的我们写的代码只是一种代指,是为了方便理解,并不是真正的指我们写的代码
所以如果太早的话可能找不到 dom 节点,太晚的话,可能不能第一时间进行事件的响应
mounted () {
window.onkeydown = e => {
switch (e.keyCode) {
case 37:
// ←
console.log('←')
this.Command(e.keyCode)
break
case 38:
// ↑
console.log('↑')
this.Command(e.keyCode)
break
case 39:
// →
this.Command(e.keyCode)
console.log('→')
break
case 40:
// ↓
console.log('↓')
this.Command(e.keyCode)
break
}
}
}
将操作简化为只有左右
这段代码我是某天半梦半醒想到的,可能思维不好转过来,可以看看代码下面的图
这样一来就将向上的操作转换成了向左的操作
向下的操作就转换成了向右的操作
这样折腾下可以少写一半的数字合并代码
WhetherToRotate (keyCode) { // 是否需要将上下操作转换为左右操作
if (keyCode === 38 || keyCode === 40) { // 38 是上 40 是下
this.Copyarr = this.ToRotate(this.arr)
} else if (keyCode === 37 || keyCode === 39) { // 37 是左 39 是右
[...this.Copyarr] = this.arr
}
// 将当前操作做一个标识
if (keyCode === 37 || keyCode === 38) { // 数据转换后只有左右操作
this.itIsLeft = true
} else if (keyCode === 39 || keyCode === 40) {
this.itIsLeft = false
}
}
转换代码
ToRotate (arr) { // 将数据从 x 到 y y 到 x 相互转换
let afterCopyingArr = [[], [], [], []]
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr[i].length; j++) {
afterCopyingArr[i][j] = arr[j][i]
}
}
return afterCopyingArr
}
数据初始化
- 数组中的 0 在这个小作品中仅用作占位,视为垃圾数据,所以开始前需要处理掉,在结束后再加上
- 两种数据格式,一种是包含详细信息的,用来做一些判断; 一种是纯数字的二维数组,之后用来从新渲染页面
Init () { // 数据初始化
this.initData = this.DataDetails() // 非零数字详情
this.Copyarr = this.NumberMerger() // 数字合并
}
判断是否无效
IfInvalid () { // 判断是否无效
// 判断每行中间有没有空隙
this.MiddleGap() // 真 为某行中间有空隙
this.EndPointGap() // 在没有中间空隙的条件下去判断最边上有没有空隙
}
- 判断两个数字之间有没有空隙
MiddleGap () { // 检查每行中间有没有空隙
// 当所有的数都是挨着的,那么 x 下标两两相减并除以组数得到的绝对数是 1 ,比他大说明中间有空隙
// 先将 x 下标两两相减 并添加到新的数组
let subarr = [[], [], [], []] // 两两相减的数据
let sumarr = [] // 处理后的最终数据
this.initData.forEach((items, index) => {
items.forEach((item, i) => {
if (typeof items[i + 1] !== 'undefined') {
subarr[index].push(item.col - items[i + 1].col)
}
})
})
// 将每一行的结果相加得到总和 然后除以每一行结果的长度
subarr.forEach((items) => {
sumarr.push(items.reduceRight((a, b) => a + b, 0))
})
sumarr = sumarr.map((item, index) => Math.abs(item / subarr[index].length))
// 最后判断有没有比 1 大的值
sumarr.some(item => item > 1)
this.middleGap = sumarr.some(item => item > 1) // 真 为 有中间空隙
}
-
判断数字有没有到最边上
EndPointGap () { // 检查最边上有没有空隙 // 判断是向左还是向右 因为左右的判断是不一样的 this.endGap = true let end let initData = this.initData if (this.itIsLeft) { end = 0 this.endGap = initData.some(items => items.length !== 0 ? items[0].col !== end : false) } else { end = 3 this.endGap = initData.some(items => items.length !== 0 ? items[items.length - 1].col !== end : false) } // 取出每行的第一个数的 x 下标 // 判断是不是最边上 // 有不是的 说明边上 至少有一个空隙 // 是的话说明边上没有空隙 }
这样就将基本的判断是否有效,是否失败的条件都得到了
至于是否有可合并数字已经在数据初始化时就得到了
渲染页面
Rendering (keyCode) {
this.AddZero() // 先将占位符加上
// 因为之前的数据都处理好了 所以只需要将上下的数据转换回去就好了
if (keyCode === 38 || keyCode === 40) { // 38 是上 40 是下
this.Copyarr = this.ToRotate(this.Copyarr)
}
if (this.haveGrouping || this.endGap || this.middleGap) { // 满足任一条件就说明可以新建随机数字
this.RandomlyCreate(this.Copyarr)
} else if (this.haveZero) {
// 都不满足 但是有空位不做失败判断
} else {
// 以上都不满足视为没有空位,不可合并
if (this.itIs2048) { // 判断是否达成2048
this.RandomlyCreate(this.Copyarr)
alert('恭喜达成2048!')
// 下面注释掉的可让游戏在点击弹框按钮后重新开始新游戏
// this.arr = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
// this.RandomlyCreate(this.arr)
} else { //以上都不满足视为失败
this.RandomlyCreate(this.Copyarr)
alert('游戏结束!')
// this.arr = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
// this.RandomlyCreate(this.arr)
}
}
if (this.itIs2048) { // 每次页面渲染完,都判断是否达成2048
this.RandomlyCreate(this.Copyarr)
alert('恭喜达成2048!')
// this.arr = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
// this.RandomlyCreate(this.arr)
}
}
- 随机空白处创建数字
这里之前是用递归函数的形式去判断,但是用递归函数的话会有很多问题,最大的问题就是可能会堆栈溢出,或者卡死(递归函数就是在函数的最后还会去调用自己,如果不给出 return 的条件,很容易堆栈溢出或卡死)
所以这次改成抽奖的模式,将所有的空位的坐标取到,放入一个数组,然后取这个数组的随机下标,这样我们会得到一个空位的坐标,然后再对这个空位进行处理
RandomlyCreate (Copyarr) { // 随机空白处创建新数字
// 判断有没有可以新建的地方
let max = this.max
let copyarr = Copyarr
let zero = [] // 做一个抽奖的箱子
let subscript = 0 // 做一个拿到的奖品号
let number = 0 // 奖品号兑换的物品
// 找到所有的 0 将下标添加到新的数组
copyarr.forEach((items, index) => {
items.forEach((item, i) => {
if (item === 0) {
zero.push({ x: index, y: i })
}
})
})
// 取随机数 然后在空白坐标集合中找到它
subscript = Math.floor(Math.random() * zero.length)
if (Math.floor(Math.random() * 10) % 3 === 0) {
number = 4 // 三分之一的机会
} else {
number = 2 // 三分之二的机会
}
if (zero.length) {
Copyarr[zero[subscript].x][zero[subscript].y] = number
this.arr = Copyarr
}
this.total = 0
this.arr.forEach(items => {
items.forEach(item => {
if (item === max && !this.itIs2048) {
this.itIs2048 = true
}
this.total += item
})
})
}
以上就是本次 2048 的主要代码
最后,因为随机出现4的几率我改的比较大,所以相应的降低了一些难度,具体体现在当所有数字都在左边(最边上),且数字与数字间没有空隙,再按左也会生成数字
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。