[ 邏輯鍛鍊] 用 JavaScript 做一個小遊戲 ——2048 (詳解版)

yhtx1997發表於2019-01-20

前言

  • 這次使用了 vue 來編寫 2048,主要目的是溫習一下 vue。
  • 但是好像沒有用到太多 vue 的東西,==! 估計可能習慣了不用框架吧
  • 之前由於時間關係沒有對實現過程詳細講解,本次會詳細講解下比較繞的函式
  • 由於篇幅問題簡單的函式就不做詳解了
  • 程式碼地址: github.com/yhtx1997/Sm…

實現功能

  1. 數字合併
  2. 當前總分計算
  3. 沒有可移動的數字時不進行任何操作
  4. 沒有可移動,可合併的數字,並且不能新建時遊戲失敗
  5. 達到 2048 結束遊戲

用到的知識

  1. ES6
  2. vue 部分模板語法
  3. vue 生命週期
  4. 陣列方法
    1. reverse()
    2. push()
    3. unshift()
    4. some()
    5. forEach()
    6. reduceRight()
  5. 數學方法
    1. Math.abs()
    2. 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 // 最高分數
    }
  }
複製程式碼

做好初始化看起來應該是這樣的效果

init.png

新增事件監聽

在 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
    }
複製程式碼

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的機率我改的比較大,所以相應的降低了一些難度,具體體現在當所有數字都在左邊(最邊上),且數字與數字間沒有空隙,再按左也會生成數字

相關文章