[ 邏輯鍛鍊] 用 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的概率我改的比較大,因此相應的下降了一些難度,具體體如今當全部數字都在左邊(最邊上),且數字與數字間沒有空隙,再按左也會生成數字

相關文章
相關標籤/搜索