【多是個假前端】掃雷之平鋪算法

前言

做爲一名前端攻城獅,寫個假前端的 Topic ,什麼鬼?你說一個好好的前端不作,搞什麼假前端?前端

問號

FBI WARNING:正宗的前端知識移步專欄裏隔壁大神系列vue

FBI WARNINGes6

If you want authentic front-end knowledge, get out and turn left, see Lao Wang.算法

言歸正傳,這個 Topic 系列的文章我會盡可能多說一些可能與前端知識關係不太大但很是有意思的東西,是但願將本身實踐中遇到的一些邏輯問題和算法問題以及一些其餘知識與你們分享。canvas

掃雷

在工做之餘,有課外開發的習慣。目的是將本身從重複的業務代碼中擺脫出來,作一些有意思的東西。小時候很是愛玩的一個遊戲就是掃雷,因而就有了這個系列的第一個文章集 -- 假前端之掃雷系列。數組

規則小結

在我寫出這個東西之後,找了一些同窗去玩:瀏覽器

「掃雷啊~~不會啊~」數據結構

「不知道怎麼玩~~之前都是瞎點。」dom

鑑於此,科普一些規則,熟悉的同窗跳過:性能

掃雷就是點格子!!!

固然,仍是有點技巧的:格子數字表明周圍一圈 8 個格子裏面藏着多少個雷。

不當心暴露了單身 20 年的手速~

初始化遊戲實現

不少同窗用 canvas 來實現遊戲,其緣由是方便數據渲染視圖和遊戲狀態的刷新。

這個系列的掃雷,我起了 vue 來實現個人數據與視圖的同步,爲何不用 canvas ?假前端系列將來會有一大波 canvas 的文章,就暫時不用再這裏了。

畫格子

這一步,就要考慮咱們要用一個怎樣的數據結構來表示整個遊戲雷區。

沒錯,一個二維數組。可是咱們能不能就用一個一維數組實現呢?徹底能夠。這裏,咱們就用二維數組來實現,直觀。

const SAFE_CELL = 0

export default {
  // ...
  // 生成一個 15 行 10 列的二位數組
  data () {
    return {
      dataList: (new Array(15))
        .fill(0)
        .map(
          it => (new Array(10))
          .fill(SAFE_CELL)
        )
    }
  }
  // ...
}
複製代碼

咱們設置了 const SAFE_CELL = 0 做爲默認填充,這個 0 表示周圍都沒有地雷,這樣一個空白的地雷區域就出現了。

平鋪算法佈雷

遊戲中,地雷的分佈是徹底了隨機的。須要你經過必定的邏輯判斷找出來。這裏,我選擇了 const MINE_CELL = 9 做爲地雷標識,緣由是 1~8 做爲標識周圍雷數須要用到,這個 1~8 的數字就是標識周圍有多少個地雷。

那麼,咱們如何將必定數量的地雷隨機分佈到整個二維數組中呢?

這裏的隨機分佈的要求很簡單:

  1. 雷的數量是固定的
  2. 每一個格子是不是雷的機率是同樣的

可能首先想到的方法,是【替換法】:在這個二維數組裏隨機找出不是地雷的格子,將其替換成地雷就能夠了。

看起來彷佛是不錯的方案。實際上, 是有問題的 。問題在哪?

若是雷數密度到達必定高度,挑出一個不是地雷的格子是至關困難的,例如:一個 10 * 10 的雷區,裏面有 99 個地雷,那麼第 99 個地雷想找出剩下的兩個 SAFE_CELL 很是困難,若是進行判斷:是地雷,再從新隨機挑選一個格子。不只消耗時間,還很容易進入一個死循環,這個方案只能放棄。

那麼我不進行替換,有沒有別的方法?

【插入法】,生成一個 SAFE_CELL 數量的一維數組,將雷隨機插入到數組中,再裂成一個二維數組。例如 10 * 10 的雷區有 10 個雷,我先生成長度 90 的覺得數組,再將 10 個地雷隨機插入到數組中,最後裂成一個 10 * 10 的二維數組。

看似完美解決了無限循環的問題,可是咱們知道,對數組元素進行添刪操做是很是消耗性能的,咱們在數組中增減一個元素,其後的每個元素的下表隨之須要移位。

這裏,我介紹下個人平鋪思路:

生成一個包含全部地雷和空白區域的一維有序數組, 利用 洗牌算法 將數組的順序打亂,最後裂成二維數組。

const SAFE_CELL = 0
const MINE_CELL = 9

export default {
  methods: {
    //...
    // 初始化數據
    initData () {
      const rows = 15
      const cols = 10
      const mines = 10
      const safeCellNum = rows * cols - mines
      const safeArea = (new Array(safeCellNum)).fill(SAFE_CELL)
      const mineArea = (new Array(mines)).fill(MINE_CELL)
      let totalArea = safeArea.concat(mineArea)
      totalArea = this.mineShuffle(totalArea) // 洗牌
      this.dataList = totalArea.reduce((memo, curr, index) => {
        if (index % cols === 0) memo.push([curr])
        else memo[memo.length - 1].push(curr)
        return memo
      }, [])
    }
  }
}
複製代碼

這裏面涉及到一個洗牌算法,我簡單的介紹一下。前輩們實現的洗牌算法多種多樣,性能和效果各異。這裏我選用的是我認爲性能和效果兼優,實現也很是簡單的 Fisher–Yates shuffle 算法。若是你注意過 lodash 源碼的話,lodash 裏面的 shuffle 也是用這個算法實現的

其思路就是從尾部開始將未打亂的元素與一個隨機的未打亂的剩餘元素進行調換,直到數組的全部元素都被打亂。

下面給出個人實現:

export default {
  methods: {
    //...
    // Fisher–Yates shuffle 算法
    mineShuffle (array, mine = array.length) {
      let count = mine
      let index
      while (count) {
        index = Math.floor(Math.random() * count--)
        ;[array[count], array[index]] = [array[index], array[count]]
      }
      return array
    }
  }
}
複製代碼

代碼中,元素調換是利用 es6 的解構賦值,因爲是就地調換元素的值,因此不存在性能問題。

圖中對比明顯能夠看出: 百萬長度的數組,在瀏覽器環境下 Fisher-Yates 洗牌算法穩定在 70 ms 左右;而一樣是 O(n) 時間複雜度的插入算法,在處理一樣長度的數組時,性能落後很是多!

完成洗牌後,咱們將數組裂爲二維數組交給 vue 渲染。此時,咱們的視圖呈現:

計算環境數字

雷區有了地雷,咱們就該計算地雷周圍的環境數字了,這個數字的意義是標識這個數字周圍隱藏着多少個地雷,這個在規則一節中有講。

計算環境數字很簡單,循環一遍二維數組,若是遇到這個格子是個地雷,周圍全部個字的數字 +1 就好了。注意格子在邊緣的狀況。

const AROUND = [
  [-1, -1],
  [-1, 0],
  [-1, 1],
  [0, -1],
  [0, 1],
  [1, -1],
  [1, 0],
  [1, 1]
]
const MINE_CELL = 9

export default {
  methods: {
    // ...
    // 設置環境數字
    setEnvNum () {
      this.dataList.forEach((row, rowIndex) => {
        row.forEach((cell, colIndex) => {
          if (cell === MINE_CELL) {
            AROUND.forEach(offset => {
              const row = rowIndex + offset[0]
              const col = colIndex + offset[1]
              if (
                this.dataList[row] &&
                this.dataList[row][col] !== undefined &&
                this.dataList[row][col] !== MINE_CELL
              ) this.dataList[row][col]++
            })
          }
        })
      })
    }
  }
}
複製代碼

此時:

小總結

到此爲止,咱們終於生成個一個掃雷的初始雷區,包含了隨機分佈的地雷以及地雷周圍正確的數字。

在實現的過程當中,咱們作一下總結:

  1. 先思考再動手。
  2. 時常保持對代碼邏輯邊際狀況的考慮,多想一想這麼寫會怎麼崩潰。
  3. 把握好細節

下一步,咱們須要的是給雷區裏的格子加上各類狀態,來隱藏地雷。同時給格子們綁上事件。

在綁定事件的過程當中,又有好玩的思考。期待一下吧。

相關文章
相關標籤/搜索