今天要延續兩個箱子的做法,來討論勝負判定、死亡判定、要怎麼從邊緣推出箱子的邏輯。開始前我想先預告一下今天的兩個重點,其中之一是 AI 的協作,昨天我們有讓 AI 為我們做了精簡的動作,今天我們要利用 AI 來幫我們除錯,我會分享我讓 AI 修復錯誤之後如何追蹤他修改的內容,這部分我相信資深的工程師讀者一定早已熟知,不過由於我的目標讀者是初學者,因此我想這也是我在開發中思考歷程的一部分,因此將這部分納入文章內。
另一個重點則是為了避免推箱子遊戲在整個系列中過於膨脹,因此這會是推箱子的最後一篇,但無論是對於我或者是讀者而言,應該都有很多懸而未決的問題吧!我會在文末來解釋我為什麼會選擇停在這裡,那麼我們就回頭繼續用前面提到的拆解步驟和小步試錯的方法來實作吧!
回到今天的程式碼撰寫前,想邀請大家先想一想在兩個箱子、一個目標點的情況下,怎樣算獲勝、怎樣算失敗?
參考昨天 AI 寫的函式,我發現可以把 boxes 抽出來,並且一樣用 find 的方法來找到是否有任意 box 在目標點上,先來寫寫這個邏輯。
let player = pickRandomEmptyCell(map);
let goal = pickRandomEmptyCell(map, [player]);
let box1 = boxPicker([player, goal]);
let box2 = boxPicker([player, goal, box1]);
let button = pickRandomEmptyCell(map, [player, box1, box2, goal]);
const boxes = [box1,box2]; // 這裡我們加上這一句,因為之後會常常用到
/// 獲勝判定的邏輯改成用 boxes.find 來找是否有任意 box 位於目標點
const isOnGoal = boxes.find(box => box.x === goal.x && box.y === goal.y);
if (isOnGoal) {
process.stdout.write("\n恭喜獲勝\n");
process.exit();
}
死局的判定和箱子彈出的邏輯依賴的是 checkBox 這個共用函式,因此先來看看原始的程式碼
function checkBox(map, box) {
const top = map[box.y - 1][box.x] === '#';
const bottom = map[box.y + 1][box.x] === '#';
const left = map[box.y][box.x - 1] === '#';
const right = map[box.y][box.x + 1] === '#';
const isDeadlock = (top || bottom) && (left || right);
const position = { top, bottom, left, right, isDeadlock }
return position
}
原本的 checkBox 邏輯是去判斷單一的箱子是否在四個邊上或死角,不過在兩個 box 的情況下必須要兩個 box 都位於死角,遊戲才算結束,所以我們要利用這個 checkBox 函式來判斷這兩個箱子是否有在邊上或死角:
// 把這句原本的單一判斷
const { top, bottom, left, right, isDeadlock } = checkBox(map, box);
// 改成分別判斷
const position1 = checkBox(map, box1);
const position2 = checkBox(map, box2);
接著來改死局判定
// 改成兩個箱子都在死角,就結束遊戲
if (position1.isDeadlock && position2.isDeadlock) {
process.stdout.write("\n箱子卡住了,遊戲結束\n");
process.exit();
}
我想在這邊嘗試兩個做法:
按照前面我們練習過的打造原型的方法,我想將兩個都實作出來試試看效果如何,一樣用剛剛的 position1 和 position2 就可以知道兩個箱子的位置:
// 判斷 box1 在不在邊緣上
if (player.y === button.y && player.x === button.x && !position1.isDeadlock) {
if (position1.top) {
box1.y += 1;
} else if (position1.bottom) {
box1.y -= 1;
} else if (position1.left) {
box1.x += 1;
} else if (position1.right) {
box1.x -= 1;
}
render();
}
// 判斷 box2 在不在邊緣上
if (player.y === button.y && player.x === button.x && !position2.isDeadlock) {
if (position2.top) {
box2.y += 1;
} else if (position2.bottom) {
box2.y -= 1;
} else if (position2.left) {
box2.x += 1;
} else if (position2.right) {
box2.x -= 1;
}
render();
}
現在來玩看看:
再來寫寫看將箱子推到按鈕上,推出另一個箱子的作法,其實只要把 player 改成 box1 和 box2 就好
// 如果箱子2在按鈕上,可以推出 box1
if (box2.y === button.y && box2.x === button.x && !position1.isDeadlock) {
if (position1.top) {
box1.y += 1;
} else if (position1.bottom) {
box1.y -= 1;
} else if (position1.left) {
box1.x += 1;
} else if (position1.right) {
box1.x -= 1;
}
render();
}
// 如果箱子1在按鈕上,可以推出 box2
if (box1.y === button.y && box1.x === button.x && !position2.isDeadlock) {
if (position2.top) {
box2.y += 1;
} else if (position2.bottom) {
box2.y -= 1;
} else if (position2.left) {
box2.x += 1;
} else if (position2.right) {
box2.x -= 1;
}
render();
}
玩看看吧!!!!
將一個箱子推到 button 上,確實可以推出另一個箱子,但是卻發現另外一個問題,有時 player 竟然可以踩到箱子上,這時候我們不要浪費時間自己看,直接請出 AI 來幫我們診斷一下。
首先我們先 commit 一次當前的進度,接著請 AI 更新,然後我們就可以藉由 git 來追蹤他改了哪些內容。雖然說我使用 CLI 的時候,他也會及時跳出修改的內容,但他有時會截斷上下文以致我無法完整的判斷他修改了什麼,而既然本篇的主題強調思考,我們當然不能夠不做任何確認就通過這段修改,因此藉由 git 來確認修改的內容我想也是一個對於自己和這個主題負責任的做法。

首先我們看到他改了移動的順序,從最遠的箱子先移動,避免玩家先移動,但是箱子還留在原地的情況,這是我們這次遇到的情況,玩家移動了,但是箱子沒有移動到,反而被玩家覆蓋掉了

第二段則是他發現了另外一個問題,就是每次我們在將箱子推出之前,沒有先判斷有沒有東西阻擋推出,所以他寫了一個函式來計算這件事

我們一樣來寫個註解
// 用來計算該點是否屬於 player 或 另一個 box
function isOccupiedByPlayerOrBox(x, y, ignoredBox = null) {
return (
(player.x === x && player.y === y) ||
(box1 !== ignoredBox && box1.x === x && box1.y === y) ||
(box2 !== ignoredBox && box2.x === x && box2.y === y)
);
}
// 判斷是否可以移動箱子,傳入箱子以及分別要在 x 軸和 y軸移動幾格
function moveBoxIfAvailable(box, dx, dy) {
const targetX = box.x + dx; // 計算出目標位置的 x
const targetY = box.y + dy; // 計算出目標位置的 y
// 如果在地圖上該目標點不是空的 或者 目標點為 player 或另一個 box 的話回傳 否
if (map[targetY][targetX] !== ' ' || isOccupiedByPlayerOrBox(targetX, targetY, box)) {
return false;
}
// 移動箱子並回傳 是
box.x = targetX;
box.y = targetY;
return true;
}
接著就交給大家來遊玩看看吧!!!
很快的我們就會在遊玩過程中發現到更多的問題,可以分成三部分:
以 Bug 來說,我們可以看到當兩個箱子卡在一起時,怎麼辦呢,此時連 button 也被覆蓋住了,卻沒有結束掉遊戲:

從使用者的角度出發,每次都是難度相同的隨機地圖似乎是無法將我自己留在這個遊戲中的,我或許會期待能夠有持續晉級、難度提升的設計,比方說後期的關卡可以加上中間的障礙物等等。
就設計師的角度而言,要讓一個箱子推到 button 上來解救另外一個箱子的這個機制確實發揮效果,我們必須確保一個箱子必定位於邊緣,且目標點不在該邊緣上,這樣才有遊戲的意義,這就涉及到關卡設計的問題了,因此單純的隨機地圖是否真正能符合我們的需要呢?回頭再思考先前提到的另外一個曾經想過卻沒有實作的原型:手工設計不同的關卡,或許會是更符合需要的解方?
不過在我們繼續就這些問題探究下去之前,我想到 AI agents 每次都會提醒我:這是另一個工作範疇,你要停在這邊,再另外開一個小分支處理呢,還是想合併在這個分支一起處理呢?我覺得這個幫自己喊停的機制很重要。
所以我也想模仿 AI 問我自己問題:這個是現在這個專案需要處理的問題嗎?
關於這題的解題方法自然必須要來回顧一下推箱子小遊戲的目的:認識終端機環境以及嘗試拆解問題、逐一設計出一個可以玩的遊戲,至此這個遊戲已經達到目的了,礙於篇幅的關係,我想這些後續的問題就留給各位夥伴自行來探討吧!! 或許你們也會有想到更多有趣的玩法,如果有打造出更有趣的推箱子小遊戲,歡迎留言跟我分享!
明天我們終於要進入到另外一個遊戲:爆爆王的設計當中啦,超期待的,那就明天見!
如果你想要看完整的程式碼,可以參考這裡:Day8 程式碼