javascript
2048小游戏html制作,[ 逻辑锻炼] 用 JavaScript 做一个小游戏 ——2048 (详解版)
前言
這次使用了 vue 來編寫 2048,主要目的是溫習(xí)一下 vue。
但是好像沒有用到太多 vue 的東西,==! 估計(jì)可能習(xí)慣了不用框架吧
之前由于時(shí)間關(guān)系沒有對(duì)實(shí)現(xiàn)過程詳細(xì)講解,本次會(huì)詳細(xì)講解下比較繞的函數(shù)
由于篇幅問題簡(jiǎn)單的函數(shù)就不做詳解了
實(shí)現(xiàn)功能
數(shù)字合并
當(dāng)前總分計(jì)算
沒有可移動(dòng)的數(shù)字時(shí)不進(jìn)行任何操作
沒有可移動(dòng),可合并的數(shù)字,并且不能新建時(shí)游戲失敗
達(dá)到 2048 結(jié)束游戲
用到的知識(shí)
ES6
vue 部分模板語法
vue 生命周期
數(shù)組方法
reverse()
push()
unshift()
some()
forEach()
reduceRight()
數(shù)學(xué)方法
Math.abs()
Math.floor()
具體實(shí)現(xiàn)
是否需要將上下操作轉(zhuǎn)換為左右操作
數(shù)據(jù)初始化
合并數(shù)字
判斷操作是否無效
渲染到頁面
隨機(jī)創(chuàng)建數(shù)字
計(jì)算總分
判斷成功
判斷失敗
總體流程如下所示
command (keyCode) { // 總部
this.WhetherToRotate(keyCode) // 是否需要將上下操作轉(zhuǎn)換為左右操作
this.Init() // 數(shù)據(jù)初始化 合并數(shù)字
this.IfInvalid() // 判斷是否無效
this.Rendering(keyCode) // 渲染到頁面
}
初始化
首先先將基本的 HTML 標(biāo)簽跟 CSS 樣式寫出來
由于用的 vue ,所以渲染 html 部分的代碼不用我們?nèi)ナ謱?/p>總分: {{this.total}} 分 // {{}} 這個(gè)中間表示 JavaScript 表達(dá)式// v-for表示循環(huán)渲染當(dāng)前元素,具體渲染次數(shù)為 arr.length
:class='`c-${item} item`'
v-for='(item,index) of items'
:key='index'
>{{item>0?item:''}}
// :class= 表示將 JavaScript 變量作為類名玩法說明:
1.用鍵盤上下左右鍵控制數(shù)字走向
2.當(dāng)點(diǎn)擊了一個(gè)方向時(shí),格子中的數(shù)字會(huì)全部往那個(gè)方向移動(dòng),直到不能再移動(dòng),如果有相同的數(shù)字則會(huì)合并
3.當(dāng)格子中不再有可移動(dòng)和可合并的數(shù)字時(shí),游戲結(jié)束
css由于太長(zhǎng)就不放了跟之前基本沒有太多區(qū)別
接下來是數(shù)據(jù)的初始化
data () {
return {
arr: [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // 與頁面綁定的數(shù)組
Copyarr: [[], [], [], []], // 用來數(shù)據(jù)操作的數(shù)組
initData: [], // 包含數(shù)字詳細(xì)坐標(biāo)的數(shù)組
haveGrouping: false, // 有可以合并的數(shù)字
itIsLeft: false, // 是否為向左合并,默認(rèn)不是向左合并
endGap: true, // 判斷最邊上有沒有空隙 默認(rèn)有空隙
middleGap: true, // 真 為某行中間有空隙
haveZero: true, // 當(dāng)前頁面有沒有 0
total: 0, // 總分?jǐn)?shù)
itIs2048: false, // 是否成功
max: 2048 // 最高分?jǐn)?shù)
}
}
做好初始化看起來應(yīng)該是這樣的效果
添加事件監(jiān)聽
在 mounted 添加事件監(jiān)聽
為什么在 mounted 添加事件?
我們先了解下vue的生命周期
beforeCreate 實(shí)例創(chuàng)建之前 在這個(gè)階段我們寫的代碼還沒有被運(yùn)行
created 實(shí)例創(chuàng)建之后 在這個(gè)階段我們寫的代碼已經(jīng)運(yùn)行了但是還沒有將 HTML 渲染到頁面
mounted 掛載之后 在這個(gè)階段 html 渲染到頁面了,可以取到 dom 節(jié)點(diǎn)
beforeUpdate 數(shù)據(jù)更新前 在我們需要重新渲染 html 前調(diào)用 類似執(zhí)行 warp.innerHTML = html; 之前
updated 數(shù)據(jù)更新后 在重新渲染 HTML 后調(diào)用
destroyed 實(shí)例銷毀后調(diào)用 將我們寫的代碼丟棄掉后調(diào)用
errorCaptured 當(dāng)捕獲一個(gè)來自子孫組件的錯(cuò)誤時(shí)被調(diào)用 2.5.0+ 新增
注:我說的我們寫的代碼只是一種代指,是為了方便理解,并不是真正的指我們寫的代碼
所以如果太早的話可能找不到 dom 節(jié)點(diǎn),太晚的話,可能不能第一時(shí)間進(jìn)行事件的響應(yīng)
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
}
}
}
將操作簡(jiǎn)化為只有左右
這段代碼我是某天半夢(mèng)半醒想到的,可能思維不好轉(zhuǎn)過來,可以看看代碼下面的圖
這樣一來就將向上的操作轉(zhuǎn)換成了向左的操作
向下的操作就轉(zhuǎn)換成了向右的操作
這樣折騰下可以少寫一半的數(shù)字合并代碼
WhetherToRotate (keyCode) { // 是否需要將上下操作轉(zhuǎn)換為左右操作
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
}
// 將當(dāng)前操作做一個(gè)標(biāo)識(shí)
if (keyCode === 37 || keyCode === 38) { // 數(shù)據(jù)轉(zhuǎn)換后只有左右操作
this.itIsLeft = true
} else if (keyCode === 39 || keyCode === 40) {
this.itIsLeft = false
}
}
轉(zhuǎn)換代碼
ToRotate (arr) { // 將數(shù)據(jù)從 x 到 y y 到 x 相互轉(zhuǎn)換
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
}
數(shù)據(jù)初始化
數(shù)組中的 0 在這個(gè)小作品中僅用作占位,視為垃圾數(shù)據(jù),所以開始前需要處理掉,在結(jié)束后再加上
兩種數(shù)據(jù)格式,一種是包含詳細(xì)信息的,用來做一些判斷; 一種是純數(shù)字的二維數(shù)組,之后用來從新渲染頁面
Init () { // 數(shù)據(jù)初始化
this.initData = this.DataDetails() // 非零數(shù)字詳情
this.Copyarr = this.NumberMerger() // 數(shù)字合并
}
判斷是否無效
IfInvalid () { // 判斷是否無效
// 判斷每行中間有沒有空隙
this.MiddleGap() // 真 為某行中間有空隙
this.EndPointGap() // 在沒有中間空隙的條件下去判斷最邊上有沒有空隙
}
判斷兩個(gè)數(shù)字之間有沒有空隙
MiddleGap () { // 檢查每行中間有沒有空隙
// 當(dāng)所有的數(shù)都是挨著的,那么 x 下標(biāo)兩兩相減并除以組數(shù)得到的絕對(duì)數(shù)是 1 ,比他大說明中間有空隙
// 先將 x 下標(biāo)兩兩相減 并添加到新的數(shù)組
let subarr = [[], [], [], []] // 兩兩相減的數(shù)據(jù)
let sumarr = [] // 處理后的最終數(shù)據(jù)
this.initData.forEach((items, index) => {
items.forEach((item, i) => {
if (typeof items[i + 1] !== 'undefined') {
subarr[index].push(item.col - items[i + 1].col)
}
})
})
// 將每一行的結(jié)果相加得到總和 然后除以每一行結(jié)果的長(zhǎng)度
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) // 真 為 有中間空隙
}
判斷數(shù)字有沒有到最邊上
EndPointGap () { // 檢查最邊上有沒有空隙
// 判斷是向左還是向右 因?yàn)樽笥业呐袛嗍遣灰粯拥?/p>
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)
}
// 取出每行的第一個(gè)數(shù)的 x 下標(biāo)
// 判斷是不是最邊上
// 有不是的 說明邊上 至少有一個(gè)空隙
// 是的話說明邊上沒有空隙
}
這樣就將基本的判斷是否有效,是否失敗的條件都得到了
至于是否有可合并數(shù)字已經(jīng)在數(shù)據(jù)初始化時(shí)就得到了
現(xiàn)在所有數(shù)據(jù)應(yīng)該是這樣的
渲染頁面
Rendering (keyCode) {
this.AddZero() // 先將占位符加上
// 因?yàn)橹暗臄?shù)據(jù)都處理好了 所以只需要將上下的數(shù)據(jù)轉(zhuǎn)換回去就好了
if (keyCode === 38 || keyCode === 40) { // 38 是上 40 是下
this.Copyarr = this.ToRotate(this.Copyarr)
}
if (this.haveGrouping || this.endGap || this.middleGap) { // 滿足任一條件就說明可以新建隨機(jī)數(shù)字
this.RandomlyCreate(this.Copyarr)
} else if (this.haveZero) {
// 都不滿足 但是有空位不做失敗判斷
} else {
// 以上都不滿足視為沒有空位,不可合并
if (this.itIs2048) { // 判斷是否達(dá)成2048
this.RandomlyCreate(this.Copyarr)
alert('恭喜達(dá)成2048!')
// 下面注釋掉的可讓游戲在點(diǎn)擊彈框按鈕后重新開始新游戲
// 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('游戲結(jié)束!')
// 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) { // 每次頁面渲染完,都判斷是否達(dá)成2048
this.RandomlyCreate(this.Copyarr)
alert('恭喜達(dá)成2048!')
// this.arr = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
// this.RandomlyCreate(this.arr)
}
}
隨機(jī)空白處創(chuàng)建數(shù)字
這里之前是用遞歸函數(shù)的形式去判斷,但是用遞歸函數(shù)的話會(huì)有很多問題,最大的問題就是可能會(huì)堆棧溢出,或者卡死(遞歸函數(shù)就是在函數(shù)的最后還會(huì)去調(diào)用自己,如果不給出 return 的條件,很容易堆棧溢出或卡死)
所以這次改成抽獎(jiǎng)的模式,將所有的空位的坐標(biāo)取到,放入一個(gè)數(shù)組,然后取這個(gè)數(shù)組的隨機(jī)下標(biāo),這樣我們會(huì)得到一個(gè)空位的坐標(biāo),然后再對(duì)這個(gè)空位進(jìn)行處理
RandomlyCreate (Copyarr) { // 隨機(jī)空白處創(chuàng)建新數(shù)字
// 判斷有沒有可以新建的地方
let max = this.max
let copyarr = Copyarr
let zero = [] // 做一個(gè)抽獎(jiǎng)的箱子
let subscript = 0 // 做一個(gè)拿到的獎(jiǎng)品號(hào)
let number = 0 // 獎(jiǎng)品號(hào)兌換的物品
// 找到所有的 0 將下標(biāo)添加到新的數(shù)組
copyarr.forEach((items, index) => {
items.forEach((item, i) => {
if (item === 0) {
zero.push({ x: index, y: i })
}
})
})
// 取隨機(jī)數(shù) 然后在空白坐標(biāo)集合中找到它
subscript = Math.floor(Math.random() * zero.length)
if (Math.floor(Math.random() * 10) % 3 === 0) {
number = 4 // 三分之一的機(jī)會(huì)
} else {
number = 2 // 三分之二的機(jī)會(huì)
}
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 的主要代碼
最后,因?yàn)殡S機(jī)出現(xiàn)4的幾率我改的比較大,所以相應(yīng)的降低了一些難度,具體體現(xiàn)在當(dāng)所有數(shù)字都在左邊(最邊上),且數(shù)字與數(shù)字間沒有空隙,再按左也會(huì)生成數(shù)字
總結(jié)
以上是生活随笔為你收集整理的2048小游戏html制作,[ 逻辑锻炼] 用 JavaScript 做一个小游戏 ——2048 (详解版)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 五种聚类方法
- 下一篇: SpringBoot充电桩平台