迷宮一定得是方形的嗎?當然不是!就算〈遞迴回溯迷宮〉是基於行列的隨機演算,透過適當地編碼轉換,也是可以轉為〈蜂巢狀迷宮〉或者其他形狀的迷宮。
或許製造不同形狀的迷宮,最簡單的方式是,將迷宮演算結合遮罩,原理很簡單,迷宮是一組細胞組成,事先設定某些細胞不能造訪,就可以將迷宮塑造為不同的形狀。例如:
這是個 30 x 30 的迷宮,透過底下 30 x 30 的點白圖片轉為遮罩資料產生出來,黑色部份是不能走的:
圖片轉出來的遮罩資料,基本上是一堆 0 與 1,0 是黑色,1 是白色,圖片轉換我寫了個 img2binary 來處理。
為了簡化說明遮罩資料的部份,就用簡單的愛心為例好了,底下是個 11 x 11 的愛心轉換出來的遮罩資料:
const mask = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
為了能開始走訪迷宮,走訪起點必須是在遮罩中 1 的位置,如果希望迷宮能走訪全部的 1,記得 1 彼此之間必須接續,另外要知道的是,並非所有迷宮演算法都可以使用遮罩,例如〈二元樹迷宮〉使用的二元樹演算就不行,理由很簡單,如果目前已經打通至以下狀態:
黑色是遮罩,若接下來丟硬幣決定了要打通上牆:
一旦形成以上狀態,右下角那一格,不能打通上牆(因為遮罩)或右牆(因為邊界),它左邊那格又已決定狀態為打通上牆,最後右下角那一格就只能是無法進入的狀態了。
實作遮罩
遞迴回溯演算可以結合遮罩,而且目前我們的實作,可以很簡單地加入遮罩的功能,首先,為細胞增加一個 Maze.MASK
的可能狀態:
Maze.NO_WALL = 'no_wall';
Maze.TOP_WALL = 'top_wall';
Maze.RIGHT_WALL = 'right_wall';
Maze.TOP_RIGHT_WALL = 'top_right_wall';
Maze.MASK = 'mask'; // 增加一個可能的狀態
接著,寫個 maskCells
,將 0、1 二維陣列轉為細胞資料:
function maskCells(mask) {
const maskCells = [];
for(let y = 0; y < mask.length; y++) {
for(let x = 0; x < mask[y].length; x++) {
if(mask[y][x] === 0) {
maskCells.push([x, y]);
}
}
}
return maskCells;
}
Maze
的 backtracker
實作中,加入 mask
參數,預設值是 []
,表示不設定遮罩,若有指定就建立遮罩細胞加入 cells
的最前端:
class Maze {
constructor(rows, columns) {
this.rows = rows;
this.columns = columns;
}
backtracker(x = 0, y = 0, mask = []) {
this.cells = [];
maskCells(mask).forEach(coord => {
let [x, y] = coord;
this.cells.push(cell(x, y, Maze.MASK));
});
this.cells.push(cell(x, y, Maze.TOP_RIGHT_WALL));
backtracker(this);
}
}
因為 Maze
在走訪迷宮細胞時,若發現 cells
中已經有迷宮細胞,就會視為已走訪,自然地,就不會去走訪那些被加入 cells
的遮罩細胞了。
最後就可以來繪製迷宮了,這只要修改 drawMaze
,讓它畫出遮罩細胞以及迷宮細胞就可以了:
function drawMaze(maze, cellWidth) {
const maskCells = maze.cells.filter(cell => cell.wallType === Maze.MASK);
// 遮罩細胞
maskCells.filter(cell => cell.wallType === Maze.MASK)
.forEach(cell => {
push();
translate(cell.x * cellWidth, cell.y * cellWidth);
square(0, 0, cellWidth);
pop();
});
// 迷宮細胞
maze.cells.slice(maskCells.length).forEach(cell => {
push();
translate(cell.x * cellWidth, cell.y * cellWidth);
drawCell(cell.wallType, cellWidth);
pop();
});
}
底下是完整的範例:
你可以將底下的「迷」字遮罩資料,取代以上範例的 mask
部份,就可以畫出一開始看到的「迷」字迷宮:
const mask = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];