3

Well, suppose that you try to walk across the "northern edge" of the maze, you'll come back to the same maze but at the "southern edge". Kinda like navigating through a sphere of maze.

Would it be possible to generate and solve a maze like that? I have yet to find a documentation on this subject...

vxs8122
  • 834
  • 2
  • 10
  • 22

1 Answers1

5

The key point is reconceptualizing the maze from a grid of pixels to a graph. You can then connect the graph so it forms a toroid.

Wilson's Algorithm might be particularly easy to understand. It's also nice in that it generates a Uniform Spanning Tree, which is a spanning tree drawn uniformly from the set of all possible spanning trees of a space.

Perform the following:

  1. Choose any vertex at random and add it to the UST.
  2. Select any vertex that is not already in the UST and perform a loop-erasing random walk until you encounter a vertex that is in the UST. You can modify this random walk so that when it encounters an edge it will wrap around.
  3. Add the vertices and edges touched in the random walk to the UST.
  4. Repeat 2 and 3 until all vertices have been added to the UST.

Discussions are available here and here.

Here's a draft of what the algorithm might look like (the remaining pink cells are an artefact of something in the drawing routine, but don't affect the result).

<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
  background: #000;
}

</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>

var width  = 200,
    height = 200;

var N = 1 << 0,
    S = 1 << 1,
    W = 1 << 2,
    E = 1 << 3;

var cellSize    = 4,
    cellSpacing = 4,
    cellWidth   = Math.floor((width - cellSpacing) / (cellSize + cellSpacing)),
    cellHeight  = Math.floor((height - cellSpacing) / (cellSize + cellSpacing)),
    cells       = new Array(cellWidth * cellHeight), // each cell’s edge bits
    remaining   = d3.range(cellWidth * cellHeight),  // cell indexes to visit
    previous    = new Array(cellWidth * cellHeight), // current random walk path
    i0, x0, y0; // end of current random walk

var canvas = d3.select("body").append("canvas")
    .attr("width", width)
    .attr("height", height);

var context = canvas.node().getContext("2d");

context.translate(
  Math.round((width - cellWidth * cellSize - (cellWidth + 1) * cellSpacing) / 2),
  Math.round((height - cellHeight * cellSize - (cellHeight + 1) * cellSpacing) / 2)
);

// Add the starting cell.
context.fillStyle = "white";
var start         = remaining.pop();
cells[start]      = 0;
fillCell(start);

// While there are remaining cells,
// add a loop-erased random walk to the maze.
context.fillStyle = "magenta";
d3.timer(function() {
  for (var k = 0; k < 50; ++k) {
    if (loopErasedRandomWalk()) {
      return true;
    }
  }
});

function loopErasedRandomWalk() {
  var i1;

  // Pick a location that’s not yet in the maze (if any).
  if (i0 == null) {
    do if ((i0 = remaining.pop()) == null) return true;
    while (cells[i0] >= 0);
    previous[i0] = i0;
    fillCell(i0);
    x0 = i0 % cellWidth;
    y0 = i0 / cellWidth | 0;
    return;
  }

  // Perform a random walk starting at this location,
  // by picking a legal random direction.
  i1 = Math.random() * 4 | 0;
  if (i1 === 0) {
    if (y0 <= 0){
      y0 = cellHeight-1;
      i1 = i0 - cellWidth + cellWidth*cellHeight;
    } else{
      --y0;
      i1 = i0 - cellWidth;
    }
  } else if (i1 === 1) {
    if (y0 >= cellHeight - 1){
      y0 = 0;
      i1 = i0 + cellWidth - cellWidth*cellHeight;
    } else {
      ++y0;
      i1 = i0 + cellWidth;
    }
  } else if (i1 === 2) {
    if (x0 <= 0){
      x0 = cellWidth-1;
      i1 = i0+cellWidth-1;
    } else {
      --x0;
      i1 = i0 - 1;
    }
  } else { 
    if (x0 >= cellWidth - 1) {
      x0 = 0;
      i1 = i0-cellWidth+1;
    } else {
      ++x0;
      i1 = i0 + 1;
    }
  }

  // If this new cell was visited previously during this walk,
  // erase the loop, rewinding the path to its earlier state.
  if (previous[i1] >= 0) eraseWalk(i0, i1);

  // Otherwise, just add it to the walk.
  else {
    previous[i1] = i0;
    fillCell(i1);
    if (i1 === i0 - 1) fillEast(i1);
    else if (i1 === i0 + 1) fillEast(i0);
    else if (i1 === i0 - cellWidth) fillSouth(i1);
    else fillSouth(i0);
  }

  // If this cell is part of the maze, we’re done walking.
  if (cells[i1] >= 0) {

    // Add the random walk to the maze by backtracking to the starting cell.
    // Also erase this walk’s history to not interfere with subsequent walks.
    context.save();
    context.fillStyle = "#fff";
    fillCell(i1);
    while ((i0 = previous[i1]) !== i1) {
      fillCell(i0);
      if (i1 === i0 + 1) cells[i0] |= E, cells[i1] |= W, fillEast(i0);
      else if (i1 === i0 - 1) cells[i0] |= W, cells[i1] |= E, fillEast(i1);
      else if (i1 === i0 + cellWidth) cells[i0] |= S, cells[i1] |= N, fillSouth(i0);
      else cells[i0] |= N, cells[i1] |= S, fillSouth(i1);
      previous[i1] = NaN;
      i1 = i0;
    }
    context.restore();

    previous[i1] = NaN;
    i0 = null;
  } else {
    i0 = i1;
  }
}

function eraseWalk(i0, i2) {
  var i1;
  context.save();
  context.globalCompositeOperation = "destination-out";
  do {
    i1 = previous[i0];
    if (i1 === i0 - 1) fillEast(i1);
    else if (i1 === i0 + 1) fillEast(i0);
    else if (i1 === i0 - cellWidth) fillSouth(i1);
    else fillSouth(i0);
    fillCell(i0);
    previous[i0] = NaN;
    i0 = i1;
  } while (i1 !== i2);
  context.restore();
}

function fillCell(i) {
  var x = i % cellWidth, y = i / cellWidth | 0;
  context.fillRect(x * cellSize + (x + 1) * cellSpacing, y * cellSize + (y + 1) * cellSpacing, cellSize, cellSize);
}

function fillEast(i) {
  var x = i % cellWidth, y = i / cellWidth | 0;
  context.fillRect((x + 1) * (cellSize + cellSpacing), y * cellSize + (y + 1) * cellSpacing, cellSpacing, cellSize);
}

function fillSouth(i) {
  var x = i % cellWidth, y = i / cellWidth | 0;
  context.fillRect(x * cellSize + (x + 1) * cellSpacing, (y + 1) * (cellSize + cellSpacing), cellSize, cellSpacing);
}

d3.select(self.frameElement).style("height", height + "px");

</script>
Richard
  • 56,349
  • 34
  • 180
  • 251
  • 1
    @vxs8122 - This is a good answer, but in case you don't like Wilson's algorithm (because it's slow and a UST is not a very nice maze), note that most maze generation algorithms can be modified to work on a toroidal topology in the same way. – Matt Timmermans Aug 11 '18 at 18:15