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/lib/theseus.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'theseus/maze'
2
+
3
+ require 'theseus/delta_maze'
4
+ require 'theseus/orthogonal_maze'
5
+ require 'theseus/sigma_maze'
6
+ require 'theseus/upsilon_maze'
@@ -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