theseus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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