[ 逻辑锻炼] 用 JavaScript 作一个小游戏 ——2048 (详解版)

前言

  • 此次使用了 vue 来编写 2048,主要目的是温习一下 vue。
  • 可是好像没有用到太多 vue 的东西,==! 估计可能习惯了不用框架吧
  • 以前因为时间关系没有对实现过程详细讲解,本次会详细讲解下比较绕的函数
  • 因为篇幅问题简单的函数就不作详解了
  • 代码地址: https://github.com/yhtx1997/S...

实现功能

  1. 数字合并
  2. 当前总分计算
  3. 没有可移动的数字时不进行任何操做
  4. 没有可移动,可合并的数字,而且不能新建时游戏失败
  5. 达到 2048 结束游戏

用到的知识

  1. ES6
  2. vue 部分模板语法
  3. vue 生命周期
  4. 数组方法css

    1. reverse()
    2. push()
    3. unshift()
    4. some()
    5. forEach()
    6. reduceRight()
  5. 数学方法html

    1. Math.abs()
    2. Math.floor()

具体实现

  • 是否须要将上下操做转换为左右操做
  • 数据初始化
  • 合并数字
  • 判断操做是否无效
  • 渲染到页面
  • 随机建立数字
  • 计算总分
  • 判断成功
  • 判断失败

整体流程以下所示vue

command (keyCode) { // 总部
      this.WhetherToRotate(keyCode) // 是否须要将上下操做转换为左右操做
      this.Init() // 数据初始化 合并数字
      this.IfInvalid() // 判断是否无效
      this.Rendering(keyCode) // 渲染到页面
    }

初始化

首先先将基本的 HTML 标签跟 CSS 样式写出来git

因为用的 vue ,因此渲染 html 部分的代码不用咱们去手写github

<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因为太长就不放了跟以前基本没有太多区别数组

接下来是数据的初始化app

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 // 最高分数
    }
  }

作好初始化看起来应该是这样的效果
init.png框架

添加事件监听

在 mounted 添加事件监听 dom

为何在 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
    }

zhuanhuan.png

数据初始化

  • 数组中的 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 下标
         // 判断是否是最边上
         // 有不是的 说明边上 至少有一个空隙
         // 是的话说明边上没有空隙
       }

这样就将基本的判断是否有效,是否失败的条件都获得了
至因而否有可合并数字已经在数据初始化时就获得了

如今全部数据应该是这样的
1.png

渲染页面

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的概率我改的比较大,因此相应的下降了一些难度,具体体如今当全部数字都在左边(最边上),且数字与数字间没有空隙,再按左也会生成数字

相关文章
相关标签/搜索