theseus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ require 'theseus/solvers/base'
2
+
3
+ module Theseus
4
+ module Solvers
5
+ # An implementation of a recursive backtracker for solving a maze. Although it will
6
+ # work (eventually) for multiply-connected mazes, it will almost certainly not
7
+ # return an optimal solution in that case. Thus, this solver is best suited only
8
+ # for "perfect" mazes (those with no loops).
9
+ #
10
+ # For mazes that contain loops, see the Theseus::Solvers::Astar class.
11
+ class Backtracker < Base
12
+ def initialize(maze, a=maze.start, b=maze.finish) #:nodoc:
13
+ super
14
+ @visits = Array.new(@maze.height) { Array.new(@maze.width, 0) }
15
+ @stack = []
16
+ end
17
+
18
+ VISIT_MASK = { false => 1, true => 2 }
19
+
20
+ def current_solution #:nodoc:
21
+ @stack[1..-1].map { |item| item[0] }
22
+ end
23
+
24
+ def step #:nodoc:
25
+ if @stack == [:fail]
26
+ return false
27
+ elsif @stack.empty?
28
+ @stack.push(:fail)
29
+ @stack.push([@a, @maze.potential_exits_at(@a[0], @a[1]).dup])
30
+ return @a.dup
31
+ elsif @stack.last[0] == @b
32
+ @solution = @stack[1..-1].map { |pt, tries| pt }
33
+ return false
34
+ else
35
+ x, y = @stack.last[0]
36
+ cell = @maze[x, y]
37
+ loop do
38
+ try = @stack.last[1].pop
39
+
40
+ if try.nil?
41
+ spot = @stack.pop
42
+ x, y = spot[0]
43
+ return :backtrack
44
+ elsif (cell & try) != 0
45
+ # is the current path an "under" path for the current cell (x,y)?
46
+ is_under = (try & Maze::UNDER != 0)
47
+
48
+ dir = is_under ? (try >> Maze::UNDER_SHIFT) : try
49
+ opposite = @maze.opposite(dir)
50
+
51
+ nx, ny = @maze.move(x, y, dir)
52
+
53
+ # is the new path an "under" path for the next cell (nx,ny)?
54
+ going_under = @maze[nx, ny] & (opposite << Maze::UNDER_SHIFT) != 0
55
+
56
+ # might be out of bounds, due to the entrance/exit passages
57
+ next if !@maze.valid?(nx, ny) || (@visits[ny][nx] & VISIT_MASK[going_under] != 0)
58
+
59
+ @visits[ny][nx] |= VISIT_MASK[going_under]
60
+ ncell = @maze[nx, ny]
61
+ p = [nx, ny]
62
+
63
+ if ncell & (opposite << Maze::UNDER_SHIFT) != 0 # underpass
64
+ unders = (ncell & Maze::UNDER) >> Maze::UNDER_SHIFT
65
+ exit_dir = unders & ~opposite
66
+ directions = [exit_dir << Maze::UNDER_SHIFT]
67
+ else
68
+ directions = @maze.potential_exits_at(nx, ny) - [@maze.opposite(dir)]
69
+ end
70
+
71
+ @stack.push([p, directions])
72
+ return p.dup
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,95 @@
1
+ require 'theseus/maze'
2
+
3
+ module Theseus
4
+ module Solvers
5
+ # The abstract superclass for solver implementations. It simply provides
6
+ # some helper methods that implementations would otherwise have to duplicate.
7
+ class Base
8
+ # The maze object that this solver will provide a solution for.
9
+ attr_reader :maze
10
+
11
+ # The point (2-tuple array) at which the solution path should begin.
12
+ attr_reader :a
13
+
14
+ # The point (2-tuple array) at which the solution path should end.
15
+ attr_reader :b
16
+
17
+ # Create a new solver instance for the given maze, using the given
18
+ # start (+a+) and finish (+b+) points. The solution will not be immediately
19
+ # generated; to do so, use the #step or #solve methods.
20
+ def initialize(maze, a=maze.start, b=maze.finish)
21
+ @maze = maze
22
+ @a = a
23
+ @b = b
24
+ @solution = nil
25
+ end
26
+
27
+ # Returns +true+ if the solution has been generated.
28
+ def solved?
29
+ @solution != nil
30
+ end
31
+
32
+ # Returns the solution path as an array of 2-tuples, beginning with #a and
33
+ # ending with #b. If the solution has not yet been generated, this will
34
+ # generate the solution first, and then return it.
35
+ def solution
36
+ solve unless solved?
37
+ @solution
38
+ end
39
+
40
+ # Generates the solution to the maze, and returns +self+. If the solution
41
+ # has already been generated, this does nothing.
42
+ def solve
43
+ while !solved?
44
+ step
45
+ end
46
+
47
+ self
48
+ end
49
+
50
+ # If the maze is solved, this yields each point in the solution, in order.
51
+ #
52
+ # If the maze has not yet been solved, this yields the result of calling
53
+ # #step, until the maze has been solved.
54
+ def each
55
+ if solved?
56
+ solution.each { |s| yield s }
57
+ else
58
+ yield s while s = step
59
+ end
60
+ end
61
+
62
+ # Returns the solution (or, if the solution is not yet fully generated,
63
+ # the current_solution) as a Theseus::Path object.
64
+ def to_path(options={})
65
+ path = @maze.new_path(options)
66
+ prev = @maze.entrance
67
+
68
+ (@solution || current_solution).each do |pt|
69
+ how = path.link(prev, pt)
70
+ path.set(pt, how)
71
+ prev = pt
72
+ end
73
+
74
+ how = path.link(prev, @maze.exit)
75
+ path.set(@maze.exit, how)
76
+
77
+ path
78
+ end
79
+
80
+ # Returns the current (potentially partial) solution to the maze. This
81
+ # is for use while the algorithm is running, so that the current best-solution
82
+ # may be inspected (or displayed).
83
+ def current_solution
84
+ raise NotImplementedError, "solver subclasses must implement #current_solution"
85
+ end
86
+
87
+ # Runs a single iteration of the solution algorithm. Returns +false+ if the
88
+ # algorithm has completed, and non-nil otherwise. The return value is
89
+ # algorithm-dependent.
90
+ def step
91
+ raise NotImplementedError, "solver subclasses must implement #step"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,37 @@
1
+ require 'theseus/maze'
2
+
3
+ module Theseus
4
+ # An upsilon maze is one in which the field is tesselated into octogons and
5
+ # squares:
6
+ #
7
+ # _ _ _ _
8
+ # / \_/ \_/ \_/ \
9
+ # | |_| |_| |_| |
10
+ # \_/ \_/ \_/ \_/
11
+ # |_| |_| |_| |_|
12
+ # / \_/ \_/ \_/ \
13
+ # | |_| |_| |_| |
14
+ # \_/ \_/ \_/ \_/
15
+ #
16
+ # Upsilon mazes in Theseus support weaving, but not symmetry (yet).
17
+ #
18
+ # maze = Theseus::UpsilonMaze.generate(width: 10)
19
+ # puts maze
20
+ class UpsilonMaze < Maze
21
+ def potential_exits_at(x, y) #:nodoc:
22
+ if (x+y) % 2 == 0 # octogon
23
+ [N, S, E, W, NW, NE, SW, SE]
24
+ else # square
25
+ [N, S, E, W]
26
+ end
27
+ end
28
+
29
+ def perform_weave(from_x, from_y, to_x, to_y, direction) #:nodoc:
30
+ apply_move_at(to_x, to_y, direction << UNDER_SHIFT)
31
+ apply_move_at(to_x, to_y, opposite(direction) << UNDER_SHIFT)
32
+
33
+ nx, ny = move(to_x, to_y, direction)
34
+ [nx, ny, direction]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,10 @@
1
+ module Theseus
2
+ # The current version of the Theseus library.
3
+ module Version
4
+ MAJOR = 1
5
+ MINOR = 0
6
+ TINY = 0
7
+
8
+ STRING = [MAJOR, MINOR, TINY].join(".")
9
+ end
10
+ end
data/test/maze_test.rb ADDED
@@ -0,0 +1,193 @@
1
+ require 'minitest/autorun'
2
+ require 'theseus'
3
+
4
+ class MazeTest < MiniTest::Unit::TestCase
5
+ def test_maze_without_explicit_height_uses_width
6
+ maze = Theseus::OrthogonalMaze.new(width: 10)
7
+ assert_equal 10, maze.width
8
+ assert_equal maze.width, maze.height
9
+ end
10
+
11
+ def test_maze_without_explicit_width_uses_height
12
+ maze = Theseus::OrthogonalMaze.new(height: 10)
13
+ assert_equal 10, maze.height
14
+ assert_equal maze.height, maze.width
15
+ end
16
+
17
+ def test_maze_is_initially_blank
18
+ maze = Theseus::OrthogonalMaze.new(width: 10)
19
+ assert !maze.generated?
20
+
21
+ zeros = 0
22
+ maze.height.times do |y|
23
+ maze.width.times do |x|
24
+ zeros += 1if maze[x, y] == 0
25
+ end
26
+ end
27
+
28
+ assert_equal 100, zeros
29
+ end
30
+
31
+ def test_maze_created_with_generate_is_identical_to_maze_created_with_step
32
+ srand(14)
33
+ maze1 = Theseus::OrthogonalMaze.generate(width: 10)
34
+ assert maze1.generated?
35
+
36
+ srand(14)
37
+ maze2 = Theseus::OrthogonalMaze.new(width: 10)
38
+ maze2.step until maze2.generated?
39
+
40
+ assert_equal maze1.width, maze2.width
41
+ assert_equal maze1.height, maze2.height
42
+
43
+ differences = 0
44
+
45
+ maze1.width.times do |x|
46
+ maze1.height.times do |y|
47
+ differences += 1 unless maze1[x,y] == maze2[x,y]
48
+ end
49
+ end
50
+
51
+ assert_equal 0, differences
52
+ end
53
+
54
+ def test_apply_move_at_should_combine_direction_with_existing_directions
55
+ maze = Theseus::OrthogonalMaze.new(width: 10)
56
+
57
+ maze[5,5] = Theseus::Maze::E
58
+ maze.apply_move_at(5, 5, Theseus::Maze::N)
59
+ assert_equal (Theseus::Maze::N | Theseus::Maze::E), maze[5,5]
60
+ end
61
+
62
+ def test_apply_move_at_with_under_should_move_existing_directions_to_under_plane
63
+ maze = Theseus::OrthogonalMaze.new(width: 10)
64
+
65
+ maze[5,5] = Theseus::Maze::E
66
+ maze.apply_move_at(5, 5, :under)
67
+ assert_equal (Theseus::Maze::E << Theseus::Maze::UNDER_SHIFT), maze[5,5]
68
+ end
69
+
70
+ def test_apply_move_at_with_x_symmetry_should_populate_x_mirror
71
+ maze = Theseus::OrthogonalMaze.new(width: 10, symmetry: :x)
72
+
73
+ maze.apply_move_at(1, 2, Theseus::Maze::E)
74
+ assert_equal Theseus::Maze::W, maze[8, 2]
75
+
76
+ maze.apply_move_at(2, 1, Theseus::Maze::NE)
77
+ assert_equal Theseus::Maze::NW, maze[7, 1]
78
+
79
+ maze.apply_move_at(2, 3, Theseus::Maze::N)
80
+ assert_equal Theseus::Maze::N, maze[7, 3]
81
+ end
82
+
83
+ def test_apply_move_at_with_y_symmetry_should_populate_y_mirror
84
+ maze = Theseus::OrthogonalMaze.new(width: 10, symmetry: :y)
85
+
86
+ maze.apply_move_at(1, 2, Theseus::Maze::S)
87
+ assert_equal Theseus::Maze::N, maze[1, 7]
88
+
89
+ maze.apply_move_at(2, 1, Theseus::Maze::SW)
90
+ assert_equal Theseus::Maze::NW, maze[2, 8]
91
+
92
+ maze.apply_move_at(2, 3, Theseus::Maze::W)
93
+ assert_equal Theseus::Maze::W, maze[2, 6]
94
+ end
95
+
96
+ def test_apply_move_at_with_xy_symmetry_should_populate_xy_mirror
97
+ maze = Theseus::OrthogonalMaze.new(width: 10, symmetry: :xy)
98
+
99
+ maze.apply_move_at(1, 2, Theseus::Maze::S)
100
+ assert_equal Theseus::Maze::N, maze[1, 7]
101
+ assert_equal Theseus::Maze::S, maze[8, 2]
102
+ assert_equal Theseus::Maze::N, maze[8, 7]
103
+
104
+ maze.apply_move_at(2, 1, Theseus::Maze::SW)
105
+ assert_equal Theseus::Maze::NW, maze[2, 8]
106
+ assert_equal Theseus::Maze::SE, maze[7, 1]
107
+ assert_equal Theseus::Maze::NE, maze[7, 8]
108
+
109
+ maze.apply_move_at(2, 3, Theseus::Maze::W)
110
+ assert_equal Theseus::Maze::W, maze[2, 6]
111
+ assert_equal Theseus::Maze::E, maze[7, 3]
112
+ assert_equal Theseus::Maze::E, maze[7, 6]
113
+ end
114
+
115
+ def test_apply_move_at_with_radial_symmetry_should_populate_radial_mirror
116
+ maze = Theseus::OrthogonalMaze.new(width: 10, symmetry: :radial)
117
+
118
+ maze.apply_move_at(1, 2, Theseus::Maze::S)
119
+ assert_equal Theseus::Maze::E, maze[2, 8]
120
+ assert_equal Theseus::Maze::W, maze[7, 1]
121
+ assert_equal Theseus::Maze::N, maze[8, 7]
122
+
123
+ maze.apply_move_at(2, 1, Theseus::Maze::SW)
124
+ assert_equal Theseus::Maze::SE, maze[1, 7]
125
+ assert_equal Theseus::Maze::NW, maze[8, 2]
126
+ assert_equal Theseus::Maze::NE, maze[7, 8]
127
+
128
+ maze.apply_move_at(2, 3, Theseus::Maze::W)
129
+ assert_equal Theseus::Maze::S, maze[3, 7]
130
+ assert_equal Theseus::Maze::N, maze[6, 2]
131
+ assert_equal Theseus::Maze::E, maze[7, 6]
132
+ end
133
+
134
+ def test_dx_east_should_increase
135
+ maze = Theseus::OrthogonalMaze.new(width: 10)
136
+ assert_equal 1, maze.dx(Theseus::Maze::E)
137
+ assert_equal 1, maze.dx(Theseus::Maze::NE)
138
+ assert_equal 1, maze.dx(Theseus::Maze::SE)
139
+ end
140
+
141
+ def test_dx_west_should_decrease
142
+ maze = Theseus::OrthogonalMaze.new(width: 10)
143
+ assert_equal -1, maze.dx(Theseus::Maze::W)
144
+ assert_equal -1, maze.dx(Theseus::Maze::NW)
145
+ assert_equal -1, maze.dx(Theseus::Maze::SW)
146
+ end
147
+
148
+ def test_dy_south_should_increase
149
+ maze = Theseus::OrthogonalMaze.new(width: 10)
150
+ assert_equal 1, maze.dy(Theseus::Maze::S)
151
+ assert_equal 1, maze.dy(Theseus::Maze::SE)
152
+ assert_equal 1, maze.dy(Theseus::Maze::SW)
153
+ end
154
+
155
+ def test_dy_north_should_decrease
156
+ maze = Theseus::OrthogonalMaze.new(width: 10)
157
+ assert_equal -1, maze.dy(Theseus::Maze::N)
158
+ assert_equal -1, maze.dy(Theseus::Maze::NE)
159
+ assert_equal -1, maze.dy(Theseus::Maze::NW)
160
+ end
161
+
162
+ def test_opposite_should_report_inverse_direction
163
+ maze = Theseus::OrthogonalMaze.new(width: 10)
164
+ assert_equal Theseus::Maze::N, maze.opposite(Theseus::Maze::S)
165
+ assert_equal Theseus::Maze::NE, maze.opposite(Theseus::Maze::SW)
166
+ assert_equal Theseus::Maze::E, maze.opposite(Theseus::Maze::W)
167
+ assert_equal Theseus::Maze::SE, maze.opposite(Theseus::Maze::NW)
168
+ assert_equal Theseus::Maze::S, maze.opposite(Theseus::Maze::N)
169
+ assert_equal Theseus::Maze::SW, maze.opposite(Theseus::Maze::NE)
170
+ assert_equal Theseus::Maze::W, maze.opposite(Theseus::Maze::E)
171
+ assert_equal Theseus::Maze::NW, maze.opposite(Theseus::Maze::SE)
172
+ end
173
+
174
+ def test_step_should_populate_current_cell_and_next_cell
175
+ maze = Theseus::OrthogonalMaze.new(width: 10)
176
+
177
+ cx, cy = maze.x, maze.y
178
+ assert cx >= 0 && cx < maze.width
179
+ assert cy >= 0 && cy < maze.height
180
+ assert_equal 0, maze[cx, cy]
181
+
182
+ assert maze.step
183
+
184
+ direction = maze[cx, cy]
185
+ refute_equal 0, direction
186
+
187
+ nx, ny = maze.move(cx, cy, direction)
188
+ refute_equal [nx, ny], [cx, cy]
189
+ assert_equal [nx, ny], [maze.x, maze.y]
190
+
191
+ assert_equal maze.opposite(direction), maze[nx, ny]
192
+ end
193
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: theseus
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Jamis Buck
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-12-19 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: chunky_png
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 12
31
+ - 0
32
+ version: 0.12.0
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: Theseus is a library for building random mazes.
36
+ email: jamis@jamisbuck.org
37
+ executables:
38
+ - theseus
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - README.rdoc
45
+ - Rakefile
46
+ - lib/theseus/delta_maze.rb
47
+ - lib/theseus/formatters/ascii/delta.rb
48
+ - lib/theseus/formatters/ascii/orthogonal.rb
49
+ - lib/theseus/formatters/ascii/sigma.rb
50
+ - lib/theseus/formatters/ascii/upsilon.rb
51
+ - lib/theseus/formatters/ascii.rb
52
+ - lib/theseus/formatters/png/delta.rb
53
+ - lib/theseus/formatters/png/orthogonal.rb
54
+ - lib/theseus/formatters/png/sigma.rb
55
+ - lib/theseus/formatters/png/upsilon.rb
56
+ - lib/theseus/formatters/png.rb
57
+ - lib/theseus/mask.rb
58
+ - lib/theseus/maze.rb
59
+ - lib/theseus/orthogonal_maze.rb
60
+ - lib/theseus/path.rb
61
+ - lib/theseus/sigma_maze.rb
62
+ - lib/theseus/solvers/astar.rb
63
+ - lib/theseus/solvers/backtracker.rb
64
+ - lib/theseus/solvers/base.rb
65
+ - lib/theseus/upsilon_maze.rb
66
+ - lib/theseus/version.rb
67
+ - lib/theseus.rb
68
+ - examples/a-star-search.rb
69
+ - bin/theseus
70
+ - test/maze_test.rb
71
+ has_rdoc: true
72
+ homepage: http://github.com/jamis/theseus
73
+ licenses: []
74
+
75
+ post_install_message:
76
+ rdoc_options: []
77
+
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ requirements:
97
+ - Ruby 1.9
98
+ rubyforge_project:
99
+ rubygems_version: 1.3.7
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Maze generator for Ruby
103
+ test_files: []
104
+