分享一個簡單的小遊戲,利用 BFS 尋找距離出口的最短路徑。javascript
遊戲規則以下:html
玩家點擊空白塊截住硬幣,雙方一次一步, 若硬幣到達邊界處則逃脫, 成功圍住硬幣則算贏。java
硬幣如何找到最近出口呢,這裏就用到了 BFS 搜索。git
首先咱們來看下 BFS ( 廣度優先搜索 ) 的描述:github
BFS 的思想是從一個頂點 V0 開始,輻射狀地優先遍歷其周圍較廣的區域。相似於樹的按層次遍歷的過程算法
這種搜索很適合咱們遊戲裏硬幣的尋找出口。瀏覽器
在我們遊戲中,每一個點通常有六個相鄰方向:ui
其中,尋找的規則大體以下:this
1. 從起點開始,查看周圍相鄰點是否存在出口;
2. 若不存在,標記每一個點爲已訪問,接着從每一個點的相鄰方向繼續尋找下去;
3. 直至找到一個出口或遍歷完全部相鄰點爲止。
複製代碼
搜索的過程如圖所示,從起點向外擴散:spa
算法代碼:
/** * BFS 求離出口最短路徑 * * 1. 初始化一個隊列,添加起點 * 2. 遍歷隊列中每個元素,與它周圍 6 個相鄰塊 * * 若沒遍歷過,查找周圍是否有出口 * * 若無出口,添加進隊列循環查找 * 3. 循環中止條件 * * 尋找到出口,即爲最短路徑,返回該出口 * * 全部相鄰點均遍歷,無出口,玩家勝利 ✌️ */
function getNearestSolver (point) {
const records = {}
// 將當前位置做爲隊列第一個元素
let arr = [point]
let des = null
// 若隊列還有元素,則繼續查找
while (arr.length) {
// 取出隊首元素
const V = arr.shift()
// 獲取隊首元素的相鄰節點並過濾障礙物和已訪問過的節點
const neighbours = getNeighbours(V).filter(p => typeof status[p] !== 'undefined' && !status[p].isWall && !status[p].hasVisited)
// 將節點添加進 records 對象中並指明來源節點,同時狀態設爲已訪問
neighbours.forEach(p => {
records[p] = { source: V }
status[p].hasVisited = true
})
// 查看相鄰節點中是否有邊界節點,即出口
const hasEdge = neighbours.find(p => status[p].isEdge)
arr = arr.concat(neighbours)
if (hasEdge) {
des = hasEdge
break
}
}
// 遍歷結束,重置元素訪問狀態
for (const s of Object.values(status)) {
s.hasVisited = false
}
// 返回查找的終點和路徑記錄對象
return { des, records }
}
複製代碼
根據終點倒推第一步的座標:
function getSolution (point) {
const [x, y] = point
const { des, records } = getNearestSolver(`${x},${y}`)
const sixDir = getNeighbours(`${x},${y}`)
const routes = []
let nextPos = des
// 反向尋找第一個座標
while (nextPos && !sixDir.includes(nextPos)) {
routes.push(nextPos)
nextPos = records[nextPos].source
}
return { nextPos, routes: routes.concat(nextPos) }
}
複製代碼
用於肯定給定點周圍六個相鄰塊的位置:
function getNeighbours (point) {
const split = point.split(',')
const x = +split[0]
const y = +split[1]
// 奇偶行共同的相鄰座標
const commonDir = [`${x},${y+1}`, `${x+1},${y}`, `${x},${y-1}`, `${x-1},${y}`]
// 因奇偶行偏移量致使不一樣的相鄰座標
const differentDir = y % 2 ? [`${x+1},${y+1}`, , `${x+1},${y-1}`] : [`${x-1},${y+1}`, `${x-1},${y-1}`]
return commonDir.concat(differentDir)
}
複製代碼
至此,JS 部分就結束了,當咱們隨意點擊棋盤上某一空白處時,該位置會變成障礙物,同時硬幣會將本身位置傳進 getSolution(point)
方法以肯定下一步行走的方向。
來看下效果吧:
最後,小小總結下 廣度優先搜索:
1. 查找與當前頂點相鄰的未訪問頂點,將其添加到隊列並標爲已訪問;
2. 從隊列中取出下一個頂點,繼續(1)的步驟,查找相鄰頂點是否存在目標頂點;
3. 循環往復,直至找到目標頂點或隊列爲空。
複製代碼
用一段通用代碼表示以下:
function BFS(s) {
const queue = []
s.visited = true
queue.push(s)
while (queue.length) {
const V = queue.shift()
for each(const w in this.neighbours(V)) {
if (!w.visited) {
w.source = V
w.visited = true
queue.push(w)
if (w.isEdge) break
}
}
}
}
複製代碼
代碼倉庫在 Catch-the-coin,下載後在瀏覽器打開 index.html
便可試玩哦(喜歡就點個贊吧,逃~ 😂