theseus 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|