maze 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/.gitignore +3 -0
- data/.rvmrc.example +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +18 -0
- data/README.md +55 -0
- data/examples/maze_helper.rb +27 -0
- data/examples/orthogonal.rb +3 -0
- data/examples/sigma.rb +3 -0
- data/lib/maze.rb +8 -0
- data/lib/maze/algorithms/recursive_backtracker.rb +46 -0
- data/lib/maze/formatters/ascii.rb +47 -0
- data/lib/maze/formatters/ascii/orthogonal.rb +51 -0
- data/lib/maze/formatters/ascii/sigma.rb +56 -0
- data/lib/maze/formatters/png.rb +36 -0
- data/lib/maze/formatters/png/orthogonal.rb +127 -0
- data/lib/maze/formatters/png/sigma.rb +137 -0
- data/lib/maze/generic.rb +63 -0
- data/lib/maze/orthogonal.rb +28 -0
- data/lib/maze/point.rb +39 -0
- data/lib/maze/sigma.rb +92 -0
- data/lib/maze/version.rb +3 -0
- data/maze.gemspec +21 -0
- metadata +80 -0
data/.gitignore
ADDED
data/.rvmrc.example
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.2
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
## Overview
|
2
|
+
|
3
|
+
This is a library for generating perfect mazes. The default tessellation is orthogonal. It supports ASCII output.
|
4
|
+
|
5
|
+
## Tessellations
|
6
|
+
|
7
|
+
+ Orthogonal - the standard rectangular grid with passages intersecting at right angles
|
8
|
+
+ Sigma - composed of interlocking hexagons
|
9
|
+
|
10
|
+
## Supported Output Formats
|
11
|
+
|
12
|
+
+ ASCII
|
13
|
+
+ PNG
|
14
|
+
|
15
|
+
## Algorithms
|
16
|
+
|
17
|
+
+ Recursive Backtracking
|
18
|
+
|
19
|
+
## Examples
|
20
|
+
|
21
|
+
Generating an orthogonal maze with default parameters:
|
22
|
+
|
23
|
+
$ ruby -Ilib examples/orthogonal.rb
|
24
|
+
|
25
|
+
_ _ _ _ _ _ _ _ _ _
|
26
|
+
|_ _ _| _ | |
|
27
|
+
| _|_| _ _| |_ _| |
|
28
|
+
| _ _ _|_ _ | _|
|
29
|
+
|_ | _ _ |_| |_ |
|
30
|
+
| _| | |_ | |_|
|
31
|
+
|_ |_| | _|_ _| | |
|
32
|
+
| |_ _ _| _ _ |_ |
|
33
|
+
| _ _|_ | |_ _ _|
|
34
|
+
|_ |_ _ |_ _ | |
|
35
|
+
|_ _ _ _|_ _ _ _ _|_|
|
36
|
+
Generated with: examples/orthogonal.rb 10 10 4083063427
|
37
|
+
Written to output file: sample_orthogonal.png
|
38
|
+
|
39
|
+
Generating a sigma maze of dimensions 20x5
|
40
|
+
|
41
|
+
$ ruby -Ilib examples/sigma.rb 20 5
|
42
|
+
_ _ _ _ _ _ _ _ _ _
|
43
|
+
/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_
|
44
|
+
\_ \_ / _/ _ _/ \_ _/ _ \
|
45
|
+
/ _ \ / \_ _/ \_ \_/ \_/ / \_/
|
46
|
+
\_ \_ \ / \_/ _ \_/ \ / \ / _/ \ \
|
47
|
+
/ \ / \_/ \ \_ \_ _/ / \_ \_ \ /
|
48
|
+
\ \ \_ \_ _/ \_/ \_/ \ / \_ \_/ \
|
49
|
+
/ \_/ \_ \_/ \_/ _ _/ _/ _ _/ /
|
50
|
+
\_ \ \_/ _/ _/ \_ _/ _ \_/ _/ \
|
51
|
+
/ \_/ \ / _ \_ \ \_/ \_/ \_/ \ / \ /
|
52
|
+
\_ _/ _ \_ _ \_ _ _ _ \_ \
|
53
|
+
\_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/
|
54
|
+
Generated with: examples/sigma.rb 20 5 2645469508
|
55
|
+
Written to output file: sample_sigma.png
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'maze'
|
2
|
+
|
3
|
+
module MazeHelper
|
4
|
+
ORTHOGONAL = :orthogonal
|
5
|
+
SIGMA = :sigma
|
6
|
+
|
7
|
+
def self.generate(type, format = :ascii, output_file = nil)
|
8
|
+
width = (ARGV[0] || 10).to_i
|
9
|
+
height = (ARGV[1] || width).to_i
|
10
|
+
seed = (ARGV[2] || rand(0xFFFF_FFFF)).to_i
|
11
|
+
|
12
|
+
srand(seed)
|
13
|
+
|
14
|
+
params = { :width => width, :height => height }
|
15
|
+
maze = Maze.const_get(type.capitalize).new(params)
|
16
|
+
|
17
|
+
puts maze.generate.draw(:ascii)
|
18
|
+
unless :ascii == format
|
19
|
+
maze.generate.draw(:png, output_file)
|
20
|
+
end
|
21
|
+
|
22
|
+
puts "Generated with: #{__FILE__} #{width} #{height} #{seed}"
|
23
|
+
puts "Written to output file: #{output_file}" if output_file
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
data/examples/sigma.rb
ADDED
data/lib/maze.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Maze
|
2
|
+
module Algorithm
|
3
|
+
class RecursiveBacktracker
|
4
|
+
|
5
|
+
attr_reader :maze
|
6
|
+
|
7
|
+
def initialize(maze)
|
8
|
+
@maze = maze
|
9
|
+
end
|
10
|
+
|
11
|
+
# Start from any random point in the grid.
|
12
|
+
def run
|
13
|
+
find_path_from(Point.random(maze))
|
14
|
+
end
|
15
|
+
|
16
|
+
# 1. From the current point pick a neighbouring point which has not been
|
17
|
+
# visited in any direction.
|
18
|
+
# 2. The coordinates of the next point has to be fetched w.r.t the
|
19
|
+
# direction of traversal. In certain cases like for edge points its
|
20
|
+
# possible to step out of bounds. We ignore these moves and pick
|
21
|
+
# another one which will keep us within the grid.
|
22
|
+
# 3. When there is a valid move to a neighbouring point and it has not been
|
23
|
+
# visited earlier, both points are connected. Now the neighbouring point
|
24
|
+
# becomes the current point.
|
25
|
+
# 4. We continue recursively connecting paths until we reach certain points
|
26
|
+
# from which there are no unvisited neighbours, in which case we
|
27
|
+
# continuously backtrack until we find a neighbouring unvisited point.
|
28
|
+
# 5. The algorithm ends its run when it returns to initial point from where
|
29
|
+
# it started it's traversal. At this point it's guaranteed to have
|
30
|
+
# visited all the other points.
|
31
|
+
def find_path_from(current_point)
|
32
|
+
maze.random_directions.each do |direction|
|
33
|
+
next_point = current_point.next(maze, direction)
|
34
|
+
|
35
|
+
if next_point && next_point.unvisited?(maze)
|
36
|
+
maze.connect(current_point, next_point, direction)
|
37
|
+
|
38
|
+
find_path_from(next_point)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Maze
|
2
|
+
module Formatter
|
3
|
+
module Ascii
|
4
|
+
|
5
|
+
def to_s
|
6
|
+
canvas.map { |line| line.join }.join("\n")
|
7
|
+
end
|
8
|
+
|
9
|
+
alias_method :draw, :to_s
|
10
|
+
|
11
|
+
def canvas_draw
|
12
|
+
maze.height.times do |y|
|
13
|
+
maze.width.times do |x|
|
14
|
+
canvas_update Point.new(x, y)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def canvas_update(for_point)
|
20
|
+
origin = calculate_origin(for_point)
|
21
|
+
|
22
|
+
maze.directions.each do |direction|
|
23
|
+
offset = origin + calculate_offset(direction, for_point)
|
24
|
+
|
25
|
+
unless maze.connected?(for_point, direction)
|
26
|
+
canvas[offset.y][offset.x] = select_character(direction)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def calculate_origin(for_point)
|
34
|
+
raise NotImplementedError, "#{self.class.name} does not implement private method: #{__method__}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def calculate_offset(*args)
|
38
|
+
raise NotImplementedError, "#{self.class.name} does not implement private method: #{__method__}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def select_character(direction)
|
42
|
+
raise NotImplementedError, "#{self.class.name} does not implement private method: #{__method__}"
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative '../ascii'
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Formatter
|
5
|
+
module Ascii
|
6
|
+
class Orthogonal
|
7
|
+
|
8
|
+
attr_reader :maze, :canvas
|
9
|
+
|
10
|
+
include Ascii
|
11
|
+
|
12
|
+
def initialize(maze)
|
13
|
+
@maze = maze
|
14
|
+
@canvas = Array.new(maze.height + 1) do
|
15
|
+
Array.new(maze.width * 2, ' ')
|
16
|
+
end
|
17
|
+
|
18
|
+
canvas_draw
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def calculate_origin(for_point)
|
24
|
+
for_point.update do |other|
|
25
|
+
other.x *= 2
|
26
|
+
other.y += 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def calculate_offset(*args)
|
31
|
+
direction = args.first
|
32
|
+
|
33
|
+
case direction
|
34
|
+
when maze.class::N then Point.new(1, -1)
|
35
|
+
when maze.class::W then Point.new(0, 0)
|
36
|
+
when maze.class::S then Point.new(1, 0)
|
37
|
+
when maze.class::E then Point.new(2, 0)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def select_character(for_direction)
|
42
|
+
case for_direction
|
43
|
+
when maze.class::N, maze.class::S then "_"
|
44
|
+
when maze.class::W, maze.class::E then "|"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require_relative '../ascii'
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Formatter
|
5
|
+
module Ascii
|
6
|
+
class Sigma
|
7
|
+
|
8
|
+
attr_reader :maze, :canvas
|
9
|
+
|
10
|
+
include Ascii
|
11
|
+
|
12
|
+
def initialize(maze)
|
13
|
+
@maze = maze
|
14
|
+
@canvas = Array.new(maze.height * 2 + 2) do
|
15
|
+
Array.new(maze.width * 2 + 2, ' ')
|
16
|
+
end
|
17
|
+
|
18
|
+
canvas_draw
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def calculate_origin(for_point)
|
24
|
+
for_point.update do |other|
|
25
|
+
other.x *= 2
|
26
|
+
other.y *= 2
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def calculate_offset(*args)
|
31
|
+
direction, point = args
|
32
|
+
|
33
|
+
shift_y = maze.shifted_down?(point) ? 1 : 0
|
34
|
+
|
35
|
+
case direction
|
36
|
+
when :north then Point.new(1, 0 + shift_y)
|
37
|
+
when :north_west then Point.new(0, 1 + shift_y)
|
38
|
+
when :north_east then Point.new(2, 1 + shift_y)
|
39
|
+
when :south_west then Point.new(0, 2 + shift_y)
|
40
|
+
when :south then Point.new(1, 2 + shift_y)
|
41
|
+
when :south_east then Point.new(2, 2 + shift_y)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def select_character(direction)
|
46
|
+
case direction
|
47
|
+
when :north, :south then "_"
|
48
|
+
when :north_west, :south_east then "/"
|
49
|
+
when :north_east, :south_west then "\\"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Maze
|
2
|
+
module Formatter
|
3
|
+
module Png
|
4
|
+
|
5
|
+
PADDING = 10
|
6
|
+
LENGTH = 30
|
7
|
+
|
8
|
+
def draw(output_file = nil)
|
9
|
+
lines.each do |end_points|
|
10
|
+
image.line(*end_points, ChunkyPNG::Color::BLACK)
|
11
|
+
end
|
12
|
+
image.save(output_file, :fast_rgb) if output_file
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def background_colour
|
18
|
+
ChunkyPNG::Color::WHITE
|
19
|
+
end
|
20
|
+
|
21
|
+
def padding_offset
|
22
|
+
PADDING * 2
|
23
|
+
end
|
24
|
+
|
25
|
+
def draw_image
|
26
|
+
maze.height.times do |row|
|
27
|
+
maze.width.times do |column|
|
28
|
+
closed_line_segments(row, column).each do |line_segment|
|
29
|
+
lines << line_segment.map { |point| point.raw }.flatten
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require_relative '../png'
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Formatter
|
5
|
+
module Png
|
6
|
+
class Orthogonal
|
7
|
+
|
8
|
+
attr_reader :maze, :image, :lines
|
9
|
+
|
10
|
+
include Png
|
11
|
+
|
12
|
+
def initialize(maze)
|
13
|
+
@maze = maze
|
14
|
+
@image = ChunkyPNG::Image.new(width, height, background_colour)
|
15
|
+
@lines = []
|
16
|
+
|
17
|
+
draw_image
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def width
|
23
|
+
(maze.width * LENGTH + padding_offset).to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def height
|
27
|
+
(maze.height * LENGTH + padding_offset).to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def origins
|
31
|
+
@origins ||= compute_origin_points
|
32
|
+
end
|
33
|
+
|
34
|
+
def compute_origin_points
|
35
|
+
points = Array.new(maze.height) { Array.new(maze.width) }
|
36
|
+
|
37
|
+
maze.height.times do |row|
|
38
|
+
maze.width.times do |column|
|
39
|
+
points[row][column] =
|
40
|
+
if 0 == column
|
41
|
+
first_origin_point(row)
|
42
|
+
else
|
43
|
+
next_horizontal_origin_point(points[row][column - 1])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
points
|
49
|
+
end
|
50
|
+
|
51
|
+
def first_origin_point(for_row)
|
52
|
+
x = (PADDING + LENGTH * 0.5).to_i
|
53
|
+
y = (PADDING + LENGTH * 0.5 + for_row * LENGTH).to_i
|
54
|
+
|
55
|
+
Point.new(x, y)
|
56
|
+
end
|
57
|
+
|
58
|
+
def next_horizontal_origin_point(previous_point)
|
59
|
+
previous_point.update { |next_origin| next_origin.x += LENGTH }
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Given the origin computes the corner points of the square
|
64
|
+
#
|
65
|
+
# 1 2
|
66
|
+
# <--------L-------->
|
67
|
+
# +--------+--------+
|
68
|
+
# | ^ |<-.5 L->|
|
69
|
+
# | .5L| |
|
70
|
+
# | V | |
|
71
|
+
# +--------+--------+
|
72
|
+
# | |O |
|
73
|
+
# | | |
|
74
|
+
# | | |
|
75
|
+
# +--------+--------+
|
76
|
+
# 4 3
|
77
|
+
#
|
78
|
+
# O is Origin
|
79
|
+
#
|
80
|
+
# Faces in clockwise direction:
|
81
|
+
# * North : 1 - 2
|
82
|
+
# * East : 2 - 3
|
83
|
+
# * South : 3 - 4
|
84
|
+
# * West : 4 - 1
|
85
|
+
#
|
86
|
+
def corner_points(origin)
|
87
|
+
faces = []
|
88
|
+
offsets = [ [-LENGTH, -LENGTH],
|
89
|
+
[ LENGTH, -LENGTH],
|
90
|
+
[ LENGTH, LENGTH],
|
91
|
+
[-LENGTH, LENGTH],
|
92
|
+
[-LENGTH, -LENGTH] ]
|
93
|
+
|
94
|
+
offsets.each do |(offset_x, offset_y)|
|
95
|
+
faces << origin.update do |corner|
|
96
|
+
corner.x += offset_x / 2
|
97
|
+
corner.y += offset_y / 2
|
98
|
+
end
|
99
|
+
end
|
100
|
+
faces
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Group adjacent corner points into pairs
|
105
|
+
#
|
106
|
+
def face_line_segment(origin)
|
107
|
+
corner_points(origin).each_cons(2)
|
108
|
+
end
|
109
|
+
|
110
|
+
def clockwise_directions
|
111
|
+
[ maze.class::N, maze.class::E, maze.class::S, maze.class::W ]
|
112
|
+
end
|
113
|
+
|
114
|
+
def filter_open_faces(line_segments, open = [])
|
115
|
+
line_segments.reject.each_with_index do |segment, i|
|
116
|
+
open.include? clockwise_directions[i]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def closed_line_segments(row, column)
|
121
|
+
filter_open_faces( face_line_segment(origins[row][column]),
|
122
|
+
maze.grid[row][column] )
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require_relative '../png'
|
2
|
+
|
3
|
+
module Maze
|
4
|
+
module Formatter
|
5
|
+
module Png
|
6
|
+
class Sigma
|
7
|
+
|
8
|
+
attr_reader :maze, :image, :lines
|
9
|
+
|
10
|
+
include Png
|
11
|
+
|
12
|
+
def initialize(maze)
|
13
|
+
@maze = maze
|
14
|
+
@image = ChunkyPNG::Image.new(width, height, background_colour)
|
15
|
+
@lines = []
|
16
|
+
|
17
|
+
draw_image
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def width
|
23
|
+
if 0 == maze.width % 2
|
24
|
+
(1.5 * maze.width * LENGTH + 0.5 * LENGTH + padding_offset).to_i
|
25
|
+
else
|
26
|
+
horizontal_units = (1..maze.width).map do |cell_number|
|
27
|
+
0 == cell_number % 2 ? 1 : 2
|
28
|
+
end.inject do |s, x|
|
29
|
+
s + x
|
30
|
+
end
|
31
|
+
|
32
|
+
(horizontal_units * LENGTH + padding_offset).to_i
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def height
|
37
|
+
(maze.height * 2 * LENGTH + LENGTH + padding_offset).to_i
|
38
|
+
end
|
39
|
+
|
40
|
+
def origins
|
41
|
+
@origins ||= compute_origin_points
|
42
|
+
end
|
43
|
+
|
44
|
+
def compute_origin_points
|
45
|
+
points = Array.new(maze.height) { Array.new(maze.width) }
|
46
|
+
|
47
|
+
maze.height.times do |row|
|
48
|
+
maze.width.times do |column|
|
49
|
+
points[row][column] =
|
50
|
+
if 0 == column
|
51
|
+
first_origin_point(row)
|
52
|
+
else
|
53
|
+
next_horizontal_origin_point(points[row][column - 1], column)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
points
|
59
|
+
end
|
60
|
+
|
61
|
+
def first_origin_point(row)
|
62
|
+
x = (LENGTH + PADDING).to_i
|
63
|
+
y = (LENGTH + row * 2 * LENGTH + PADDING).to_i
|
64
|
+
|
65
|
+
Point.new(x, y)
|
66
|
+
end
|
67
|
+
|
68
|
+
def next_horizontal_origin_point(previous_point, current_column)
|
69
|
+
previous_point.update do |next_origin|
|
70
|
+
next_origin.x += (1.5 * LENGTH).to_i
|
71
|
+
next_origin.y += (current_column % 2 == 0 ? -LENGTH : LENGTH).to_i
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Compute corner points traversing in the clock-wise direction
|
77
|
+
#
|
78
|
+
# 0 _____ 1
|
79
|
+
# / \
|
80
|
+
# 5 / + \ 2
|
81
|
+
# \ origin/
|
82
|
+
# 4 \_____/ 3
|
83
|
+
#
|
84
|
+
# Once the origin is known the points 0 - 5 through can be computed
|
85
|
+
#
|
86
|
+
# Output: [x0, y0, x1, y1, ... , x5, y5, x0, y0]
|
87
|
+
# Is the closed path of the hexagon
|
88
|
+
#
|
89
|
+
def corner_points(origin)
|
90
|
+
faces = []
|
91
|
+
offsets = [ [ -LENGTH / 2, -LENGTH ],
|
92
|
+
[ LENGTH / 2, -LENGTH ],
|
93
|
+
[ LENGTH, 0 ],
|
94
|
+
[ LENGTH / 2, LENGTH ],
|
95
|
+
[ -LENGTH / 2, LENGTH ],
|
96
|
+
[ -LENGTH, 0 ],
|
97
|
+
[ -LENGTH / 2, -LENGTH ] ]
|
98
|
+
|
99
|
+
offsets.each do |(offset_x, offset_y)|
|
100
|
+
faces << origin.update do |corner|
|
101
|
+
corner.x += offset_x
|
102
|
+
corner.y += offset_y
|
103
|
+
end
|
104
|
+
end
|
105
|
+
faces
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Group adjacent corner points into pairs
|
110
|
+
#
|
111
|
+
def face_line_segment(origin)
|
112
|
+
corner_points(origin).each_cons(2)
|
113
|
+
end
|
114
|
+
|
115
|
+
#
|
116
|
+
# Traces the clockwise directions of the hexagon w.r.t the closed path
|
117
|
+
#
|
118
|
+
def clockwise_directions
|
119
|
+
[ maze.class::N, maze.class::NE, maze.class::SE,
|
120
|
+
maze.class::S, maze.class::SW, maze.class::NW ]
|
121
|
+
end
|
122
|
+
|
123
|
+
def filter_open_faces(line_segments, open = [])
|
124
|
+
line_segments.reject.each_with_index do |segment, i|
|
125
|
+
open.include? clockwise_directions[i]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def closed_line_segments(row, column)
|
130
|
+
filter_open_faces( face_line_segment(origins[row][column]),
|
131
|
+
maze.grid[row][column] )
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
data/lib/maze/generic.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'algorithms/recursive_backtracker'
|
2
|
+
require_relative 'formatters/ascii'
|
3
|
+
|
4
|
+
module Maze
|
5
|
+
class Generic
|
6
|
+
|
7
|
+
attr_reader :width, :height, :grid, :algorithm
|
8
|
+
|
9
|
+
N = :north
|
10
|
+
S = :south
|
11
|
+
E = :east
|
12
|
+
W = :west
|
13
|
+
NE = :north_east
|
14
|
+
NW = :north_west
|
15
|
+
SE = :south_east
|
16
|
+
SW = :south_west
|
17
|
+
|
18
|
+
def initialize(options)
|
19
|
+
@width = options[:width] || 10
|
20
|
+
@height = options[:height] || 10
|
21
|
+
@grid = Array.new(height) { Array.new(width) }
|
22
|
+
@algorithm = Algorithm::RecursiveBacktracker.new(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate
|
26
|
+
tap { algorithm.run }
|
27
|
+
end
|
28
|
+
|
29
|
+
def connected?(point, direction)
|
30
|
+
self[point].include?(direction)
|
31
|
+
end
|
32
|
+
|
33
|
+
def connect(current_point, next_point, direction)
|
34
|
+
self[current_point] = direction
|
35
|
+
self[next_point] = opposite(direction)
|
36
|
+
end
|
37
|
+
|
38
|
+
def []=(point, direction)
|
39
|
+
grid[point.y][point.x] ||= []
|
40
|
+
grid[point.y][point.x] << direction
|
41
|
+
end
|
42
|
+
|
43
|
+
def [](point)
|
44
|
+
grid[point.y][point.x]
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw(format, output_file = nil)
|
48
|
+
format = format.capitalize.to_s
|
49
|
+
class_name = Formatter.const_get(format).const_get(tessellation)
|
50
|
+
|
51
|
+
unless output_file
|
52
|
+
class_name.new(self).draw
|
53
|
+
else
|
54
|
+
class_name.new(self).draw(output_file)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def tessellation
|
59
|
+
self.class.name[/::(.*)$/, 1]
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'formatters/ascii/orthogonal'
|
2
|
+
require_relative 'formatters/png/orthogonal'
|
3
|
+
|
4
|
+
module Maze
|
5
|
+
class Orthogonal < Generic
|
6
|
+
OPPOSITE = { N => S, S => N, E => W, W => E }
|
7
|
+
|
8
|
+
def random_directions
|
9
|
+
[N, S, E, W].sort_by { rand }
|
10
|
+
end
|
11
|
+
|
12
|
+
alias_method :directions, :random_directions
|
13
|
+
|
14
|
+
def opposite(direction)
|
15
|
+
OPPOSITE[direction]
|
16
|
+
end
|
17
|
+
|
18
|
+
def calculate_offset_x_axis(*args)
|
19
|
+
distance_x = { N => 0, S => 0, E => 1, W => -1 }
|
20
|
+
distance_x[args.first]
|
21
|
+
end
|
22
|
+
|
23
|
+
def calculate_offset_y_axis(*args)
|
24
|
+
distance_y = { N => -1, S => 1, E => 0, W => 0 }
|
25
|
+
distance_y[args.first]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/maze/point.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Maze
|
2
|
+
class Point < Struct.new(:x, :y)
|
3
|
+
|
4
|
+
def self.random(maze)
|
5
|
+
new(rand(maze.width), rand(maze.height))
|
6
|
+
end
|
7
|
+
|
8
|
+
def out_of_bounds?(maze)
|
9
|
+
!(x.between?(0, maze.width - 1) && y.between?(0, maze.height - 1))
|
10
|
+
end
|
11
|
+
|
12
|
+
def unvisited?(maze)
|
13
|
+
maze[self].nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def next(maze, direction)
|
17
|
+
next_x = x + maze.calculate_offset_x_axis(direction, self)
|
18
|
+
next_y = y + maze.calculate_offset_y_axis(direction, self)
|
19
|
+
next_point = self.class.new(next_x, next_y)
|
20
|
+
|
21
|
+
next_point.out_of_bounds?(maze) ? nil : next_point
|
22
|
+
end
|
23
|
+
|
24
|
+
def update
|
25
|
+
other = self.dup
|
26
|
+
yield other if block_given?
|
27
|
+
other
|
28
|
+
end
|
29
|
+
|
30
|
+
def +(other)
|
31
|
+
self.class.new(x + other.x, y + other.y)
|
32
|
+
end
|
33
|
+
|
34
|
+
def raw
|
35
|
+
[x, y]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
data/lib/maze/sigma.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require_relative 'formatters/ascii/sigma'
|
2
|
+
require_relative 'formatters/png/sigma'
|
3
|
+
|
4
|
+
module Maze
|
5
|
+
class Sigma < Generic
|
6
|
+
OPPOSITE = { N => S, S => N, NE => SW, SW => NE, NW => SE, SE => NW }
|
7
|
+
|
8
|
+
def random_directions
|
9
|
+
[N, S, NE, NW, SE, SW].sort_by { rand }
|
10
|
+
end
|
11
|
+
|
12
|
+
alias_method :directions, :random_directions
|
13
|
+
|
14
|
+
def opposite(direction)
|
15
|
+
OPPOSITE[direction]
|
16
|
+
end
|
17
|
+
|
18
|
+
def calculate_offset_x_axis(*args)
|
19
|
+
direction, point = args
|
20
|
+
|
21
|
+
case direction
|
22
|
+
when N, S
|
23
|
+
0
|
24
|
+
when NW, SW
|
25
|
+
send("noexit_#{direction}?", point) ? 0 : -1
|
26
|
+
when NE, SE
|
27
|
+
send("noexit_#{direction}?", point) ? 0 : 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def calculate_offset_y_axis(*args)
|
32
|
+
direction, point = args
|
33
|
+
|
34
|
+
case direction
|
35
|
+
when N
|
36
|
+
noexit_north?(point) ? 0 : -1
|
37
|
+
when S
|
38
|
+
noexit_south?(point) ? 0 : 1
|
39
|
+
when NW, NE
|
40
|
+
send("noexit_#{direction}?", point) || shifted_down?(point) ? 0 : -1
|
41
|
+
when SW, SE
|
42
|
+
send("noexit_#{direction}?", point) || shifted_up?(point) ? 0 : 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def shifted_up?(point)
|
47
|
+
0 == point.x % 2
|
48
|
+
end
|
49
|
+
|
50
|
+
def shifted_down?(point)
|
51
|
+
!shifted_up?(point)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def noexit_north?(point)
|
57
|
+
0 == point.y
|
58
|
+
end
|
59
|
+
|
60
|
+
def noexit_south?(point)
|
61
|
+
height - 1 == point.y
|
62
|
+
end
|
63
|
+
|
64
|
+
def noexit_west?(point)
|
65
|
+
0 == point.x
|
66
|
+
end
|
67
|
+
|
68
|
+
def noexit_east?(point)
|
69
|
+
width - 1 == point.y
|
70
|
+
end
|
71
|
+
|
72
|
+
def noexit_north_west?(point)
|
73
|
+
noexit_west?(point) &&
|
74
|
+
(noexit_north?(point) && shifted_up?(point))
|
75
|
+
end
|
76
|
+
|
77
|
+
def noexit_north_east?(point)
|
78
|
+
noexit_east?(point) &&
|
79
|
+
(noexit_north?(point) && shifted_up?(point))
|
80
|
+
end
|
81
|
+
|
82
|
+
def noexit_south_west?(point)
|
83
|
+
noexit_west?(point) &&
|
84
|
+
(noexit_south?(point) && shifted_down?(point))
|
85
|
+
end
|
86
|
+
|
87
|
+
def noexit_south_east?(point)
|
88
|
+
noexit_east?(point) &&
|
89
|
+
(noexit_south?(point) && shifted_down?(point))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/maze/version.rb
ADDED
data/maze.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require 'maze/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "maze"
|
6
|
+
s.version = Maze::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Sherin C"]
|
9
|
+
s.email = ["sherin.jc@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/csherin/mazes"
|
11
|
+
s.summary = "A library for generating mazes."
|
12
|
+
s.description = <<-DESC
|
13
|
+
A library for generating mazes. It supports orthogonal & sigma tessellations
|
14
|
+
in the ASCII or PNG output formats.
|
15
|
+
DESC
|
16
|
+
|
17
|
+
s.add_runtime_dependency "oily_png"
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: maze
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sherin C
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-29 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: oily_png
|
16
|
+
requirement: &2157606700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2157606700
|
25
|
+
description: ! " A library for generating mazes. It supports orthogonal & sigma
|
26
|
+
tessellations\n in the ASCII or PNG output formats.\n"
|
27
|
+
email:
|
28
|
+
- sherin.jc@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- .gitignore
|
34
|
+
- .rvmrc.example
|
35
|
+
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- README.md
|
38
|
+
- examples/maze_helper.rb
|
39
|
+
- examples/orthogonal.rb
|
40
|
+
- examples/sigma.rb
|
41
|
+
- lib/maze.rb
|
42
|
+
- lib/maze/algorithms/recursive_backtracker.rb
|
43
|
+
- lib/maze/formatters/ascii.rb
|
44
|
+
- lib/maze/formatters/ascii/orthogonal.rb
|
45
|
+
- lib/maze/formatters/ascii/sigma.rb
|
46
|
+
- lib/maze/formatters/png.rb
|
47
|
+
- lib/maze/formatters/png/orthogonal.rb
|
48
|
+
- lib/maze/formatters/png/sigma.rb
|
49
|
+
- lib/maze/generic.rb
|
50
|
+
- lib/maze/orthogonal.rb
|
51
|
+
- lib/maze/point.rb
|
52
|
+
- lib/maze/sigma.rb
|
53
|
+
- lib/maze/version.rb
|
54
|
+
- maze.gemspec
|
55
|
+
homepage: https://github.com/csherin/mazes
|
56
|
+
licenses: []
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.8.13
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: A library for generating mazes.
|
79
|
+
test_files: []
|
80
|
+
has_rdoc:
|