Theta 迷宮(一)


如果想做個圓形迷宮,方式之一是透過遮罩,例如,透過圓形遮罩結合〈迷宮與遮罩〉的程式,可以做出以下的迷宮:

Theta 迷宮

不過,也許會覺得用這種方式做圓形迷宮有點投機取巧,畢竟,你可能看過這樣的圓形迷宮:

Theta 迷宮

這種迷宮是基於極座標繪製而成,常被稱為 Theta 迷宮,有沒有可能基於〈遞迴回溯迷宮〉實作 Theta 迷宮呢?畢竟,〈蜂巢狀迷宮〉就是基於遞迴回溯迷宮,只是繪圖上做些變化。

基於方形迷宮變化

基本上是可以,出發點是可以將底下的圖形:

Theta 迷宮

繞為底下的圓:

Theta 迷宮

若要配合遞迴回溯迷宮的細胞索引方式,就像是將矩形迷宮轉 90 度後,將每一列剪下來,變成環狀,細胞索引 y 就是 y 環索引,索引 x 就是每一環細胞的逆時針 x 索引:

Theta 迷宮

如果只是將方拉成圓,你也可以不用如上拉成圓,只不過這樣拉圓,配合原本方形迷宮的索引方式會比較方便罷了,顯然地,若要用這樣的方式建立 Theta 迷宮,Maze 的實作完全不用更改,純粹就是繪圖的方式罷了。

因為將方形迷宮逆時針轉 90 度了,方形迷宮中每個細胞的上與右牆,會變成:

Theta 迷宮

TWO_PI 除以行(column) 數,就可以求得 Θ,方形迷宮的 x 索引,就是對應至的 Θ * xΘ * (x + 1) 間的弧,圓形細胞的大小會以上圖中的「細胞寬度」決定,圖中定義了「內半徑向量」與「外半徑向量」,是因為 p5.js 中提供了 p5.Vector,透過這兩個向量的操作,可以很簡單的取得繪製上牆、右牆必要的頂點。

繪製 Theta 迷宮

從上頭的描述可以知道,〈遞迴回溯迷宮〉的 Maze 實作不用任何修改,需要的就只是繪製方形改為繪製圓形,一個簡化繪製程式實作的方式是透過 p5.Vector,這樣就不用直接使用三角函式了:

function drawMaze(maze, cellWidth) {
    const thetaStep = TWO_PI / maze.columns;

    maze.cells.forEach(cell => {
        const innerR = (cell.y) * cellWidth;
        const outerR = (cell.y + 1) * cellWidth;   
        const theta1 = -thetaStep * cell.x;
        const theta2 = -thetaStep * (cell.x + 1);

        const innerVt1 = p5.Vector.fromAngle(theta1, innerR);
        const innerVt2 = p5.Vector.fromAngle(theta2, innerR);
        const outerVt2 = p5.Vector.fromAngle(theta2, outerR);

        if(cell.wallType === Maze.TOP_WALL || cell.wallType === Maze.TOP_RIGHT_WALL) {
            line(innerVt1.x, innerVt1.y, innerVt2.x, innerVt2.y);
        }


        if(cell.wallType === Maze.RIGHT_WALL || cell.wallType === Maze.TOP_RIGHT_WALL) {
            line(innerVt2.x, innerVt2.y, outerVt2.x, outerVt2.y);
        }
    });

    const r = cellWidth * (maze.rows);
    for(let theta = 0; theta < TWO_PI; theta = theta + thetaStep) {
        const vt1 = p5.Vector.fromAngle(theta, r);
        const vt2 = p5.Vector.fromAngle(theta + thetaStep, r);
        line(vt1.x, vt1.y, vt2.x, vt2.y);
    }
}

然而,若直接使用這個 drawMaze 來繪製 rowscolumns 都是 8 的迷宮,會長這樣:

Theta 迷宮

怎麼越外圍開口越大呢?而且,為什麼我是使用 line 來繪製啊?使用 line 來繪製,就是為了突顯越外圍開口越大這件事,實際上,就算你試圖使用 arc 來繪製,試著讓開口變小,也不能改變這迷宮在繪製時,越外環細胞越大的事實,畢竟,這只是純粹將方形拉成圓形罷了。

單純只是想讓這迷宮好看一些,不要內外環開口差距太明顯的話,可以讓最內環的半徑大一些,然後減少 rows、增加 columns

function drawMaze(maze, cellWidth) {
    const thetaStep = TWO_PI / maze.columns;

    maze.cells.forEach(cell => {
        // 增加最內環的半徑
        const innerR = (cell.y + 4) * cellWidth;
        const outerR = (cell.y + 5) * cellWidth;   
        ...
    });

    // 最外的牆也要做調整
    const r = cellWidth * (maze.rows + 4);
    for(let theta = 0; theta < TWO_PI; theta = theta + thetaStep) {
        const vt1 = p5.Vector.fromAngle(theta, r);
        const vt2 = p5.Vector.fromAngle(theta + thetaStep, r);
        line(vt1.x, vt1.y, vt2.x, vt2.y);
    }   
}

這樣畫出來就會好一些:

Theta 迷宮

只不過這比較算是迷宮環了,或許在中間開個口會比較好,當寶物室?

function drawMaze(maze, cellWidth) {
    const thetaStep = TWO_PI / maze.columns;

    maze.cells.forEach(cell => {
        // (0, 0) 不畫牆
        if(cell.x === 0 && cell.y === 0) {
            return;
        } 
        ...
    });

    ...
}

另一個問題是,角度為 0 時的牆始終是封閉的,也就是以上的繪製方法,右方總是會有一條實線?當然,因為是將方形拉成圓,記得嗎?方形迷宮時,最右邊的牆是不會打通的,因為是邊界嘛!

如果你讓最右邊的牆可以打通,這意謂著穿越回到最左邊的細胞,反之亦然,這樣拉成圓時,表示可以順、逆時針無阻礙地打牆,這只要在 MazenextX 小修改一下:

function nextX(maze, x, dir) {
    let nx = x + [1, 0, -1, 0][dir];
    return nx >= 0 ? (nx % maze.columns) : (nx + maze.columns);
}

當然,呼叫 nextX 的地方,也要記得改一下傳入 Maze 實例,而 visitRightvisitTopvisitLeftvisitBottom,不能只是對 currentCell.x 加減 1,改呼叫 nextX 才行:

function visitRight(maze, currentCell) {
    if(currentCell.wallType === Maze.TOP_RIGHT_WALL) {
        currentCell.wallType = Maze.TOP_WALL;
    }
    else {
        currentCell.wallType = Maze.NO_WALL;
    }
    maze.cells.push(cell(nextX(maze, currentCell.x, R), nextY(currentCell.y, R), Maze.TOP_RIGHT_WALL));
}

function visitTop(maze, currentCell) {
    if(currentCell.wallType === Maze.TOP_RIGHT_WALL) {
        currentCell.wallType = Maze.RIGHT_WALL;
    }
    else {
        currentCell.wallType = Maze.NO_WALL;
    }
    maze.cells.push(cell(nextX(maze, currentCell.x, T), nextY(currentCell.y, T), Maze.TOP_RIGHT_WALL));
}

function visitLeft(maze, currentCell) {
    maze.cells.push(cell(nextX(maze, currentCell.x, L), nextY(currentCell.y, L), Maze.TOP_WALL));
}

function visitBottom(maze, currentCell) {
    maze.cells.push(cell(nextX(maze, currentCell.x, B), nextY(currentCell.y, B), Maze.RIGHT_WALL));
}

這樣的話就可以繪製出以下的迷宮:

Theta 迷宮

要說這是 Theta 迷宮,也算是啦!只不過不是這篇文件一開頭看到的那個 Theta 迷宮,那個 Theta 迷宮的製作方式,就留待下一篇來探討了。

底下列出至今的成果: