theseus 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +137 -0
- data/Rakefile +42 -0
- data/bin/theseus +262 -0
- data/examples/a-star-search.rb +106 -0
- data/lib/theseus.rb +6 -0
- data/lib/theseus/delta_maze.rb +45 -0
- data/lib/theseus/formatters/ascii.rb +41 -0
- data/lib/theseus/formatters/ascii/delta.rb +79 -0
- data/lib/theseus/formatters/ascii/orthogonal.rb +156 -0
- data/lib/theseus/formatters/ascii/sigma.rb +57 -0
- data/lib/theseus/formatters/ascii/upsilon.rb +67 -0
- data/lib/theseus/formatters/png.rb +183 -0
- data/lib/theseus/formatters/png/delta.rb +85 -0
- data/lib/theseus/formatters/png/orthogonal.rb +87 -0
- data/lib/theseus/formatters/png/sigma.rb +105 -0
- data/lib/theseus/formatters/png/upsilon.rb +137 -0
- data/lib/theseus/mask.rb +113 -0
- data/lib/theseus/maze.rb +855 -0
- data/lib/theseus/orthogonal_maze.rb +195 -0
- data/lib/theseus/path.rb +91 -0
- data/lib/theseus/sigma_maze.rb +107 -0
- data/lib/theseus/solvers/astar.rb +144 -0
- data/lib/theseus/solvers/backtracker.rb +79 -0
- data/lib/theseus/solvers/base.rb +95 -0
- data/lib/theseus/upsilon_maze.rb +37 -0
- data/lib/theseus/version.rb +10 -0
- data/test/maze_test.rb +193 -0
- metadata +104 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'theseus/formatters/png'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
module Formatters
|
5
|
+
class PNG
|
6
|
+
# Renders a UpsilonMaze to a PNG canvas. Does not currently support the
|
7
|
+
# +:wall_width+ option.
|
8
|
+
#
|
9
|
+
# You will almost never access this class directly. Instead, use
|
10
|
+
# UpsilonMaze#to(:png, options) to return the raw PNG data directly.
|
11
|
+
class Upsilon < PNG
|
12
|
+
# Create and return a fully initialized PNG::Upsilon object, with the
|
13
|
+
# maze rendered. To get the maze data, call #to_blob.
|
14
|
+
#
|
15
|
+
# See Theseus::Formatters::PNG for a list of all supported options.
|
16
|
+
def initialize(maze, options={})
|
17
|
+
super
|
18
|
+
|
19
|
+
width = @options[:outer_padding] * 2 + (3 * maze.width + 1) * @options[:cell_size] / 4
|
20
|
+
height = @options[:outer_padding] * 2 + (3 * maze.height + 1) * @options[:cell_size] / 4
|
21
|
+
|
22
|
+
canvas = ChunkyPNG::Image.new(width, height, @options[:background])
|
23
|
+
|
24
|
+
metrics = { size: @options[:cell_size] - @options[:cell_padding] * 2 }
|
25
|
+
metrics[:s4] = metrics[:size] / 4.0
|
26
|
+
metrics[:inc] = 3 * @options[:cell_size] / 4.0
|
27
|
+
|
28
|
+
maze.height.times do |y|
|
29
|
+
py = @options[:outer_padding] + y * metrics[:inc]
|
30
|
+
maze.row_length(y).times do |x|
|
31
|
+
cell = maze[x, y]
|
32
|
+
next if cell == 0
|
33
|
+
|
34
|
+
px = @options[:outer_padding] + x * metrics[:inc]
|
35
|
+
|
36
|
+
if (y + x) % 2 == 0
|
37
|
+
draw_octogon_cell(canvas, [x, y], px, py, cell, metrics)
|
38
|
+
else
|
39
|
+
draw_square_cell(canvas, [x, y], px, py, cell, metrics)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@blob = canvas.to_blob
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def draw_octogon_cell(canvas, point, x, y, cell, metrics) #:nodoc:
|
50
|
+
p1 = [x + options[:cell_padding] + metrics[:s4], y + options[:cell_padding]]
|
51
|
+
p2 = [x + options[:cell_size] - options[:cell_padding] - metrics[:s4], p1[1]]
|
52
|
+
p3 = [x + options[:cell_size] - options[:cell_padding], y + options[:cell_padding] + metrics[:s4]]
|
53
|
+
p4 = [p3[0], y + options[:cell_size] - options[:cell_padding] - metrics[:s4]]
|
54
|
+
p5 = [p2[0], y + options[:cell_size] - options[:cell_padding]]
|
55
|
+
p6 = [p1[0], p5[1]]
|
56
|
+
p7 = [x + options[:cell_padding], p4[1]]
|
57
|
+
p8 = [p7[0], p3[1]]
|
58
|
+
|
59
|
+
fill_poly(canvas, [p1, p2, p3, p4, p5, p6, p7, p8], color_at(point))
|
60
|
+
|
61
|
+
any = proc { |x| x | (x << Maze::UNDER_SHIFT) }
|
62
|
+
|
63
|
+
if cell & any[Maze::NE] != 0
|
64
|
+
far_p6 = move(p6, metrics[:inc], -metrics[:inc])
|
65
|
+
far_p7 = move(p7, metrics[:inc], -metrics[:inc])
|
66
|
+
fill_poly(canvas, [p2, far_p7, far_p6, p3], color_at(point, any[Maze::NE]))
|
67
|
+
line(canvas, p2, far_p7, options[:wall_color])
|
68
|
+
line(canvas, p3, far_p6, options[:wall_color])
|
69
|
+
end
|
70
|
+
|
71
|
+
if cell & any[Maze::E] != 0
|
72
|
+
edge = (x + options[:cell_size] + options[:cell_padding] > canvas.width)
|
73
|
+
r1, r2 = p3, edge ? move(p4, options[:cell_padding], 0) : move(p7, options[:cell_size], 0)
|
74
|
+
fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[Maze::E]))
|
75
|
+
line(canvas, r1, [r2[0], r1[1]], options[:wall_color])
|
76
|
+
line(canvas, r2, [r1[0], r2[1]], options[:wall_color])
|
77
|
+
end
|
78
|
+
|
79
|
+
if cell & any[Maze::SE] != 0
|
80
|
+
far_p1 = move(p1, metrics[:inc], metrics[:inc])
|
81
|
+
far_p8 = move(p8, metrics[:inc], metrics[:inc])
|
82
|
+
fill_poly(canvas, [p4, far_p1, far_p8, p5], color_at(point, any[Maze::SE]))
|
83
|
+
line(canvas, p4, far_p1, options[:wall_color])
|
84
|
+
line(canvas, p5, far_p8, options[:wall_color])
|
85
|
+
end
|
86
|
+
|
87
|
+
if cell & any[Maze::S] != 0
|
88
|
+
r1, r2 = p6, move(p2, 0, options[:cell_size])
|
89
|
+
fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[Maze::S]))
|
90
|
+
line(canvas, r1, [r1[0], r2[1]], options[:wall_color])
|
91
|
+
line(canvas, r2, [r2[0], r1[1]], options[:wall_color])
|
92
|
+
end
|
93
|
+
|
94
|
+
line(canvas, p1, p2, options[:wall_color]) if cell & Maze::N == 0
|
95
|
+
line(canvas, p2, p3, options[:wall_color]) if cell & Maze::NE == 0
|
96
|
+
line(canvas, p3, p4, options[:wall_color]) if cell & Maze::E == 0
|
97
|
+
line(canvas, p4, p5, options[:wall_color]) if cell & Maze::SE == 0
|
98
|
+
line(canvas, p5, p6, options[:wall_color]) if cell & Maze::S == 0
|
99
|
+
line(canvas, p6, p7, options[:wall_color]) if cell & Maze::SW == 0
|
100
|
+
line(canvas, p7, p8, options[:wall_color]) if cell & Maze::W == 0
|
101
|
+
line(canvas, p8, p1, options[:wall_color]) if cell & Maze::NW == 0
|
102
|
+
end
|
103
|
+
|
104
|
+
def draw_square_cell(canvas, point, x, y, cell, metrics) #:nodoc:
|
105
|
+
v = options[:cell_padding] + metrics[:s4]
|
106
|
+
p1 = [x + v, y + v]
|
107
|
+
p2 = [x + options[:cell_size] - v, y + options[:cell_size] - v]
|
108
|
+
|
109
|
+
fill_rect(canvas, p1[0], p1[1], p2[0], p2[1], color_at(point))
|
110
|
+
|
111
|
+
any = proc { |x| x | (x << Maze::UNDER_SHIFT) }
|
112
|
+
|
113
|
+
if cell & any[Maze::E] != 0
|
114
|
+
r1 = [p2[0], p1[1]]
|
115
|
+
r2 = [x + metrics[:inc] + v, p2[1]]
|
116
|
+
fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[Maze::E]))
|
117
|
+
line(canvas, r1, [r2[0], r1[1]], options[:wall_color])
|
118
|
+
line(canvas, [r1[0], r2[1]], r2, options[:wall_color])
|
119
|
+
end
|
120
|
+
|
121
|
+
if cell & any[Maze::S] != 0
|
122
|
+
r1 = [p1[0], p2[1]]
|
123
|
+
r2 = [p2[0], y + metrics[:inc] + v]
|
124
|
+
fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[Maze::S]))
|
125
|
+
line(canvas, r1, [r1[0], r2[1]], options[:wall_color])
|
126
|
+
line(canvas, [r2[0], r1[1]], r2, options[:wall_color])
|
127
|
+
end
|
128
|
+
|
129
|
+
line(canvas, p1, [p2[0], p1[1]], options[:wall_color]) if cell & Maze::N == 0
|
130
|
+
line(canvas, [p2[0], p1[1]], p2, options[:wall_color]) if cell & Maze::E == 0
|
131
|
+
line(canvas, [p1[0], p2[1]], p2, options[:wall_color]) if cell & Maze::S == 0
|
132
|
+
line(canvas, p1, [p1[0], p2[1]], options[:wall_color]) if cell & Maze::W == 0
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/lib/theseus/mask.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'chunky_png'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
# A "mask" is, conceptually, a grid of true/false values that corresponds,
|
5
|
+
# one-to-one, with the cells of a maze object. For every mask cell that is true,
|
6
|
+
# the corresponding cell in a maze may contain passages. For every mask cell that
|
7
|
+
# is false, the corresponding maze cell must be blank.
|
8
|
+
#
|
9
|
+
# Any object may be used as a mask as long as it responds to #height, #width, and
|
10
|
+
# #[].
|
11
|
+
class Mask
|
12
|
+
# Given a string, treat each line as rows and each character as a cell. Every
|
13
|
+
# period character (".") will be mapped to +true+, and everything else to +false+.
|
14
|
+
# This lets you define simple masks as ASCII art:
|
15
|
+
#
|
16
|
+
# mask_string = <<MASK
|
17
|
+
# ..........
|
18
|
+
# .X....XXX.
|
19
|
+
# ..X....XX.
|
20
|
+
# ...X....X.
|
21
|
+
# ....X.....
|
22
|
+
# .....X....
|
23
|
+
# .X....X...
|
24
|
+
# .XX....X..
|
25
|
+
# .XXX....X.
|
26
|
+
# ..........
|
27
|
+
# MASK
|
28
|
+
#
|
29
|
+
# mask = Theseus::Mask.from_text(mask_string)
|
30
|
+
#
|
31
|
+
def self.from_text(text)
|
32
|
+
new(text.strip.split(/\n/).map { |line| line.split(//).map { |c| c == '.' } })
|
33
|
+
end
|
34
|
+
|
35
|
+
# Given a PNG file with the given +file_name+, read the file and create a new
|
36
|
+
# mask where transparent pixels will be considered +true+, and all others +false+.
|
37
|
+
# Note that a pixel with any transparency at all will be considered +true+.
|
38
|
+
#
|
39
|
+
# The resulting mask will have the same dimensions as the image file.
|
40
|
+
def self.from_png(file_name)
|
41
|
+
image = ChunkyPNG::Image.from_file(file_name)
|
42
|
+
grid = Array.new(image.height) { |y| Array.new(image.width) { |x| (image[x, y] & 0xff) == 0 } }
|
43
|
+
new(grid)
|
44
|
+
end
|
45
|
+
|
46
|
+
# The number of rows in the mask.
|
47
|
+
attr_reader :height
|
48
|
+
|
49
|
+
# the length of the longest row in the mask.
|
50
|
+
attr_reader :width
|
51
|
+
|
52
|
+
# Instantiate a new mask from the given grid, which must be an Array of rows, and each
|
53
|
+
# row must be an Array of true/false values for each column in the row.
|
54
|
+
def initialize(grid)
|
55
|
+
@grid = grid
|
56
|
+
@height = @grid.length
|
57
|
+
@width = @grid.map { |row| row.length }.max
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the +true+/+false+ value for the corresponding cell in the grid.
|
61
|
+
def [](x,y)
|
62
|
+
@grid[y][x]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# This is a specialized mask, intended for use with DeltaMaze instances (although
|
67
|
+
# it will work with any maze). This lets you easily create triangular delta mazes.
|
68
|
+
#
|
69
|
+
# mask = Theseus::TriangleMask.new(10)
|
70
|
+
# maze = Theseus::DeltaMaze.generate(mask: mask)
|
71
|
+
class TriangleMask
|
72
|
+
attr_reader :height, :width
|
73
|
+
|
74
|
+
# Returns a new TriangleMask instance with the given height. The width will
|
75
|
+
# always be <code>2h+1</code> (where +h+ is the height).
|
76
|
+
def initialize(height)
|
77
|
+
@height = height
|
78
|
+
@width = @height * 2 + 1
|
79
|
+
@grid = Array.new(@height) do |y|
|
80
|
+
run = y * 2 + 1
|
81
|
+
from = @height - y
|
82
|
+
to = from + run - 1
|
83
|
+
Array.new(@width) do |x|
|
84
|
+
(x >= from && x <= to) ? true : false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the +true+/+false+ value for the corresponding cell in the grid.
|
90
|
+
def [](x,y)
|
91
|
+
@grid[y][x]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# This is the default mask used by a maze when an explicit mask is not given.
|
96
|
+
# It simply reports every cell as available.
|
97
|
+
#
|
98
|
+
# mask = Theseus::TransparentMask.new(20, 20)
|
99
|
+
# maze = Theseus::OrthogonalMaze.new(mask: mask)
|
100
|
+
class TransparentMask
|
101
|
+
attr_reader :width, :height
|
102
|
+
|
103
|
+
def initialize(width=0, height=0)
|
104
|
+
@width = width
|
105
|
+
@height = height
|
106
|
+
end
|
107
|
+
|
108
|
+
# Always returns +true+.
|
109
|
+
def [](x,y)
|
110
|
+
true
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/theseus/maze.rb
ADDED
@@ -0,0 +1,855 @@
|
|
1
|
+
require 'theseus/mask'
|
2
|
+
require 'theseus/path'
|
3
|
+
|
4
|
+
module Theseus
|
5
|
+
# Theseus::Maze is an abstract class, intended to act solely as a superclass
|
6
|
+
# for specific maze types. Subclasses include OrthogonalMaze, DeltaMaze,
|
7
|
+
# SigmaMaze, and UpsilonMaze.
|
8
|
+
#
|
9
|
+
# Each cell in the maze is a bitfield. The bits that are set indicate which
|
10
|
+
# passages exist leading AWAY from this cell. Bits in the low byte (corresponding
|
11
|
+
# to the PRIMARY bitmask) represent passages on the normal plane. Bits
|
12
|
+
# in the high byte (corresponding to the UNDER bitmask) represent passages
|
13
|
+
# that are passing under this cell. (Under/over passages are controlled via the
|
14
|
+
# #weave setting, and are not supported by all maze types.)
|
15
|
+
#
|
16
|
+
# Mazes are generated using the recursive backtracking algorithm, which is fast,
|
17
|
+
# quite customizable, easily generalized to a variety of different maze types,
|
18
|
+
# and gives generally good results. On the down side, the current implementation
|
19
|
+
# will not regularly generate very challenging mazes, compared to human-built
|
20
|
+
# mazes of the same size.
|
21
|
+
class Maze
|
22
|
+
N = 0x01 # North
|
23
|
+
S = 0x02 # South
|
24
|
+
E = 0x04 # East
|
25
|
+
W = 0x08 # West
|
26
|
+
NW = 0x10 # Northwest
|
27
|
+
NE = 0x20 # Northeast
|
28
|
+
SW = 0x40 # Southwest
|
29
|
+
SE = 0x80 # Southeast
|
30
|
+
|
31
|
+
# bitmask identifying directional bits on the primary plane
|
32
|
+
PRIMARY = 0x00FF
|
33
|
+
|
34
|
+
# bitmask identifying directional bits under the primary plane
|
35
|
+
UNDER = 0xFF00
|
36
|
+
|
37
|
+
# The size of the PRIMARY bitmask (e.g. how far to the left the
|
38
|
+
# UNDER bitmask is shifted).
|
39
|
+
UNDER_SHIFT = 8
|
40
|
+
|
41
|
+
# The width of the maze (number of columns).
|
42
|
+
#
|
43
|
+
# In general, it is safest to use the #row_length method for a particular
|
44
|
+
# row, since it is theoretically possible for a maze subclass to describe
|
45
|
+
# a different width for each row.
|
46
|
+
attr_reader :width
|
47
|
+
|
48
|
+
# The height of the maze (number of rows).
|
49
|
+
attr_reader :height
|
50
|
+
|
51
|
+
# An integer between 0 and 100 (inclusive). 0 means passages will only
|
52
|
+
# change direction when they encounter a barrier they cannot move through
|
53
|
+
# (or under). 100 means that as passages are built, a new direction will
|
54
|
+
# always be randomly chosen for each step of the algorithm.
|
55
|
+
attr_reader :randomness
|
56
|
+
|
57
|
+
# An integer between 0 and 100 (inclusive). 0 means passages will never
|
58
|
+
# move over or under existing passages. 100 means whenever possible,
|
59
|
+
# passages will move over or under existing passages. Note that not all
|
60
|
+
# maze types support weaving.
|
61
|
+
attr_reader :weave
|
62
|
+
|
63
|
+
# An integer between 0 and 100 (inclusive), signifying the percentage
|
64
|
+
# of deadends in the maze that will be extended in some direction until
|
65
|
+
# they join with an existing passage. This will create loops in the
|
66
|
+
# graph. Thus, 0 is a "perfect" maze (with no loops), and 100 is a
|
67
|
+
# maze that is totally multiply-connected, with no dead-ends.
|
68
|
+
attr_reader :braid
|
69
|
+
|
70
|
+
# One of :none, :x, :y, or :xy, indicating which boundaries the maze
|
71
|
+
# should wrap around. The default is :none, indicating no wrapping.
|
72
|
+
# If :x, the maze will wrap around the left and right edges. If
|
73
|
+
# :y, the maze will wrap around the top and bottom edges. If :xy, the
|
74
|
+
# maze will wrap around both edges.
|
75
|
+
#
|
76
|
+
# A maze that wraps in a single direction may be mapped onto a cylinder.
|
77
|
+
# A maze that wraps in both x and y may be mapped onto a torus.
|
78
|
+
attr_reader :wrap
|
79
|
+
|
80
|
+
# A Theseus::Mask (or similar) instance, that is used by the algorithm to
|
81
|
+
# determine which cells in the space are allowed. This lets you create
|
82
|
+
# mazes that fill shapes, or flow around patterns.
|
83
|
+
attr_reader :mask
|
84
|
+
|
85
|
+
# One of :none, :x, :y, :xy, or :radial. Note that not all maze types
|
86
|
+
# support symmetry. The :x symmetry means the maze will be mirrored
|
87
|
+
# across the x axis. Similarly, :y symmetry means the maze will be
|
88
|
+
# mirrored across the y axis. :xy symmetry causes the maze to be
|
89
|
+
# mirrored across both axes, and :radial symmetry causes the maze to
|
90
|
+
# be mirrored radially about the center of the maze.
|
91
|
+
attr_reader :symmetry
|
92
|
+
|
93
|
+
# A 2-tuple (array) indicating the x and y coordinates where the maze
|
94
|
+
# should be entered. This is used primarly when generating the solution
|
95
|
+
# to the maze, and generally defaults to the upper-left corner.
|
96
|
+
attr_reader :entrance
|
97
|
+
|
98
|
+
# A 2-tuple (array) indicating the x and y coordinates where the maze
|
99
|
+
# should be exited. This is used primarly when generating the solution
|
100
|
+
# to the maze, and generally defaults to the lower-right corner.
|
101
|
+
attr_reader :exit
|
102
|
+
|
103
|
+
# The x-coordinate that the generation algorithm will consider next.
|
104
|
+
# This value is meaningless once a maze has been generated.
|
105
|
+
attr_reader :x
|
106
|
+
|
107
|
+
# The y-coordinate that the generation algorithm will consider next.
|
108
|
+
# This value is meaningless once a maze has been generated.
|
109
|
+
attr_reader :y
|
110
|
+
|
111
|
+
# A short-hand method for creating a new maze object and causing it to
|
112
|
+
# be generated, in one step. Returns the newly generated maze.
|
113
|
+
def self.generate(options={})
|
114
|
+
new(options).generate!
|
115
|
+
end
|
116
|
+
|
117
|
+
# Creates and returns a new maze object. Note that the maze will _not_
|
118
|
+
# be generated; the maze is initially blank.
|
119
|
+
#
|
120
|
+
# Many options are supported:
|
121
|
+
#
|
122
|
+
# [:width] The number of columns in the maze. Note that different
|
123
|
+
# maze types count columns and rows differently; you'll
|
124
|
+
# want to see individual maze types for more info.
|
125
|
+
# [:height] The number of rows in the maze.
|
126
|
+
# [:symmetry] The symmetry to be used when generating the maze. This
|
127
|
+
# defaults to +:none+, but may also be +:x+ (to have the
|
128
|
+
# maze mirrored across the x-axis), +:y+ (to mirror the
|
129
|
+
# maze across the y-axis), +:xy+ (to mirror across both
|
130
|
+
# axes simultaneously), and +:radial+ (to mirror the maze
|
131
|
+
# radially about the center). Some symmetry types may
|
132
|
+
# result in loops being added to the maze, regardless of
|
133
|
+
# the braid value (see the +:braid+ parameter).
|
134
|
+
# (NOTE: not all maze types support symmetry equally.)
|
135
|
+
# [:randomness] An integer between 0 and 100 (inclusive) indicating how
|
136
|
+
# randomly the maze is generated. A 0 means that the maze
|
137
|
+
# passages will prefer to go straight whenever possible.
|
138
|
+
# A 100 means the passages will choose random directions
|
139
|
+
# as often as possible.
|
140
|
+
# [:mask] An instance of Theseus::Mask (or something that acts
|
141
|
+
# similarly). This can be used to constrain the maze so that
|
142
|
+
# it fills or avoids specific areas, so that shapes and
|
143
|
+
# patterns can be made.
|
144
|
+
# [:weave] An integer between 0 and 100 (inclusive) indicating how
|
145
|
+
# frequently passages move under or over other passages.
|
146
|
+
# A 0 means the passages will never move over/under other
|
147
|
+
# passages, while a 100 means they will do so as often
|
148
|
+
# as possible. (NOTE: not all maze types support weaving.)
|
149
|
+
# [:braid] An integer between 0 and 100 (inclusive) representing
|
150
|
+
# the percentage of dead-ends that should be removed after
|
151
|
+
# the maze has been generated. Dead-ends are removed by
|
152
|
+
# extending them in some direction until they join with
|
153
|
+
# another passage. This will introduce loops into the maze,
|
154
|
+
# making it "multiply-connected". A braid value of 0 will
|
155
|
+
# always result in a "perfect" maze (with no loops), while
|
156
|
+
# a value of 100 will result in a maze with no dead-ends.
|
157
|
+
# [:wrap] Indicates which edges of the maze should wrap around.
|
158
|
+
# +:x+ will cause the left and right edges to wrap, and
|
159
|
+
# +:y+ will cause the top and bottom edges to wrap. You
|
160
|
+
# can specify +:xy+ to wrap both left-to-right and
|
161
|
+
# top-to-bottom. The default is +:none+ (for no wrapping).
|
162
|
+
# [:entrance] A 2-tuple indicating from where the maze is entered.
|
163
|
+
# By default, the maze's entrance will be the upper-left-most
|
164
|
+
# point. Note that it may lie outside the bounds of the maze
|
165
|
+
# by one cell (e.g. [-1,0]), indicating that the entrance
|
166
|
+
# is on the very edge of the maze.
|
167
|
+
# [:exit] A 2-tuple indicating from where the maze is exited.
|
168
|
+
# By default, the maze's entrance will be the lower-right-most
|
169
|
+
# point. Note that it may lie outside the bounds of the maze
|
170
|
+
# by one cell (e.g. [width,height-1]), indicating that the
|
171
|
+
# exit is on the very edge of the maze.
|
172
|
+
# [:prebuilt] Sometimes, you may want the new maze to be considered to be
|
173
|
+
# generated, but not actually have anything generated into it.
|
174
|
+
# You can set the +:prebuilt+ parameter to +true+ in this case,
|
175
|
+
# allowing you to then set the contents of the maze by hand,
|
176
|
+
# using the #[]= method.
|
177
|
+
def initialize(options={})
|
178
|
+
@width = (options[:width] || 10).to_i
|
179
|
+
@height = (options[:height] || 10).to_i
|
180
|
+
|
181
|
+
@symmetry = (options[:symmetry] || :none).to_sym
|
182
|
+
configure_symmetry
|
183
|
+
|
184
|
+
@randomness = options[:randomness] || 100
|
185
|
+
@mask = options[:mask] || TransparentMask.new
|
186
|
+
@weave = options[:weave].to_i
|
187
|
+
@braid = options[:braid].to_i
|
188
|
+
@wrap = options[:wrap] || :none
|
189
|
+
|
190
|
+
@cells = setup_grid or raise "expected #setup_grid to return the new grid"
|
191
|
+
|
192
|
+
@entrance = options[:entrance] || default_entrance
|
193
|
+
@exit = options[:exit] || default_exit
|
194
|
+
|
195
|
+
loop do
|
196
|
+
@y = rand(@cells.length)
|
197
|
+
@x = rand(@cells[@y].length)
|
198
|
+
break if valid?(@x, @y)
|
199
|
+
end
|
200
|
+
|
201
|
+
@tries = potential_exits_at(@x, @y).sort_by { rand }
|
202
|
+
@stack = []
|
203
|
+
|
204
|
+
@generated = options[:prebuilt]
|
205
|
+
end
|
206
|
+
|
207
|
+
# Generates the maze if it has not already been generated. This is
|
208
|
+
# essentially the same as calling #step repeatedly. If a block is given,
|
209
|
+
# it will be called after each step.
|
210
|
+
def generate!
|
211
|
+
yield if block_given? while step unless generated?
|
212
|
+
self
|
213
|
+
end
|
214
|
+
|
215
|
+
# Creates a new Theseus::Path object based on this maze instance. This can
|
216
|
+
# be used to (for instance) create special areas of the maze or routes through
|
217
|
+
# the maze that you want to color specially. The following demonstrates setting
|
218
|
+
# a particular cell in the maze to a light-purple color:
|
219
|
+
#
|
220
|
+
# path = maze.new_path(color: 0xff7fffff)
|
221
|
+
# path.set([5,5])
|
222
|
+
# maze.to(:png, paths: [path])
|
223
|
+
def new_path(meta={})
|
224
|
+
Path.new(self, meta)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Instantiates and returns a new solver instance which encapsulates a
|
228
|
+
# solution algorithm. The options may contain the following keys:
|
229
|
+
#
|
230
|
+
# [:type] This defaults to +:backtracker+ (for the Theseus::Solvers::Backtracker
|
231
|
+
# solver), but may also be set to +:astar+ (for the Theseus::Solvers::Astar
|
232
|
+
# solver).
|
233
|
+
# [:a] A 2-tuple (defaulting to #start) that says where in the maze the
|
234
|
+
# solution should begin.
|
235
|
+
# [:b] A 2-tuple (defaulting to #finish) that says where in the maze the
|
236
|
+
# solution should finish.
|
237
|
+
#
|
238
|
+
# The returned solver will not yet have generated the solution. Use
|
239
|
+
# Theseus::Solvers::Base#solve or Theseus::Solvers::Base#step to generate the
|
240
|
+
# solution.
|
241
|
+
def new_solver(options={})
|
242
|
+
type = options[:type] || :backtracker
|
243
|
+
|
244
|
+
require "theseus/solvers/#{type}"
|
245
|
+
klass = Theseus::Solvers.const_get(type.to_s.capitalize)
|
246
|
+
|
247
|
+
a = options[:a] || start
|
248
|
+
b = options[:b] || finish
|
249
|
+
|
250
|
+
klass.new(self, a, b)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns the solution for the maze as an array of 2-tuples, each indicating
|
254
|
+
# a cell (in sequence) leading from the start to the finish.
|
255
|
+
#
|
256
|
+
# See #new_solver for a description of the supported options.
|
257
|
+
def solve(options={})
|
258
|
+
new_solver(options).solution
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns the bitfield for the cell at the given (+x+,+y+) coordinate.
|
262
|
+
def [](x,y)
|
263
|
+
@cells[y][x]
|
264
|
+
end
|
265
|
+
|
266
|
+
# Sets the bitfield for the cell at the given (+x+,+y+) coordinate.
|
267
|
+
def []=(x,y,value)
|
268
|
+
@cells[y][x] = value
|
269
|
+
end
|
270
|
+
|
271
|
+
# Completes a single iteration of the maze generation algorithm. Returns
|
272
|
+
# +false+ if the method should not be called again (e.g., the maze has
|
273
|
+
# been completed), and +true+ otherwise.
|
274
|
+
def step
|
275
|
+
return false if @generated
|
276
|
+
|
277
|
+
if @deadends && @deadends.any?
|
278
|
+
dead_end = @deadends.pop
|
279
|
+
braid(dead_end[0], dead_end[1])
|
280
|
+
|
281
|
+
@generated = @deadends.empty?
|
282
|
+
return !@generated
|
283
|
+
end
|
284
|
+
|
285
|
+
direction = next_direction or return !@generated
|
286
|
+
nx, ny = move(@x, @y, direction)
|
287
|
+
|
288
|
+
apply_move_at(@x, @y, direction)
|
289
|
+
|
290
|
+
# if (nx,ny) is already visited, then we're weaving (moving either over
|
291
|
+
# or under the existing passage).
|
292
|
+
nx, ny, direction = perform_weave(@x, @y, nx, ny, direction) if @cells[ny][nx] != 0
|
293
|
+
|
294
|
+
apply_move_at(nx, ny, opposite(direction))
|
295
|
+
|
296
|
+
@stack.push([@x, @y, @tries])
|
297
|
+
@tries = potential_exits_at(nx, ny).sort_by { rand }
|
298
|
+
@tries.push direction if @tries.include?(direction) unless rand(100) < @randomness
|
299
|
+
@x, @y = nx, ny
|
300
|
+
|
301
|
+
return true
|
302
|
+
end
|
303
|
+
|
304
|
+
# Returns +true+ if the maze has been generated.
|
305
|
+
def generated?
|
306
|
+
@generated
|
307
|
+
end
|
308
|
+
|
309
|
+
# Since #entrance may be external to the maze, #start returns the cell adjacent to
|
310
|
+
# #entrance that lies within the maze. If #entrance is already internal to the
|
311
|
+
# maze, this method returns #entrance. If #entrance is _not_ adjacent to any
|
312
|
+
# internal cell, this method returns +nil+.
|
313
|
+
def start
|
314
|
+
adjacent_point(@entrance)
|
315
|
+
end
|
316
|
+
|
317
|
+
# Since #exit may be external to the maze, #finish returns the cell adjacent to
|
318
|
+
# #exit that lies within the maze. If #exit is already internal to the
|
319
|
+
# maze, this method returns #exit. If #exit is _not_ adjacent to any
|
320
|
+
# internal cell, this method returns +nil+.
|
321
|
+
def finish
|
322
|
+
adjacent_point(@exit)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Returns an array of the possible exits for the cell at the given coordinates.
|
326
|
+
# Note that this does not take into account boundary conditions: a move in any
|
327
|
+
# of the returned directions may not actually be valid, and should be verified
|
328
|
+
# before being applied.
|
329
|
+
#
|
330
|
+
# This is used primarily by subclasses to allow for different shaped cells
|
331
|
+
# (e.g. hexagonal cells for SigmaMaze, octagonal cells for UpsilonMaze).
|
332
|
+
def potential_exits_at(x, y)
|
333
|
+
raise NotImplementedError, "subclasses must implement #potential_exits_at"
|
334
|
+
end
|
335
|
+
|
336
|
+
# Returns true if the maze may be wrapped in the x direction (left-to-right).
|
337
|
+
def wrap_x?
|
338
|
+
@wrap == :x || @wrap == :xy
|
339
|
+
end
|
340
|
+
|
341
|
+
# Returns true if the maze may be wrapped in the y direction (top-to-bottom).
|
342
|
+
def wrap_y?
|
343
|
+
@wrap == :y || @wrap == :xy
|
344
|
+
end
|
345
|
+
|
346
|
+
# Returns true if the given coordinates are valid within the maze. This will
|
347
|
+
# be the case if:
|
348
|
+
#
|
349
|
+
# 1. The coordinates lie within the maze's bounds, and
|
350
|
+
# 2. The current mask for the maze does not restrict the location.
|
351
|
+
#
|
352
|
+
# If the maze wraps in x, the x coordinate is unconstrained and will be
|
353
|
+
# mapped (via modulo) to the bounds. Similarly, if the maze wraps in y,
|
354
|
+
# the y coordinate will be unconstrained.
|
355
|
+
def valid?(x, y)
|
356
|
+
return false if !wrap_y? && (y < 0 || y >= height)
|
357
|
+
y %= height
|
358
|
+
return false if !wrap_x? && (x < 0 || x >= row_length(y))
|
359
|
+
x %= row_length(y)
|
360
|
+
return @mask[x, y]
|
361
|
+
end
|
362
|
+
|
363
|
+
# Moves the given (+x+,+y+) coordinates a single step in the given
|
364
|
+
# +direction+. If wrapping in either x or y is active, the result will
|
365
|
+
# be mapped to the maze's current bounds via modulo arithmetic. The
|
366
|
+
# resulting coordinates are returned as a 2-tuple.
|
367
|
+
#
|
368
|
+
# Example:
|
369
|
+
#
|
370
|
+
# x2, y2 = maze.move(x, y, Maze::W)
|
371
|
+
def move(x, y, direction)
|
372
|
+
nx, ny = x + dx(direction), y + dy(direction)
|
373
|
+
|
374
|
+
ny %= height if wrap_y?
|
375
|
+
nx %= row_length(ny) if wrap_x? && ny > 0 && ny < height
|
376
|
+
|
377
|
+
[nx, ny]
|
378
|
+
end
|
379
|
+
|
380
|
+
# Returns a array of all dead-ends in the maze. Each element of the array
|
381
|
+
# is a 2-tuple containing the coordinates of a dead-end.
|
382
|
+
def dead_ends
|
383
|
+
dead_ends = []
|
384
|
+
|
385
|
+
@cells.each_with_index do |row, y|
|
386
|
+
row.each_with_index do |cell, x|
|
387
|
+
dead_ends << [x, y] if dead?(cell)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
dead_ends
|
392
|
+
end
|
393
|
+
|
394
|
+
# Removes one cell from all dead-ends in the maze. Each call to this method
|
395
|
+
# removes another level of dead-ends, making the maze increasingly sparse.
|
396
|
+
def sparsify!
|
397
|
+
dead_ends.each do |(x, y)|
|
398
|
+
cell = @cells[y][x]
|
399
|
+
direction = cell & PRIMARY
|
400
|
+
nx, ny = move(x, y, direction)
|
401
|
+
|
402
|
+
# if the cell includes UNDER codes, shifting it all UNDER_SHIFT bits to the right
|
403
|
+
# will convert those UNDER codes to PRIMARY codes. Otherwise, it will
|
404
|
+
# simply zero the cell, resulting in a blank spot.
|
405
|
+
@cells[y][x] >>= UNDER_SHIFT
|
406
|
+
|
407
|
+
# if it's a weave cell (that moves over or under another corridor),
|
408
|
+
# nix it and move back one more, so we don't wind up with dead-ends
|
409
|
+
# underneath another corridor.
|
410
|
+
if @cells[ny][nx] & (opposite(direction) << UNDER_SHIFT) != 0
|
411
|
+
@cells[ny][nx] &= ~((direction | opposite(direction)) << UNDER_SHIFT)
|
412
|
+
nx, ny = move(nx, ny, direction)
|
413
|
+
end
|
414
|
+
|
415
|
+
@cells[ny][nx] &= ~opposite(direction)
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Returns the direction opposite to the given +direction+. This will work
|
420
|
+
# even if the +direction+ value is in the UNDER bitmask.
|
421
|
+
def opposite(direction)
|
422
|
+
if direction & UNDER != 0
|
423
|
+
opposite(direction >> UNDER_SHIFT) << UNDER_SHIFT
|
424
|
+
else
|
425
|
+
case direction
|
426
|
+
when N then S
|
427
|
+
when S then N
|
428
|
+
when E then W
|
429
|
+
when W then E
|
430
|
+
when NE then SW
|
431
|
+
when NW then SE
|
432
|
+
when SE then NW
|
433
|
+
when SW then NE
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
# Returns the direction that is the horizontal mirror to the given +direction+.
|
439
|
+
# This will work even if the +direction+ value is in the UNDER bitmask.
|
440
|
+
def hmirror(direction)
|
441
|
+
if direction & UNDER != 0
|
442
|
+
hmirror(direction >> UNDER_SHIFT) << UNDER_SHIFT
|
443
|
+
else
|
444
|
+
case direction
|
445
|
+
when E then W
|
446
|
+
when W then E
|
447
|
+
when NW then NE
|
448
|
+
when NE then NW
|
449
|
+
when SW then SE
|
450
|
+
when SE then SW
|
451
|
+
else direction
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
# Returns the direction that is the vertical mirror to the given +direction+.
|
457
|
+
# This will work even if the +direction+ value is in the UNDER bitmask.
|
458
|
+
def vmirror(direction)
|
459
|
+
if direction & UNDER != 0
|
460
|
+
vmirror(direction >> UNDER_SHIFT) << UNDER_SHIFT
|
461
|
+
else
|
462
|
+
case direction
|
463
|
+
when N then S
|
464
|
+
when S then N
|
465
|
+
when NE then SE
|
466
|
+
when NW then SW
|
467
|
+
when SE then NE
|
468
|
+
when SW then NW
|
469
|
+
else direction
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
# Returns the direction that results by rotating the given +direction+
|
475
|
+
# 90 degrees in the clockwise direction. This will work even if the +direction+
|
476
|
+
# value is in the UNDER bitmask.
|
477
|
+
def clockwise(direction)
|
478
|
+
if direction & UNDER != 0
|
479
|
+
clockwise(direction >> UNDER_SHIFT) << UNDER_SHIFT
|
480
|
+
else
|
481
|
+
case direction
|
482
|
+
when N then E
|
483
|
+
when E then S
|
484
|
+
when S then W
|
485
|
+
when W then N
|
486
|
+
when NW then NE
|
487
|
+
when NE then SE
|
488
|
+
when SE then SW
|
489
|
+
when SW then NW
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
# Returns the direction that results by rotating the given +direction+
|
495
|
+
# 90 degrees in the counter-clockwise direction. This will work even if
|
496
|
+
# the +direction+ value is in the UNDER bitmask.
|
497
|
+
def counter_clockwise(direction)
|
498
|
+
if direction & UNDER != 0
|
499
|
+
counter_clockwise(direction >> UNDER_SHIFT) << UNDER_SHIFT
|
500
|
+
else
|
501
|
+
case direction
|
502
|
+
when N then W
|
503
|
+
when W then S
|
504
|
+
when S then E
|
505
|
+
when E then N
|
506
|
+
when NW then SW
|
507
|
+
when SW then SE
|
508
|
+
when SE then NE
|
509
|
+
when NE then NW
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
# Returns the change in x implied by the given +direction+.
|
515
|
+
def dx(direction)
|
516
|
+
case direction
|
517
|
+
when E, NE, SE then 1
|
518
|
+
when W, NW, SW then -1
|
519
|
+
else 0
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
# Returns the change in y implied by the given +direction+.
|
524
|
+
def dy(direction)
|
525
|
+
case direction
|
526
|
+
when S, SE, SW then 1
|
527
|
+
when N, NE, NW then -1
|
528
|
+
else 0
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Returns the number of cells in the given row. This is generally safer
|
533
|
+
# than relying the #width method, since it is theoretically possible for
|
534
|
+
# a maze to have a different number of cells for each of its rows.
|
535
|
+
def row_length(row)
|
536
|
+
@cells[row].length
|
537
|
+
end
|
538
|
+
|
539
|
+
# Returns +true+ if the given cell is a dead-end. This considers only
|
540
|
+
# passages on the PRIMARY plane (the UNDER bits are ignored, because the
|
541
|
+
# current algorithm for generating mazes will never result in a dead-end
|
542
|
+
# that is underneath another passage).
|
543
|
+
def dead?(cell)
|
544
|
+
raw = cell & PRIMARY
|
545
|
+
raw == N || raw == S || raw == E || raw == W ||
|
546
|
+
raw == NE || raw == NW || raw == SE || raw == SW
|
547
|
+
end
|
548
|
+
|
549
|
+
# If +point+ is already located at a valid point within the maze, this
|
550
|
+
# does nothing. Otherwise, it examines the potential exits from the
|
551
|
+
# given point and looks for the first one that leads immediately to a
|
552
|
+
# valid point internal to the maze. When it finds one, it adds a passage
|
553
|
+
# to that cell leading to +point+. If no such adjacent cell exists, this
|
554
|
+
# method silently does nothing.
|
555
|
+
def add_opening_from(point)
|
556
|
+
x, y = point
|
557
|
+
if valid?(x, y)
|
558
|
+
# nothing to be done
|
559
|
+
else
|
560
|
+
potential_exits_at(x, y).each do |direction|
|
561
|
+
nx, ny = move(x, y, direction)
|
562
|
+
if valid?(nx, ny)
|
563
|
+
@cells[ny][nx] |= opposite(direction)
|
564
|
+
return
|
565
|
+
end
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
# If +point+ is already located at a valid point withint he maze, this
|
571
|
+
# simply returns +point+. Otherwise, it examines the potential exits
|
572
|
+
# from the given point and looks for the first one that leads immediately
|
573
|
+
# to a valid point internal to the maze. When it finds one, it returns
|
574
|
+
# that point. If no such point exists, it returns +nil+.
|
575
|
+
def adjacent_point(point)
|
576
|
+
x, y = point
|
577
|
+
if valid?(x, y)
|
578
|
+
point
|
579
|
+
else
|
580
|
+
potential_exits_at(x, y).each do |direction|
|
581
|
+
nx, ny = move(x, y, direction)
|
582
|
+
return [nx, ny] if valid?(nx, ny)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
# Returns the direction of +to+ relative to +from+. +to+ and +from+
|
588
|
+
# are both points (2-tuples).
|
589
|
+
def relative_direction(from, to)
|
590
|
+
if from[0] < to[0]
|
591
|
+
if from[1] < to[1]
|
592
|
+
SE
|
593
|
+
elsif from[1] > to[1]
|
594
|
+
NE
|
595
|
+
else
|
596
|
+
E
|
597
|
+
end
|
598
|
+
elsif from[0] > to[0]
|
599
|
+
if from[1] < to[1]
|
600
|
+
SW
|
601
|
+
elsif from[1] > to[1]
|
602
|
+
NW
|
603
|
+
else
|
604
|
+
W
|
605
|
+
end
|
606
|
+
elsif from[1] < to[1]
|
607
|
+
S
|
608
|
+
elsif from[1] > to[1]
|
609
|
+
N
|
610
|
+
else
|
611
|
+
# same point!
|
612
|
+
nil
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
# Applies a move in the given direction to the cell at (x,y). The +direction+
|
617
|
+
# parameter may also be :under, in which case the cell is left-shifted so as
|
618
|
+
# to move the existing passages to the UNDER plane.
|
619
|
+
#
|
620
|
+
# This method also handles the application of symmetrical moves, in the case
|
621
|
+
# where #symmetry has been specified.
|
622
|
+
#
|
623
|
+
# You'll generally never call this method directly, except to construct grids
|
624
|
+
# yourself.
|
625
|
+
def apply_move_at(x, y, direction)
|
626
|
+
if direction == :under
|
627
|
+
@cells[y][x] <<= UNDER_SHIFT
|
628
|
+
else
|
629
|
+
@cells[y][x] |= direction
|
630
|
+
end
|
631
|
+
|
632
|
+
case @symmetry
|
633
|
+
when :x then move_symmetrically_in_x(x, y, direction)
|
634
|
+
when :y then move_symmetrically_in_y(x, y, direction)
|
635
|
+
when :xy then move_symmetrically_in_xy(x, y, direction)
|
636
|
+
when :radial then move_symmetrically_radially(x, y, direction)
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
# Returns the type of the maze as a string. OrthogonalMaze, for
|
641
|
+
# instance, is reported as "orthogonal".
|
642
|
+
def type
|
643
|
+
self.class.name[/::(.*?)Maze$/, 1]
|
644
|
+
end
|
645
|
+
|
646
|
+
# Returns the maze rendered to a particular format. Supported
|
647
|
+
# formats are currently :ascii and :png. The +options+ hash is passed
|
648
|
+
# through to the formatter.
|
649
|
+
def to(format, options={})
|
650
|
+
case format
|
651
|
+
when :ascii then
|
652
|
+
require "theseus/formatters/ascii/#{type.downcase}"
|
653
|
+
Formatters::ASCII.const_get(type).new(self, options)
|
654
|
+
when :png then
|
655
|
+
require "theseus/formatters/png/#{type.downcase}"
|
656
|
+
Formatters::PNG.const_get(type).new(self, options).to_blob
|
657
|
+
else
|
658
|
+
raise ArgumentError, "unknown format: #{format.inspect}"
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
# Returns the maze rendered to a string.
|
663
|
+
def to_s(options={})
|
664
|
+
to(:ascii, options).to_s
|
665
|
+
end
|
666
|
+
|
667
|
+
def inspect # :nodoc:
|
668
|
+
"#<#{self.class.name}:0x%X %dx%d %s>" % [
|
669
|
+
object_id, @width, @height,
|
670
|
+
generated? ? "generated" : "not generated"]
|
671
|
+
end
|
672
|
+
|
673
|
+
private
|
674
|
+
|
675
|
+
# Not all maze types support symmetry. If a subclass supports any of the
|
676
|
+
# symmetry types (or wants to implement its own), it should override this
|
677
|
+
# method.
|
678
|
+
def configure_symmetry #:nodoc:
|
679
|
+
if @symmetry != :none
|
680
|
+
raise NotImplementedError, "only :none symmetry is implemented by default"
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
# The default grid should suffice for most maze types, but if a subclass
|
685
|
+
# wants a custom grid, it must override this method. Note that the method
|
686
|
+
# MUST always return an Array of rows, with each row being an Array of cells.
|
687
|
+
def setup_grid #:nodoc:
|
688
|
+
Array.new(height) { Array.new(width, 0) }
|
689
|
+
end
|
690
|
+
|
691
|
+
# Returns an array of deadends that ought to be braided (removed), based on
|
692
|
+
# the value of the #braid setting.
|
693
|
+
def deadends_to_braid #:nodoc:
|
694
|
+
return [] if @braid.zero?
|
695
|
+
|
696
|
+
ends = dead_ends
|
697
|
+
|
698
|
+
count = ends.length * @braid / 100
|
699
|
+
count = 1 if count < 1
|
700
|
+
|
701
|
+
ends.sort_by { rand }[0,count]
|
702
|
+
end
|
703
|
+
|
704
|
+
# Calculate the default entrance, by looking for the upper-leftmost point.
|
705
|
+
def default_entrance #:nodoc:
|
706
|
+
@cells.each_with_index do |row, y|
|
707
|
+
row.each_with_index do |cell, x|
|
708
|
+
return [x-1, y] if @mask[x, y]
|
709
|
+
end
|
710
|
+
end
|
711
|
+
[0, 0] # if every cell is masked, then 0,0 is as good as any!
|
712
|
+
end
|
713
|
+
|
714
|
+
# Calculate the default exit, by looking for the lower-rightmost point.
|
715
|
+
def default_exit #:nodoc:
|
716
|
+
@cells.reverse.each_with_index do |row, y|
|
717
|
+
ry = @cells.length - y - 1
|
718
|
+
row.reverse.each_with_index do |cell, x|
|
719
|
+
rx = row.length - x - 1
|
720
|
+
return [rx+1, ry] if @mask[rx, ry]
|
721
|
+
end
|
722
|
+
end
|
723
|
+
[0, 0] # if every cell is masked, then 0,0 is as good as any!
|
724
|
+
end
|
725
|
+
|
726
|
+
# Returns the next direction that ought to be attempted by the recursive
|
727
|
+
# backtracker. This will also handle the backtracking. If there are no
|
728
|
+
# more directions to attempt, and the stack is empty, this will return +nil+.
|
729
|
+
def next_direction #:nodoc:
|
730
|
+
loop do
|
731
|
+
direction = @tries.pop
|
732
|
+
nx, ny = move(@x, @y, direction)
|
733
|
+
|
734
|
+
if valid?(nx, ny) && (@cells[@y][@x] & (direction | (direction << UNDER_SHIFT)) == 0)
|
735
|
+
if @cells[ny][nx] == 0
|
736
|
+
return direction
|
737
|
+
elsif !dead?(@cells[ny][nx]) && @weave > 0 && rand(100) < @weave
|
738
|
+
# see if we can weave over/under the cell at (nx,ny)
|
739
|
+
return direction if weave_allowed?(@x, @y, nx, ny, direction)
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
while @tries.empty?
|
744
|
+
if @stack.empty?
|
745
|
+
finish!
|
746
|
+
return nil
|
747
|
+
else
|
748
|
+
@x, @y, @tries = @stack.pop
|
749
|
+
end
|
750
|
+
end
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
def move_symmetrically_in_x(x, y, direction) #:nodoc:
|
755
|
+
row_width = @cells[y].length
|
756
|
+
if direction == :under
|
757
|
+
@cells[y][row_width - x - 1] <<= UNDER_SHIFT
|
758
|
+
else
|
759
|
+
@cells[y][row_width - x - 1] |= hmirror(direction)
|
760
|
+
end
|
761
|
+
end
|
762
|
+
|
763
|
+
def move_symmetrically_in_y(x, y, direction) #:nodoc:
|
764
|
+
if direction == :under
|
765
|
+
@cells[@cells.length - y - 1][x] <<= UNDER_SHIFT
|
766
|
+
else
|
767
|
+
@cells[@cells.length - y - 1][x] |= vmirror(direction)
|
768
|
+
end
|
769
|
+
end
|
770
|
+
|
771
|
+
def move_symmetrically_in_xy(x, y, direction) #:nodoc:
|
772
|
+
row_width = @cells[y].length
|
773
|
+
if direction == :under
|
774
|
+
@cells[y][row_width - x - 1] <<= UNDER_SHIFT
|
775
|
+
@cells[@cells.length - y - 1][x] <<= UNDER_SHIFT
|
776
|
+
@cells[@cells.length - y - 1][row_width - x - 1] <<= UNDER_SHIFT
|
777
|
+
else
|
778
|
+
@cells[y][row_width - x - 1] |= hmirror(direction)
|
779
|
+
@cells[@cells.length - y - 1][x] |= vmirror(direction)
|
780
|
+
@cells[@cells.length - y - 1][row_width - x - 1] |= opposite(direction)
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
def move_symmetrically_radially(x, y, direction) #:nodoc:
|
785
|
+
row_width = @cells[y].length
|
786
|
+
if direction == :under
|
787
|
+
@cells[@cells.length - x - 1][y] <<= UNDER_SHIFT
|
788
|
+
@cells[x][row_width - y - 1] <<= UNDER_SHIFT
|
789
|
+
@cells[@cells.length - y - 1][row_width - x - 1] <<= UNDER_SHIFT
|
790
|
+
else
|
791
|
+
@cells[@cells.length - x - 1][y] |= counter_clockwise(direction)
|
792
|
+
@cells[x][row_width - y - 1] |= clockwise(direction)
|
793
|
+
@cells[@cells.length - y - 1][row_width - x - 1] |= opposite(direction)
|
794
|
+
end
|
795
|
+
end
|
796
|
+
|
797
|
+
# Finishes the generation of the maze by adding openings for the entrance
|
798
|
+
# and exit, and determing which dead-ends to braid (if any).
|
799
|
+
def finish! #:nodoc:
|
800
|
+
add_opening_from(@entrance)
|
801
|
+
add_opening_from(@exit)
|
802
|
+
|
803
|
+
@deadends = deadends_to_braid
|
804
|
+
@generated = @deadends.empty?
|
805
|
+
end
|
806
|
+
|
807
|
+
# If (x,y) is not a dead-end, this does nothing. Otherwise, it extends the
|
808
|
+
# dead-end in some direction until it joins with another passage.
|
809
|
+
#
|
810
|
+
# TODO: look for the direction that results in the longest loop.
|
811
|
+
# might be kind of spendy, but worth trying, at least.
|
812
|
+
def braid(x, y) #:nodoc:
|
813
|
+
return unless dead?(@cells[y][x])
|
814
|
+
tries = potential_exits_at(x, y)
|
815
|
+
[opposite(@cells[y][x]), *tries].each do |try|
|
816
|
+
next if try == @cells[y][x]
|
817
|
+
nx, ny = move(x, y, try)
|
818
|
+
if valid?(nx, ny)
|
819
|
+
opp = opposite(try)
|
820
|
+
next if @cells[ny][nx] & (opp << UNDER_SHIFT) != 0
|
821
|
+
@cells[y][x] |= try
|
822
|
+
@cells[ny][nx] |= opp
|
823
|
+
return
|
824
|
+
end
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
# Returns +true+ if a weave may be applied at (thru_x,thru_y) when moving
|
829
|
+
# from (from_x,from_y) in +direction+. This will be true if the thru cell
|
830
|
+
# does not already have anything in its UNDER plane, and if the cell
|
831
|
+
# on the far side of thru is valid and blank.
|
832
|
+
#
|
833
|
+
# Subclasses may need to override this method if special interpretations
|
834
|
+
# for +direction+ need to be considered (see SigmaMaze).
|
835
|
+
def weave_allowed?(from_x, from_y, thru_x, thru_y, direction) #:nodoc:
|
836
|
+
nx2, ny2 = move(thru_x, thru_y, direction)
|
837
|
+
return (@cells[thru_y][thru_x] & UNDER == 0) && valid?(nx2, ny2) && @cells[ny2][nx2] == 0
|
838
|
+
end
|
839
|
+
|
840
|
+
def perform_weave(from_x, from_y, to_x, to_y, direction) #:nodoc:
|
841
|
+
if rand(2) == 0 # move under existing passage
|
842
|
+
apply_move_at(to_x, to_y, direction << UNDER_SHIFT)
|
843
|
+
apply_move_at(to_x, to_y, opposite(direction) << UNDER_SHIFT)
|
844
|
+
else # move over existing passage
|
845
|
+
apply_move_at(to_x, to_y, :under)
|
846
|
+
apply_move_at(to_x, to_y, direction)
|
847
|
+
apply_move_at(to_x, to_y, opposite(direction))
|
848
|
+
end
|
849
|
+
|
850
|
+
nx, ny = move(to_x, to_y, direction)
|
851
|
+
[nx, ny, direction]
|
852
|
+
end
|
853
|
+
|
854
|
+
end
|
855
|
+
end
|