Skip to content

Instantly share code, notes, and snippets.

@robinhouston
Created March 31, 2011 09:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robinhouston/896124 to your computer and use it in GitHub Desktop.
Save robinhouston/896124 to your computer and use it in GitHub Desktop.
A version of the algorithm that permits arbitrary clustering of crossings
# --------------------------------------------------------------------
# An implementation of a "weave" maze generator. Weave mazes are those
# with passages that pass both over and under other passages. The
# technique used in this program was described to me by Robin Houston,
# and works by first decorating the blank grid with the over/under
# crossings, and then using Kruskal's algorithm to fill out the rest
# of the grid. (Kruskal's is very well-suited to this approach, since
# it treats the cells as separate sets and joins them together.)
# --------------------------------------------------------------------
# NOTE: the display routine used in this script requires a terminal
# that supports ANSI escape sequences. Windows users, sorry. :(
# --------------------------------------------------------------------
# --------------------------------------------------------------------
# 1. Allow the maze to be customized via command-line parameters
# --------------------------------------------------------------------
width = (ARGV[0] || 10).to_i
height = (ARGV[1] || width).to_i
density = (ARGV[2] || 50).to_i
seed = (ARGV[3] || rand(0xFFFF_FFFF)).to_i
delay = (ARGV[4] || 0.01).to_f
srand(seed)
# --------------------------------------------------------------------
# 2. Set up constants to aid with describing the passage directions
# --------------------------------------------------------------------
N, S, E, W, U = 0x1, 0x2, 0x4, 0x8, 0x10
DX = { E => 1, W => -1, N => 0, S => 0 }
DY = { E => 0, W => 0, N => -1, S => 1 }
OPPOSITE = { E => W, W => E, N => S, S => N }
# --------------------------------------------------------------------
# 3. Data structures and methods to assist the algorithm
# --------------------------------------------------------------------
EW, NS, SE, SW, NE, NW = [0x80, 0x82, 0x8C, 0x90, 0x94, 0x98].map { |v| "\xE2\x94#{v.chr}" }
NSE, NSW, EWS, EWN = [0x9C, 0xA4, 0xAC, 0xB4].map { |v| "\xE2\x94#{v.chr}" }
TILES = {
0 => ["\e[47m \e[m", "\e[47m \e[m"],
N => ["#{NS} #{NS}", "#{NE}#{EW}#{NW}"],
S => ["#{SE}#{EW}#{SW}", "#{NS} #{NS}"],
E => ["#{SE}#{EW}#{EW}", "#{NE}#{EW}#{EW}"],
W => ["#{EW}#{EW}#{SW}", "#{EW}#{EW}#{NW}"],
N|S => ["#{NS} #{NS}", "#{NS} #{NS}"],
N|W => ["#{NW} #{NS}", "#{EW}#{EW}#{NW}"],
N|E => ["#{NS} #{NE}", "#{NE}#{EW}#{EW}"],
S|W => ["#{EW}#{EW}#{SW}", "#{SW} #{NS}"],
S|E => ["#{SE}#{EW}#{EW}", "#{NS} #{SE}"],
E|W => ["#{EW}#{EW}#{EW}", "#{EW}#{EW}#{EW}"],
N|S|E => ["#{NS} #{NE}", "#{NS} #{SE}"],
N|S|W => ["#{NW} #{NS}", "#{SW} #{NS}"],
E|W|N => ["#{NW} #{NE}", "#{EW}#{EW}#{EW}"],
E|W|S => ["#{EW}#{EW}#{EW}", "#{SW} #{SE}"],
N|S|E|W => ["#{NW} #{NE}", "#{SW} #{SE}"],
N|S|U => ["#{NSW} #{NSE}", "#{NSW} #{NSE}"],
E|W|U => ["#{EWN}#{EW}#{EWN}", "#{EWS}#{EW}#{EWS}"]
}
def display_maze(grid, sets, delay=0, show_sets=false)
print "\e[H" # move to upper-left
grid.each_with_index do |row, y|
2.times do |i|
row.each { |cell| print TILES[cell][i] } if row
if show_sets && i == 1
print " "
sets[y].each { |set| print " %2s" % set.root.id.to_s(36) }
end
puts
end
end
sleep(delay)
end
class Tree
attr_accessor :parent
attr_reader :id
@@__next_id = -1
def self.next_id
@@__next_id += 1
end
def initialize
@parent = nil
@id = self.class.next_id
end
def root
@parent ? @parent.root : self
end
def connected?(tree)
root == tree.root
end
def connect(tree)
tree.root.parent = self if not self.connected?(tree)
end
end
grid = Array.new(height)
grid[0] = Array.new(width, 0)
grid[height-1] = Array.new(width, 0)
# --------------------------------------------------------------------
# 4. Build the over/under locations
# --------------------------------------------------------------------
print "\e[2J" # clear the screen
sets = Array.new(height) { Array.new(width) { Tree.new } }
1.upto(height-2) do |cy|
# Generate a candidate row of random crossings
grid[cy] = row = Array.new(width, 0)
segment_lengths = Hash.new(0)
segment_start = 0
1.upto(width-2) do |cx|
if rand(100) < density
row[cx] = [E|W|U, N|S|U][rand(2)]
if segment_start == 0
segment_start = cx
end
segment_lengths[segment_start] += 1
display_maze(grid, sets, delay)
else
segment_start = 0
end
end
# Now check whether it would create a loop, and try again if so
has_loop = false
segment_lengths.each do |x, len|
if sets[cy][x - 1].connected?(sets[cy][x + len])
has_loop = true
break
end
if segment_lengths.has_key?(x + len + 1)
len += 1 + segment_lengths[x + len + 1]
redo
end
end
redo if has_loop
# Update the connections and the grid
segment_lengths.each do |x, len|
grid[cy][x - 1] |= E
grid[cy][x + len] |= W
display_maze(grid, sets, delay)
sets[cy][x - 1].connect(sets[cy][x + len])
end
width.times do |x|
if (row[x] & U) != 0
if (grid[cy - 1][x] & U) == 0
grid[cy - 1][x] |= S
display_maze(grid, sets, delay)
sets[cy+1][x].connect(sets[cy-1][x])
else
sets[cy+1][x].connect(sets[cy][x])
end
elsif (grid[cy-1][x] & U) != 0
grid[cy][x] |= N
display_maze(grid, sets, delay)
end
end
end
width.times do |x|
if (grid[height-2][x] & U) != 0
grid[height-1][x] |= N
display_maze(grid, sets, delay)
end
end
puts
puts "--- PHASE 1 DONE; PRESS ENTER TO START PHASE 2 ---"
STDIN.gets
print "\e[2J" # clear the screen
# --------------------------------------------------------------------
# 5. Kruskal's algorithm
# --------------------------------------------------------------------
# build the list of edges
edges = []
height.times do |y|
width.times do |x|
edges << [x, y, N] if y > 0 and (grid[y][x] & (N|U)) == 0
edges << [x, y, W] if x > 0 and (grid[y][x] & (W|U)) == 0
end
end
edges = edges.sort_by{rand}
# Add edges at random until there are none left
until edges.empty?
x, y, direction = edges.pop
nx, ny = x + DX[direction], y + DY[direction]
set1, set2 = sets[y][x], sets[ny][nx]
unless set1.connected?(set2)
set1.connect(set2)
grid[y][x] |= direction
grid[ny][nx] |= OPPOSITE[direction]
display_maze(grid, sets, delay)
end
end
# --------------------------------------------------------------------
# 6. Show the parameters used to build this maze, for repeatability
# --------------------------------------------------------------------
puts "#{$0} #{width} #{height} #{density} #{seed}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment