[ 逻辑锻炼] 用 JavaScript 做一个小游戏 ——2048 (详解版)
栏目: JavaScript · 发布时间: 5年前
内容简介:总体流程如下所示首先先将基本的 HTML 标签跟 CSS 样式写出来由于用的 vue ,所以渲染 html 部分的代码不用我们去手写
前言
- 这次使用了 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的几率我改的比较大,所以相应的降低了一些难度,具体体现在当所有数字都在左边(最边上),且数字与数字间没有空隙,再按左也会生成数字
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- SQL SERVER 2012新增函数之逻辑函数CHOOSE详解
- 详解RAC并发逻辑、硬件架构、软件架构拓扑与原理解析
- MySQL基础篇(05):逻辑架构图解和InnoDB存储引擎详解
- centos创建逻辑卷和扩容逻辑卷
- AI「王道」逻辑编程的复兴?清华提出神经逻辑机,已入选ICLR
- 内聚代码提高逻辑可读性,用MCVP接续你的大逻辑
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
RESTful Web Services Cookbook中文版
Subbu Allamaraju / 丁雪丰、常可 / 电子工业出版社 / 2011-9 / 59.00元
RESTful Web Services Cookbook中文版:REST最佳实践手册,ISBN:9787121143908,作者:(美)Subbu Allamaraju(沙布·阿拉马拉尤)著,丁雪丰,常可 译一起来看看 《RESTful Web Services Cookbook中文版》 这本书的介绍吧!