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
data/lib/theseus.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'theseus/maze'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
# A "delta" maze is one in which the field is tesselated into triangles. Thus,
|
5
|
+
# each cell has three potential exits: east, west, and either north or south
|
6
|
+
# (depending on the orientation of the cell).
|
7
|
+
#
|
8
|
+
# __ __ __
|
9
|
+
# /\ /\ /\ /
|
10
|
+
# /__\/__\/__\/
|
11
|
+
# \ /\ /\ /\
|
12
|
+
# \/__\/__\/__\
|
13
|
+
# /\ /\ /\ /
|
14
|
+
# /__\/__\/__\/
|
15
|
+
# \ /\ /\ /\
|
16
|
+
# \/__\/__\/__\
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# Delta mazes in Theseus do not support either weaving, or symmetry.
|
20
|
+
#
|
21
|
+
# maze = Theseus::DeltaMaze.generate(width: 10)
|
22
|
+
# puts maze
|
23
|
+
class DeltaMaze < Maze
|
24
|
+
def initialize(options={}) #:nodoc:
|
25
|
+
super
|
26
|
+
raise ArgumentError, "weaving is not supported for delta mazes" if @weave > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns +true+ if the cell at (x,y) is oriented so the vertex is "up", or
|
30
|
+
# north. Cells for which this returns +true+ may have exits on the south border,
|
31
|
+
# and cells for which it returns +false+ may have exits on the north.
|
32
|
+
def points_up?(x, y)
|
33
|
+
(x + y) % 2 == height % 2
|
34
|
+
end
|
35
|
+
|
36
|
+
def potential_exits_at(x, y) #:nodoc:
|
37
|
+
vertical = points_up?(x, y) ? S : N
|
38
|
+
|
39
|
+
# list the vertical direction twice. Otherwise the horizontal direction (E/W)
|
40
|
+
# will be selected more often (66% of the time), resulting in mazes with a
|
41
|
+
# horizontal bias.
|
42
|
+
[vertical, vertical, E, W]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Theseus
|
2
|
+
module Formatters
|
3
|
+
# ASCII formatters render a maze as ASCII art. The ASCII representation
|
4
|
+
# is intended mostly to give you a "quick look" at the maze, and will
|
5
|
+
# rarely suffice for showing more than an overview of the maze's shape.
|
6
|
+
#
|
7
|
+
# This is the abstract superclass of the ASCII formatters, and provides
|
8
|
+
# helpers for writing to a textual "canvas".
|
9
|
+
class ASCII
|
10
|
+
# The width of the canvas. This corresponds to, but is not necessarily the
|
11
|
+
# same as, the width of the maze.
|
12
|
+
attr_reader :width
|
13
|
+
|
14
|
+
# The height of the canvas. This corresponds to, but is not necessarily the
|
15
|
+
# same as, the height of the maze.
|
16
|
+
attr_reader :height
|
17
|
+
|
18
|
+
# Create a new ASCII canvas with the given width and height. The canvas is
|
19
|
+
# initially blank (set to whitespace).
|
20
|
+
def initialize(width, height)
|
21
|
+
@width, @height = width, height
|
22
|
+
@chars = Array.new(height) { Array.new(width, " ") }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the character at the given coordinates.
|
26
|
+
def [](x, y)
|
27
|
+
@chars[y][x]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Sets the character at the given coordinates.
|
31
|
+
def []=(x, y, char)
|
32
|
+
@chars[y][x] = char
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the canvas as a multiline string, suitable for displaying.
|
36
|
+
def to_s
|
37
|
+
@chars.map { |row| row.join }.join("\n")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'theseus/formatters/ascii'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
module Formatters
|
5
|
+
class ASCII
|
6
|
+
# Renders a DeltaMaze to an ASCII representation, using 4 characters
|
7
|
+
# horizontally and 2 characters vertically to represent a single cell.
|
8
|
+
#
|
9
|
+
# __
|
10
|
+
# /\ /
|
11
|
+
# /__\/
|
12
|
+
# /\ /\
|
13
|
+
# /__\/__\
|
14
|
+
# /\ /\ /\
|
15
|
+
# /__\/__\/__\
|
16
|
+
#
|
17
|
+
# You shouldn't ever need to instantiate this class directly. Rather, use
|
18
|
+
# DeltaMaze#to(:ascii) (or DeltaMaze#to_s to get the string directly).
|
19
|
+
class Delta < ASCII
|
20
|
+
# Returns a new Delta canvas for the given maze (which should be an
|
21
|
+
# instance of DeltaMaze). The +options+ parameter is not used.
|
22
|
+
#
|
23
|
+
# The returned object will be fully initialized, containing an ASCII
|
24
|
+
# representation of the given DeltaMaze.
|
25
|
+
def initialize(maze, options={})
|
26
|
+
super((maze.width + 1) * 2, maze.height * 2 + 1)
|
27
|
+
|
28
|
+
maze.height.times do |y|
|
29
|
+
py = y * 2
|
30
|
+
maze.row_length(y).times do |x|
|
31
|
+
cell = maze[x, y]
|
32
|
+
next if cell == 0
|
33
|
+
|
34
|
+
px = x * 2
|
35
|
+
|
36
|
+
if maze.points_up?(x, y)
|
37
|
+
if cell & Maze::W == 0
|
38
|
+
self[px+1,py+1] = "/"
|
39
|
+
self[px,py+2] = "/"
|
40
|
+
elsif y < 1
|
41
|
+
self[px+1,py] = "_"
|
42
|
+
end
|
43
|
+
|
44
|
+
if cell & Maze::E == 0
|
45
|
+
self[px+2,py+1] = "\\"
|
46
|
+
self[px+3,py+2] = "\\"
|
47
|
+
elsif y < 1
|
48
|
+
self[px+2,py] = "_"
|
49
|
+
end
|
50
|
+
|
51
|
+
if cell & Maze::S == 0
|
52
|
+
self[px+1,py+2] = self[px+2,py+2] = "_"
|
53
|
+
end
|
54
|
+
else
|
55
|
+
if cell & Maze::W == 0
|
56
|
+
self[px,py+1] = "\\"
|
57
|
+
self[px+1,py+2] = "\\"
|
58
|
+
elsif x > 0 && maze[x-1,y] & Maze::S == 0
|
59
|
+
self[px+1,py+2] = "_"
|
60
|
+
end
|
61
|
+
|
62
|
+
if cell & Maze::E == 0
|
63
|
+
self[px+3,py+1] = "/"
|
64
|
+
self[px+2,py+2] = "/"
|
65
|
+
elsif x < maze.row_length(y) && maze[x+1,y] & Maze::S == 0
|
66
|
+
self[px+2,py+2] = "_"
|
67
|
+
end
|
68
|
+
|
69
|
+
if cell & Maze::N == 0
|
70
|
+
self[px+1,py] = self[px+2,py] = "_"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'theseus/formatters/ascii'
|
4
|
+
|
5
|
+
module Theseus
|
6
|
+
module Formatters
|
7
|
+
class ASCII
|
8
|
+
# Renders an OrthogonalMaze to an ASCII representation.
|
9
|
+
#
|
10
|
+
# The ASCII formatter for the OrthogonalMaze actually supports three different
|
11
|
+
# output types:
|
12
|
+
#
|
13
|
+
# [:plain] Uses standard 7-bit ASCII characters. Width is 2x+1, height is
|
14
|
+
# y+1. This mode cannot render weave mazes without significant
|
15
|
+
# ambiguity.
|
16
|
+
# [:unicode] Uses unicode characters to render cleaner lines. Width is
|
17
|
+
# 3x, height is 2y. This mode has sufficient detail to correctly
|
18
|
+
# render mazes with weave!
|
19
|
+
# [:lines] Draws passages as lines, using unicode characters. Width is
|
20
|
+
# x, height is y. This mode can render weave mazes, but with some
|
21
|
+
# ambiguity.
|
22
|
+
#
|
23
|
+
# The :plain mode is the default, but you can specify a different one using
|
24
|
+
# the :mode option.
|
25
|
+
#
|
26
|
+
# You shouldn't ever need to instantiate this class directly. Rather, use
|
27
|
+
# OrthogonalMaze#to(:ascii) (or OrthogonalMaze#to_s to get the string directly).
|
28
|
+
class Orthogonal < ASCII
|
29
|
+
# Returns the dimensions of the given maze, rendered in the given mode.
|
30
|
+
# The +mode+ must be +:plain+, +:unicode+, or +:lines+.
|
31
|
+
def self.dimensions_for(maze, mode)
|
32
|
+
case mode
|
33
|
+
when :plain, nil then
|
34
|
+
[maze.width * 2 + 1, maze.height + 1]
|
35
|
+
when :unicode then
|
36
|
+
[maze.width * 3, maze.height * 2]
|
37
|
+
when :lines then
|
38
|
+
[maze.width, maze.height]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create and return a fully initialized ASCII canvas. The +options+
|
43
|
+
# parameter may specify a +:mode+ parameter, as described in the documentation
|
44
|
+
# for this class.
|
45
|
+
def initialize(maze, options={})
|
46
|
+
mode = options[:mode] || :plain
|
47
|
+
|
48
|
+
width, height = self.class.dimensions_for(maze, mode)
|
49
|
+
super(width, height)
|
50
|
+
|
51
|
+
maze.height.times do |y|
|
52
|
+
length = maze.row_length(y)
|
53
|
+
length.times do |x|
|
54
|
+
case mode
|
55
|
+
when :plain then draw_plain_cell(maze, x, y)
|
56
|
+
when :unicode then draw_unicode_cell(maze, x, y)
|
57
|
+
when :lines then draw_line_cell(maze, x, y)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def draw_plain_cell(maze, x, y) #:nodoc:
|
66
|
+
c = maze[x, y]
|
67
|
+
return if c == 0
|
68
|
+
|
69
|
+
px, py = x * 2, y
|
70
|
+
|
71
|
+
cnw = maze.valid?(x-1,y-1) ? maze[x-1,y-1] : 0
|
72
|
+
cn = maze.valid?(x,y-1) ? maze[x,y-1] : 0
|
73
|
+
cne = maze.valid?(x+1,y-1) ? maze[x+1,y-1] : 0
|
74
|
+
cse = maze.valid?(x+1,y+1) ? maze[x+1,y+1] : 0
|
75
|
+
cs = maze.valid?(x,y+1) ? maze[x,y+1] : 0
|
76
|
+
csw = maze.valid?(x-1,y+1) ? maze[x-1,y+1] : 0
|
77
|
+
|
78
|
+
if c & Maze::N == 0
|
79
|
+
self[px, py] = "_" if y == 0 || (cn == 0 && cnw == 0) || cnw & (Maze::E | Maze::S) == Maze::E
|
80
|
+
self[px+1, py] = "_"
|
81
|
+
self[px+2, py] = "_" if y == 0 || (cn == 0 && cne == 0) || cne & (Maze::W | Maze::S) == Maze::W
|
82
|
+
end
|
83
|
+
|
84
|
+
if c & Maze::S == 0
|
85
|
+
bottom = y+1 == maze.height
|
86
|
+
self[px, py+1] = "_" if bottom || (cs == 0 && csw == 0) || csw & (Maze::E | Maze::N) == Maze::E
|
87
|
+
self[px+1, py+1] = "_"
|
88
|
+
self[px+2, py+1] = "_" if bottom || (cs == 0 && cse == 0) || cse & (Maze::W | Maze::N) == Maze::W
|
89
|
+
end
|
90
|
+
|
91
|
+
self[px, py+1] = "|" if c & Maze::W == 0
|
92
|
+
self[px+2, py+1] = "|" if c & Maze::E == 0
|
93
|
+
end
|
94
|
+
|
95
|
+
UTF8_SPRITES = [
|
96
|
+
[" ", " "], # " "
|
97
|
+
["│ │", "└─┘"], # "╵"
|
98
|
+
["┌─┐", "│ │"], # "╷"
|
99
|
+
["│ │", "│ │"], # "│",
|
100
|
+
["┌──", "└──"], # "╶"
|
101
|
+
["│ └", "└──"], # "└"
|
102
|
+
["┌──", "│ ┌"], # "┌"
|
103
|
+
["│ └", "│ ┌"], # "├"
|
104
|
+
["──┐", "──┘"], # "╴"
|
105
|
+
["┘ │", "──┘"], # "┘"
|
106
|
+
["──┐", "┐ │"], # "┐"
|
107
|
+
["┘ │", "┐ │"], # "┤"
|
108
|
+
["───", "───"], # "─"
|
109
|
+
["┘ └", "───"], # "┴"
|
110
|
+
["───", "┐ ┌"], # "┬"
|
111
|
+
["┘ └", "┐ ┌"] # "┼"
|
112
|
+
]
|
113
|
+
|
114
|
+
def draw_unicode_cell(maze, x, y) #:nodoc:
|
115
|
+
cx, cy = 3 * x, 2 * y
|
116
|
+
cell = maze[x, y]
|
117
|
+
|
118
|
+
UTF8_SPRITES[cell & Maze::PRIMARY].each_with_index do |row, sy|
|
119
|
+
row.length.times do |sx|
|
120
|
+
char = row[sx]
|
121
|
+
self[cx+sx, cy+sy] = char
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
under = cell >> Maze::UNDER_SHIFT
|
126
|
+
|
127
|
+
if under & Maze::N != 0
|
128
|
+
self[cx, cy] = "┴"
|
129
|
+
self[cx+2, cy] = "┴"
|
130
|
+
end
|
131
|
+
|
132
|
+
if under & Maze::S != 0
|
133
|
+
self[cx, cy+1] = "┬"
|
134
|
+
self[cx+2, cy+1] = "┬"
|
135
|
+
end
|
136
|
+
|
137
|
+
if under & Maze::W != 0
|
138
|
+
self[cx, cy] = "┤"
|
139
|
+
self[cx, cy+1] = "┤"
|
140
|
+
end
|
141
|
+
|
142
|
+
if under & Maze::E != 0
|
143
|
+
self[cx+2, cy] = "├"
|
144
|
+
self[cx+2, cy+1] = "├"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
UTF8_LINES = [" ", "╵", "╷", "│", "╶", "└", "┌", "├", "╴", "┘", "┐", "┤", "─", "┴", "┬", "┼"]
|
149
|
+
|
150
|
+
def draw_line_cell(maze, x, y) #:nodoc:
|
151
|
+
self[x, y] = UTF8_LINES[maze[x, y] & Maze::PRIMARY]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'theseus/formatters/ascii'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
module Formatters
|
5
|
+
class ASCII
|
6
|
+
# Renders a SigmaMaze to an ASCII representation, using 3 characters
|
7
|
+
# horizontally and 3 characters vertically to represent a single cell.
|
8
|
+
# _ _ _
|
9
|
+
# / \_/ \_/ \_
|
10
|
+
# \_/ \_/ \_/ \
|
11
|
+
# / \_/ \_/ \_/
|
12
|
+
# \_/ \_/ \_/ \
|
13
|
+
# / \_/ \_/ \_/
|
14
|
+
# \_/ \_/ \_/ \
|
15
|
+
# / \_/ \_/ \_/
|
16
|
+
# \_/ \_/ \_/ \
|
17
|
+
#
|
18
|
+
# You shouldn't ever need to instantiate this class directly. Rather, use
|
19
|
+
# SigmaMaze#to(:ascii) (or SigmaMaze#to_s to get the string directly).
|
20
|
+
class Sigma < ASCII
|
21
|
+
# Returns a new Sigma canvas for the given maze (which should be an
|
22
|
+
# instance of SigmaMaze). The +options+ parameter is not used.
|
23
|
+
#
|
24
|
+
# The returned object will be fully initialized, containing an ASCII
|
25
|
+
# representation of the given SigmaMaze.
|
26
|
+
def initialize(maze, options={})
|
27
|
+
super(maze.width * 2 + 2, maze.height * 2 + 2)
|
28
|
+
|
29
|
+
maze.height.times do |y|
|
30
|
+
py = y * 2
|
31
|
+
maze.row_length(y).times do |x|
|
32
|
+
cell = maze[x, y]
|
33
|
+
next if cell == 0
|
34
|
+
|
35
|
+
px = x * 2
|
36
|
+
|
37
|
+
shifted = x % 2 != 0
|
38
|
+
ry = shifted ? py+1 : py
|
39
|
+
|
40
|
+
nw = shifted ? Maze::W : Maze::NW
|
41
|
+
ne = shifted ? Maze::E : Maze::NE
|
42
|
+
sw = shifted ? Maze::SW : Maze::W
|
43
|
+
se = shifted ? Maze::SE : Maze::E
|
44
|
+
|
45
|
+
self[px+1,ry] = "_" if cell & Maze::N == 0
|
46
|
+
self[px,ry+1] = "/" if cell & nw == 0
|
47
|
+
self[px+2,ry+1] = "\\" if cell & ne == 0
|
48
|
+
self[px,ry+2] = "\\" if cell & sw == 0
|
49
|
+
self[px+1,ry+2] = "_" if cell & Maze::S == 0
|
50
|
+
self[px+2,ry+2] = "/" if cell & se == 0
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'theseus/formatters/ascii'
|
2
|
+
|
3
|
+
module Theseus
|
4
|
+
module Formatters
|
5
|
+
class ASCII
|
6
|
+
# Renders an UpsilonMaze to an ASCII representation, using 3 characters
|
7
|
+
# horizontally and 4 characters vertically to represent a single octagonal
|
8
|
+
# cell, and 3 characters horizontally and 2 vertically to represent a square
|
9
|
+
# cell.
|
10
|
+
# _ _ _
|
11
|
+
# / \_/ \_/ \
|
12
|
+
# | |_| |_| |
|
13
|
+
# \_/ \_/ \_/
|
14
|
+
# |_| |_| |_|
|
15
|
+
# / \_/ \_/ \
|
16
|
+
#
|
17
|
+
# You shouldn't ever need to instantiate this class directly. Rather, use
|
18
|
+
# UpsilonMaze#to(:ascii) (or UpsilonMaze#to_s to get the string directly).
|
19
|
+
class Upsilon < ASCII
|
20
|
+
# Returns a new Sigma canvas for the given maze (which should be an
|
21
|
+
# instance of SigmaMaze). The +options+ parameter is not used.
|
22
|
+
#
|
23
|
+
# The returned object will be fully initialized, containing an ASCII
|
24
|
+
# representation of the given SigmaMaze.
|
25
|
+
def initialize(maze, options={})
|
26
|
+
super(maze.width * 2 + 1, maze.height * 2 + 3)
|
27
|
+
|
28
|
+
maze.height.times do |y|
|
29
|
+
py = y * 2
|
30
|
+
maze.row_length(y).times do |x|
|
31
|
+
cell = maze[x, y]
|
32
|
+
next if cell == 0
|
33
|
+
|
34
|
+
px = x * 2
|
35
|
+
|
36
|
+
if (x + y) % 2 == 0
|
37
|
+
draw_octogon_cell(px, py, cell)
|
38
|
+
else
|
39
|
+
draw_square_cell(px, py, cell)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def draw_octogon_cell(px, py, cell) #:nodoc:
|
48
|
+
self[px+1, py] = "_" if cell & Maze::N == 0
|
49
|
+
self[px, py+1] = "/" if cell & Maze::NW == 0
|
50
|
+
self[px+2, py+1] = "\\" if cell & Maze::NE == 0
|
51
|
+
self[px, py+2] = "|" if cell & Maze::W == 0
|
52
|
+
self[px+2, py+2] = "|" if cell & Maze::E == 0
|
53
|
+
self[px, py+3] = "\\" if cell & Maze::SW == 0
|
54
|
+
self[px+1, py+3] = "_" if cell & Maze::S == 0
|
55
|
+
self[px+2, py+3] = "/" if cell & Maze::SE == 0
|
56
|
+
end
|
57
|
+
|
58
|
+
def draw_square_cell(px, py, cell) #:nodoc:
|
59
|
+
self[px+1, py+1] = "_" if cell & Maze::N == 0
|
60
|
+
self[px, py+2] = "|" if cell & Maze::W == 0
|
61
|
+
self[px+1, py+2] = "_" if cell & Maze::S == 0
|
62
|
+
self[px+2, py+2] = "|" if cell & Maze::E == 0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|