蜂巢狀迷宮


這邊的蜂巢狀迷宮,是指每個細胞的外觀都是正六角形,如蜂巢般排列,例如:

蜂巢狀迷宮

有些人第一眼的想法可能是,因為每個細胞的外觀是正六角形,可以對先前〈遞迴回溯迷宮〉的 Maze 做些修改,讓行進路徑有六個方向,對吧?

其實不用,仔細觀察一下這個迷宮中每個細胞的排列方式,還是基於行列,將每一列以不同顏色表示,很容易就能看出了:

蜂巢狀迷宮

細胞牆面

既然是基於行列,就表示〈遞迴回溯迷宮〉的 Maze 完全不用修改,我們只要改變細胞繪製的方式就可以了,在六角形結構下,往上或往下走沒有問題,就是打通六角形的上或下邊,那麼往左或往右呢?

蜂巢狀迷宮

往左或往右時,可以看到依行的不同,打通的牆會不同,以往右為例,若行索引以 0 開始,那麼偶數索引必須打通右下牆,奇數索引必須打通右上牆。

MazeMaze.TOP_RIGHT_WALLMaze.TOP_WALLMaze.RIGHT_WALLMaze.NO_WALL,對應的牆面有哪些呢?

同樣地,不必六個邊都畫上,Maze.TOP_RIGHT_WALL 對應的牆面是:

蜂巢狀迷宮

將一組細胞排列,就會形成以下的結構:

蜂巢狀迷宮

最後只要補足上、左、右的牆就可以了:

蜂巢狀迷宮

Maze.TOP_WALL 時沒有問題,只要畫出上牆,Maze.RIGHT_WALL 呢?方才談到,往左或往右時,可以看到依行的不同,打通的牆會不同,若行索引以 0 開始,那麼偶數索引必須打通右下牆,這表示偶數索引必須繪製右上牆,奇數索引必須繪製右下牆。

最後,Maze.NO_WALL 並不是就完全不畫牆,上牆是不用畫沒錯,然而 Maze.NO_WALL 表示在方形細胞中右邊沒有牆面,而在這邊的六角形細胞中,表示偶數索引右下沒有牆面,也就是必須繪製右上牆,奇數索引是右上沒有牆面,也就是必須繪製右下牆。

繪製蜂巢狀迷宮

依照方才的說明,先來繪製單個細胞,一開始先不要管哪邊有牆,先畫出這個:

蜂巢狀迷宮

這是六角形的一部份,不過別急著拿出三角函式,使用向量會更方便一些,底下的 cellWidth 是指六角形最左至最右的寬度,除以 2 就是六角形中心至頂點的長度:

 const r = cellWidth / 2;
 const vertices = [
    p5.Vector.fromAngle(radians(60), r),
    p5.Vector.fromAngle(radians(0), r),
    p5.Vector.fromAngle(radians(-60), r),
    p5.Vector.fromAngle(radians(-120), r)
];

function drawCell(cell, r) {
    for(let i = 0; i < 3; i++) {
        line(vertices[i].x, vertices[i].y, vertices[i + 1].x, vertices[i + 1].y)
    }
}

然後來判斷哪些牆要畫:

function drawCell(cell, r) {
    const isXOdd = isOdd(cell.x);

    // 往上無法通行
    if(cell.wallType === Maze.TOP_RIGHT_WALL || cell.wallType === Maze.TOP_WALL) {
        line(vertices[3].x, vertices[3].y, vertices[2].x, vertices[2].y);
    }

    // 往右無法通行
    if(cell.wallType === Maze.TOP_RIGHT_WALL || cell.wallType === Maze.RIGHT_WALL) {
        line(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y);
        line(vertices[1].x, vertices[1].y, vertices[2].x, vertices[2].y);
    }  
    else {
        // 往右可以通行,根據列索引決定要畫右上還是右下
        if(isXOdd) {
             line(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y);
        }
        else {
             line(vertices[1].x, vertices[1].y, vertices[2].x, vertices[2].y);
        }
    }
}

接著要畫出整個迷宮:

function drawMaze(maze, cellWidth) {
     const r = cellWidth / 2;
     const vertices = [
        p5.Vector.fromAngle(radians(60), r),
        p5.Vector.fromAngle(radians(0), r),
        p5.Vector.fromAngle(radians(-60), r),
        p5.Vector.fromAngle(radians(-120), r)
    ];

    function drawCell(cell, r) {
        ...同前
    }

    // 每個細胞的基本 x, y 位移
    const xStep = cellWidth - (vertices[1].x - vertices[2].x);
    const yStep = vertices[0].y - vertices[2].y;

    maze.cells.forEach(cell => {
        const isXOdd = isOdd(cell.x);
        const isXEven = !isXOdd;
        const px = r + xStep * cell.x;
        const py = r + yStep * cell.y + (isXOdd ? vertices[0].y : 0);

        push();
        translate(px, py);
        drawCell(cell, r);

        pop();
    });

}

因為每個細胞最多只畫三面牆,上、左、下邊界有缺牆,為此必須補上邊界:

function drawMaze(maze, cellWidth) {
     const r = cellWidth / 2;
     const vertices = [
        p5.Vector.fromAngle(radians(60), r),
        p5.Vector.fromAngle(radians(0), r),
        p5.Vector.fromAngle(radians(-60), r),
        p5.Vector.fromAngle(radians(-120), r),

        // 畫邊界需要的頂點
        p5.Vector.fromAngle(radians(-180), r),  
        p5.Vector.fromAngle(radians(-240), r)  
    ];

    function drawCell(cell, r) {
        const isXOdd = isOdd(cell.x);

        if(cell.wallType === Maze.TOP_RIGHT_WALL || cell.wallType === Maze.TOP_WALL) {
            line(vertices[3].x, vertices[3].y, vertices[2].x, vertices[2].y);
        }

        if(cell.wallType === Maze.TOP_RIGHT_WALL || cell.wallType === Maze.RIGHT_WALL) {
            line(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y);
            line(vertices[1].x, vertices[1].y, vertices[2].x, vertices[2].y);
        }  
        else {
            if(isXOdd) {
                 line(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y);
            }
            else {
                 line(vertices[1].x, vertices[1].y, vertices[2].x, vertices[2].y);
            }
        }

    }

    const xStep = cellWidth - (vertices[1].x - vertices[2].x);
    const yStep = vertices[0].y - vertices[2].y;

    maze.cells.forEach(cell => {
        const isXOdd = isOdd(cell.x);
        const isXEven = !isXOdd;
        const px = r + xStep * cell.x;
        const py = r + yStep * cell.y + (isXOdd ? vertices[0].y : 0);

        push();
        translate(px, py);
        drawCell(cell, r);

        // 補上邊界
        if(cell.x === 0) {
           line(vertices[3].x, vertices[3].y, vertices[4].x, vertices[4].y);
           line(vertices[4].x, vertices[4].y, vertices[5].x, vertices[5].y);
        }

        // 補左邊界
        if(cell.y === 0 && isXEven) {
           line(vertices[3].x, vertices[3].y, vertices[4].x, vertices[4].y);

        }      

        // 補下邊界
        if(cell.y === maze.rows - 1) {
           line(vertices[5].x, vertices[5].y, vertices[0].x, vertices[0].y);
           if(isXOdd) {
               line(vertices[4].x, vertices[4].y, vertices[5].x, vertices[5].y);
           }

        }            
        pop();
    });

}

有了這個 drawDraw,就可以結合 Maze 來繪製蜂巢狀迷宮了: