今天我們要來寫生成箱子的邏輯,如同昨天所說的,因為箱子不管哪一個的行為都是一樣的,所以我們不用從 box1 寫到 box10,我們可以試著用一個函式來生成所有的箱子。
首先來想想:我們期望傳入怎樣的內容,並且輸出怎樣的資料格式,先定義好這個方向之後,再來思考內部的邏輯。

比照 pickRandomEmptyCell 函式,我想一樣把地圖以及要避免的座標傳進去,但是要再加上這次想生成幾個箱子,並且要以陣列回傳箱子們的座標。
預期使用的形式是 let boxes = createBoxes(map,10,[player,enemy1,enemy2,enemy3]);
那我們就來寫這個函式吧,還不知道怎麼寫也沒關係,我們先把 input 、output 的內容定下來,也順便在裡面用自然語言補上我們心中的步驟:
function createBoxes(map,boxesNum, occupied = []) {
// 用來儲存箱子們的座標
let boxes = [];
// 找到所有的空格
// 隨機選 10 個位置
return boxes;
}
接著再逐一來看每一個問題
這個在 pickRandomEmptyCell 已經有實作過一次,可以拿來用
function createBoxes(map,boxesNum, occupied = []) {
let boxes = [];
// 找到所有的空格
const emptyCell = [];
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
// 判斷是不是已被占用
const isOccupied = occupied.some(cell => cell.x === x && cell.y === y);
// 如果該位置是空的而且尚未被占用才將這個數值儲存到 emptyCell
if (map[y][x] === " " && !isOccupied) {
emptyCell.push({ x, y })
}
}
}
// 隨機選 10 個位置
// 存進 boxes
return boxes;
}
解決這個問題時發現腦袋實在是沒有想法,我們也不要卡在這裡,可以試著去吸取一下其他工程師的日月精華,感謝現在有 AI 摘要,可以快速幫我們找出最佳解法,當然你問 GPT、Gemini、Claude 也可以!

但我們先理解一下這個寫法,判斷哪些是我們需要的:
那我們就來寫吧!
function createBoxes(map,boxesNum, occupied = []) {
// 用來儲存箱子們的座標
let boxes = [];
// 找到所有的空格
// ... 這邊就不動
// 隨機選 10 個位置
for (let i = emptyCell.length -1; i > 0; i --) { // 這裡是由大到小遞減的迴圈
const j = Math.floor(Math.random() * (i+1)); // 隨機在小於等於 i 的範圍內選一個數字
[emptyCell[i],emptyCell[j]] = [emptyCell[j],emptyCell[i]]; // 兩者交換
}
// 存進 boxes,這裡要記得帶入 boxesNum
boxes = emptyCell.slice(0,boxesNum);
return boxes;
}
也可以更精簡的直接拿掉
boxes變數,並 returnemptyCell.slice(0,boxesNum)就好唷。不過為了這邊講解順序的連貫性,我還是先保留這個寫法。
如果你想要的話,也可以在洗牌前後都 console.log 出來看看是不是真的有洗到牌。
那麼我們就在 render 加上箱子的印出
else if (boxes.some(box => box.x === x && box.y === y)) {
row += `\x1b34mX\x1b[0m` // 用藍色和 X 來呈現箱子
}
執行看看

恭喜我們!這樣就成功完成了隨機地圖的製作了,明天我們會來完成監聽使用者動作等基礎設施,如果你可以的話,試著先完成到監聽使用者的動作吧!
明天見!
如果你想要瀏覽完整的程式碼,可以參考這裡:第十天程式碼