Prim生成迷宫算法

Prim生成迷宫算法

零、为什么要写这篇文章

近期看了一下生成迷宫的算法,有的算法要么生成的迷宫过于复杂,要么太过明显的有一条大通路,权衡之下,Prim算法生成的迷宫比较适中,写这篇文章就是为了分享一下这个算法的思想。当然,所有的迷宫生成算法如果不进行再处理的话,生成的迷宫解决路径往往不止一条,这个不在本篇的的讲解范围内。

一、讲一下Prim算法

普里姆算法(英语:Prim’s algorithm)是图论中的一种贪心算法,可在一个加权连通图中找到其最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克发现;并在1957年由美国计算机科学家罗伯特·C·普里姆独立发现;1959年,艾兹格·迪科斯彻再次发现了该算法。因此,在某些场合,普里姆算法又被称为DJP算法、亚尔尼克算法或普里姆-亚尔尼克算法。

二、迷宫用Prim算法实现

思想:因为迷宫每条路的权重都一样,所有不存在选择权重大小的问题,只用随机选择下个点即可。

定义:一个存储已访问过节点列表数组visitedList,当前节点currentNode

具体思维逻辑:

  1. 生成奇数行奇数列的二维数组,最外层是墙(第一行,最后一行,第一列,最后一列),偶数行都是墙,奇数行奇数列为路,偶数列为墙,所有路状态为未访问。
  2. 随机选择一个路的点(奇数行奇数列),此路状态改为已访问,标记为currentNode,放入visitedList
  3. 执行循环,当visitedList长度为0的时候跳出循环到5,否则执行4。
  4. 判断currentNode邻近的上下左右是否存在路及路是否访问过(隔一层墙的路)。
    4.1 存在未访问过的路:将这些路放入临时列表,再从临时列表中随机取一个,两个路之间的墙变为路,且标记为已访问,将随机取的路标记为currentNode,状态改为已访问,将此路放入visitedList,重复4。
    4.2 不存在未访问过的路或不存在路:将currentNodevisitedList中移出,此时当visitedList长度大于0,在visitedList随机选一个路标记为currentNode,执行4,否则到5。
  5. 生成最终迷宫。

三、动画呈现原理

Prim生成迷宫算法

四、代码实现

class MazeGrid {
  x: number;
  y: number;
  value: number;
  isVisited: boolean = false;
  constructor({ x, y, value, isVisited = false }: { x: number, y: number, value: number, isVisited?: boolean }) {
    this.x = x;
    this.y = y;
    this.value = value;
    this.isVisited = isVisited
  }
}

const EMPTY_MAZE_GRID = new MazeGrid({ x: 0, y: 0, value: 0, isVisited: true }); //当四边的路不存在邻近路用这个节点代替

class GenerateMaze {
  width: number = 19;
  height: number = 19;
  constructor(width?: number, height?: number) {
    if (width) {
      this.width = width;
    }
    if (height) {
      this.height = height;
    }
  }

  getMaze() {
    if (this.width % 2 == 0) {
      this.width += 1;
    }
    if (this.height % 2 == 0) {
      this.height += 1;
    }
    const list: MazeGrid[][] = [];
    for (let i = 0; i < this.height; i++) {
      const row: MazeGrid[] = [];
      for (let j = 0; j < this.width; j++) {
        if (i % 2 == 0 || j % 2 == 0) {
          row.push(new MazeGrid({ x: i, y: j, value: 0 }));
        } else {
          row.push(new MazeGrid({ x: i, y: j, value: j % 2 }));
        }
      }
      list.push(row);
    }

    let currentNode: MazeGrid;
    let randomRow = 2 * Math.floor(this.height / 2 * Math.random()) + 1;
    let randowColumn = 2 * Math.floor(this.width / 2 * Math.random()) + 1;
    while (!currentNode) {
      if(randomRow > this.height - 1 || randowColumn > this.width - 1) {
        randomRow = 2 * Math.floor(this.height / 2 * Math.random()) + 1;
        randowColumn = 2 * Math.floor(this.width / 2 * Math.random()) + 1;
        continue;
      }
      currentNode = list[randomRow][randowColumn];
    }
    let visitedNodeList: MazeGrid[] = [currentNode];

    while (visitedNodeList.length > 0) {
      const left = currentNode.y - 2 >= 0 ? list[currentNode.x][currentNode.y - 2] : EMPTY_MAZE_GRID;
      const right = currentNode.y + 2 < this.width - 1 ? list[currentNode.x][currentNode.y + 2] : EMPTY_MAZE_GRID;
      const up = currentNode.x - 2 >= 0 ? list[currentNode.x - 2][currentNode.y] : EMPTY_MAZE_GRID;
      const down = currentNode.x + 2 < this.height - 1 ? list[currentNode.x + 2][currentNode.y] : EMPTY_MAZE_GRID;

      const neighborNodes: MazeGrid[] = [];

      if (!up.isVisited) {
        neighborNodes.push(up);
      }

      if (!down.isVisited) {
        neighborNodes.push(down);
      }

      if (!left.isVisited) {
        neighborNodes.push(left);
      }

      if (!right.isVisited) {
        neighborNodes.push(right);
      }

      if (neighborNodes.length > 0) {
        const randowNode: MazeGrid = neighborNodes[Math.floor(Math.random() * neighborNodes.length)];
        randowNode.isVisited = true;
        visitedNodeList.push(randowNode);
        list[(randowNode.x + currentNode.x) / 2][(randowNode.y + currentNode.y) / 2].value = 1;
        currentNode = randowNode;
      } else {
        visitedNodeList = visitedNodeList.filter(node => node != currentNode);
        if (visitedNodeList.length === 0) {
          break;
        } else {
          currentNode = visitedNodeList[Math.floor(Math.random() * visitedNodeList.length)];
          currentNode.isVisited = true;
          visitedNodeList = visitedNodeList.filter(node => node != currentNode);
        }
      }

    }

    return list;
  }
}

五、小提示

用算法生成的迷宫,偶尔会出现一些特别长的单向通路,这种时候可能就需要自己去调整,如果想迷宫游戏体验更好,就需要手动调参或者用别的算法去优化。

六、运用Prim算法开发的迷宫小游戏(Cocos Creator)

最终用这个算法开发了一款迷宫小游戏,这个小游戏中随机模式和挑战模式生成的地图均为Prim算法生成,具体这个游戏的开发后续可能会单独写一篇文章分享给大家,下面是小游戏的二维码,可以用微信扫码体验或搜索重力迷宫试玩。

Prim生成迷宫算法

正文完
 
zemor
版权声明:本站原创文章,由 zemor 2023-11-01发表,共计3520字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码