前言
- 這次使用了 vue 來編寫 2048,主要目的是溫習一下 vue。
- 但是好像沒有用到太多 vue 的東西,==! 估計可能習慣了不用框架吧
- 之前由於時間關係沒有對實現過程詳細講解,本次會詳細講解下比較繞的函式
- 由於篇幅問題簡單的函式就不做詳解了
- 程式碼地址: github.com/yhtx1997/Sm…
實現功能
- 數字合併
- 當前總分計算
- 沒有可移動的數字時不進行任何操作
- 沒有可移動,可合併的數字,並且不能新建時遊戲失敗
- 達到 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的機率我改的比較大,所以相應的降低了一些難度,具體體現在當所有數字都在左邊(最邊上),且數字與數字間沒有空隙,再按左也會生成數字