knossos 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14ddf78b4ee346ccd47871a10ea3da7217f0a2d4b8b4057d8024bb02c8b6940c
4
- data.tar.gz: 7ccba0d9c547d891381142c46a862487d9571af0c6183fe7dfb250faaed44249
3
+ metadata.gz: 8c2bf5993f7288ba9f26d3d51455bf2844f36bd650e1cc3816eaac6e181c6316
4
+ data.tar.gz: fd09305e92ecf3ff5332b251888659187258ba3b4e6d59e36df64741e7c7c471
5
5
  SHA512:
6
- metadata.gz: bd6e51fd8a7116c1f5844e9b0bd98e80ad7edc540d0f3e9a014d16b1cc332013aa825bca03d63adea764e93d9c48e1b8492fb812619d1e2e956def4a8669f98f
7
- data.tar.gz: caa60fb3da899b1e69affbdf172e2a8621a8dd588bc9631b0fbf710aa8985f07ae2e2aa02c924940144a501bf0c74fd7c0ee0b60adff9e67e7dcee2180aae59f
6
+ metadata.gz: 14ae5ceee9e2771b005cbb7900cc753ac7279e3083cc2eb802c96372068d98bc4aea057561afc85a6daca227b45d794f128b1234688d9580ea056d38a82df5db
7
+ data.tar.gz: 135dddf5a3c9542cf346f618c14e27444fae24a58669523549140077703176dc60009159de8b23af4393a0ae996e2e7e27b669afd3cc3e30c09d61822a79470a
@@ -4,32 +4,70 @@ All notable changes to this project will be documented in this file.
4
4
  This project uses [Semantic Versioning][sv].
5
5
 
6
6
  ## [Unreleased][new]
7
+
8
+ ## [0.3.0][0.3.0] — 2020-01-26
9
+
10
+ ### Added
11
+ - Add the `HuntAndKill` algorithm for generating mazes.
12
+ - `BinaryTree` accepts an option to configure the bias.
13
+ - Algorithms accept an argument to `#carve` to set the seed for the random number
14
+ generator.
15
+ - Create a `Serializer` module to hold the methods that encode mazes for
16
+ archiving and transfer.
17
+ - Add the `Bitmask` serializer to represent the maze as a matrix of integers,
18
+ where each integer is a bitfield encoding the passage directions.
19
+ - Add `Bitmask#deserialize` to decode an array of bitmasked integers into a
20
+ maze.
21
+ - Create a `Solver` module to hold algorithms that analyze or plot solutions to
22
+ mazes.
23
+ - Create `Solver::Distances` helper class to store the distances from a root
24
+ cell to any other cell in the maze.
25
+ - Add `Dijkstra#distances_from` to calculate the distances from a given cell to
26
+ every other cell in the maze.
27
+ - Add `Dijkstra#shortest_path` to extract the shortest path from the starting
28
+ cell to a given goal.
29
+ - Add `Dijkstra#longest_path` to find the longest path in the entire maze.
30
+ - Display all distances or the solution path within the rendered grid.
31
+
32
+ ### Changed
33
+ - Change `carve` to be an instance method of the Algorithm classes.
34
+
35
+ ### Fixed
36
+ - Update Usage in README with namespace changes.
37
+
38
+ ## [0.2.0][0.2.0] — 2020-01-09
39
+
7
40
  ### Changed
8
41
  - Correctly nest the `Algorithm` module within the `Knossos` namespace.
9
42
  - Correctly nest the `Renderer` module within the `Knossos` namespace.
10
43
 
11
44
  ## [0.1.0][0.1.0] — 2020-01-07
45
+
12
46
  ### Added
13
47
  - Create an `Algorithm` module to hold the maze generators.
14
48
  - Create a `Renderer` module to hold the methods that produce graphical
15
49
  representations of a maze.
16
50
  - Create `Renderer::Text` to produce ASCII text representations.
51
+
17
52
  ### Changed
18
- - Move `AldousBroder` to the `Algorithm` module
19
- - Move `BinaryTree` to the `Algorithm` module
20
- - Move `RecursiveBacktracker` to the `Algorithm` module
21
- - Move `Sidewinder` to the `Algorithm` module
22
- - Move `Wilsons` to the `Algorithm` module
23
- - Move and rename `Display` to `Renderer::Image`
24
- - Rename `Display.to_png` to `Image.render`
53
+ - Move `AldousBroder` to the `Algorithm` module.
54
+ - Move `BinaryTree` to the `Algorithm` module.
55
+ - Move `RecursiveBacktracker` to the `Algorithm` module.
56
+ - Move `Sidewinder` to the `Algorithm` module.
57
+ - Move `Wilsons` to the `Algorithm` module.
58
+ - Move and rename `Display` to `Renderer::Image`.
59
+ - Rename `Display.to_png` to `Image.render`.
60
+
25
61
  ### Fixed
26
- - Correctly scope private class methods in `Sidewinder`
27
- - Algorithms now uniformly require a `grid:` keyword argument to `.carve`
62
+ - Correctly scope private class methods in `Sidewinder`.
63
+ - Algorithms now uniformly require a `grid:` keyword argument to `.carve`.
64
+
28
65
  ### Removed
29
66
  - `Grid` no longer has a custom `to_s` method to produce a graphical maze.
30
- Use `Renderer::Text.render` instead.
67
+ Use `Renderer::Text.render` instead.
31
68
 
32
69
  ## [0.0.0][0.0.0] — 2020-01-02
70
+
33
71
  ### Added
34
72
  - Restructure the project as a Ruby gem.
35
73
 
@@ -39,6 +77,8 @@ _This file is composed with [GitHub Flavored Markdown][gfm]._
39
77
  [gfm]: https://github.github.com/gfm/
40
78
  [sv]: https://semver.org
41
79
 
42
- [new]: https://github.com/petejh/knossos/compare/HEAD..v0.1.0
80
+ [new]: https://github.com/petejh/knossos/compare/HEAD..v0.3.0
81
+ [0.3.0]: https://github.com/petejh/knossos/releases/tag/v0.3.0
82
+ [0.2.0]: https://github.com/petejh/knossos/releases/tag/v0.2.0
43
83
  [0.1.0]: https://github.com/petejh/knossos/releases/tag/v0.1.0
44
84
  [0.0.0]: https://github.com/petejh/knossos/releases/tag/v0.0.0
data/README.md CHANGED
@@ -19,12 +19,13 @@ Or install the library globally with:
19
19
  require 'knossos'
20
20
 
21
21
  grid = Knossos::Grid.new({rows: 5, columns: 5})
22
- Algorithm::BinaryTree.carve(grid: grid)
22
+ carver = Knossos::Algorithm::BinaryTree.new
23
+ carver.carve(grid: grid)
23
24
 
24
- text_renderer = Renderer::Text.new
25
+ text_renderer = Knossos::Renderer::Text.new
25
26
  puts text_renderer.render(grid: grid)
26
27
 
27
- image_renderer = Renderer::Image.new(grid)
28
+ image_renderer = Knossos::Renderer::Image.new(grid)
28
29
  png = image_renderer.render
29
30
  png.save("maze.png")
30
31
  ```
@@ -3,6 +3,8 @@ require 'knossos/cell'
3
3
  require 'knossos/error'
4
4
  require 'knossos/grid'
5
5
  require 'knossos/renderer'
6
+ require 'knossos/serializer'
7
+ require 'knossos/solver'
6
8
  require 'knossos/version'
7
9
 
8
10
  module Knossos
@@ -1,5 +1,6 @@
1
1
  require_relative 'algorithm/aldous_broder'
2
2
  require_relative 'algorithm/binary_tree'
3
+ require_relative 'algorithm/hunt_and_kill'
3
4
  require_relative 'algorithm/recursive_backtracker'
4
5
  require_relative 'algorithm/sidewinder'
5
6
  require_relative 'algorithm/wilsons'
@@ -1,7 +1,13 @@
1
1
  module Knossos
2
2
  module Algorithm
3
3
  class AldousBroder
4
- def self.carve(grid:)
4
+ def initialize
5
+ # nothing to do
6
+ end
7
+
8
+ def carve(grid:, seed: nil)
9
+ srand(seed || Kernel.srand)
10
+
5
11
  cell = grid.random_cell
6
12
  unvisited = grid.cell_count - 1
7
13
 
@@ -1,15 +1,19 @@
1
1
  module Knossos
2
2
  module Algorithm
3
3
  class BinaryTree
4
- def self.carve(grid:)
5
- grid.each_cell do |cell|
6
- neighbors = []
4
+ attr_reader :bias
5
+
6
+ def initialize(**options)
7
+ options = defaults.merge(options)
8
+
9
+ @bias = options[:bias]
10
+ end
7
11
 
8
- north = grid.north(cell)
9
- neighbors << north if north
12
+ def carve(grid:, seed: nil)
13
+ srand(seed || Kernel.srand)
10
14
 
11
- east = grid.east(cell)
12
- neighbors << east if east
15
+ grid.each_cell do |cell|
16
+ neighbors = bias.map { |direction| grid.send(direction, cell) }.compact
13
17
 
14
18
  neighbor = neighbors.sample
15
19
  grid.build_passage(cell, neighbor) if neighbor
@@ -17,6 +21,12 @@ module Knossos
17
21
 
18
22
  grid
19
23
  end
24
+
25
+ private
26
+
27
+ def defaults
28
+ { bias: [:north, :east] }
29
+ end
20
30
  end
21
31
  end
22
32
  end
@@ -0,0 +1,49 @@
1
+ module Knossos
2
+ module Algorithm
3
+ class HuntAndKill
4
+ attr_reader :bias
5
+
6
+ def initialize(**options)
7
+ options = defaults.merge(options)
8
+ end
9
+
10
+ def carve(grid:, seed: nil)
11
+ srand(seed || Kernel.srand)
12
+
13
+ current = grid.random_cell
14
+
15
+ while current
16
+ unvisited_neighbors = grid.neighborhood(current).select { |n| n.links.empty? }
17
+
18
+ if unvisited_neighbors.any?
19
+ neighbor = unvisited_neighbors.sample
20
+ grid.build_passage(current, neighbor)
21
+ current = neighbor
22
+ else
23
+ current = nil
24
+
25
+ grid.each_cell do |cell|
26
+ visited_neighbors = grid.neighborhood(cell).select { |n| n.links.any? }
27
+ if cell.links.empty? && visited_neighbors.any?
28
+ current = cell
29
+
30
+ neighbor = visited_neighbors.sample
31
+ grid.build_passage(current, neighbor)
32
+
33
+ break
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ grid
40
+ end
41
+
42
+ private
43
+
44
+ def defaults
45
+ {}
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,7 +1,15 @@
1
1
  module Knossos
2
2
  module Algorithm
3
3
  class RecursiveBacktracker
4
- def self.carve(grid:, start_at: grid.random_cell)
4
+ def initialize
5
+ # nothing to do
6
+ end
7
+
8
+ def carve(grid:, start_at: nil, seed: nil)
9
+ srand(seed || Kernel.srand)
10
+
11
+ start_at ||= grid.random_cell
12
+
5
13
  stack = []
6
14
  stack.push start_at
7
15
 
@@ -1,7 +1,13 @@
1
1
  module Knossos
2
2
  module Algorithm
3
3
  class Sidewinder
4
- def self.carve(grid:)
4
+ def initialize
5
+ # nothing to do
6
+ end
7
+
8
+ def carve(grid:, seed: nil)
9
+ srand(seed || Kernel.srand)
10
+
5
11
  grid.each_row do |row|
6
12
  run = []
7
13
  row.each do |cell|
@@ -24,20 +30,18 @@ module Knossos
24
30
  grid
25
31
  end
26
32
 
27
- class << self
28
- private
33
+ private
29
34
 
30
- def close_out_run?(grid, cell)
31
- at_east_border?(grid, cell) || (!at_north_border?(grid, cell) && rand(2) == 0)
32
- end
35
+ def close_out_run?(grid, cell)
36
+ at_east_border?(grid, cell) || (!at_north_border?(grid, cell) && rand(2) == 0)
37
+ end
33
38
 
34
- def at_east_border?(grid, cell)
35
- grid.east(cell).nil?
36
- end
39
+ def at_east_border?(grid, cell)
40
+ grid.east(cell).nil?
41
+ end
37
42
 
38
- def at_north_border?(grid, cell)
39
- grid.north(cell).nil?
40
- end
43
+ def at_north_border?(grid, cell)
44
+ grid.north(cell).nil?
41
45
  end
42
46
  end
43
47
  end
@@ -1,7 +1,13 @@
1
1
  module Knossos
2
2
  module Algorithm
3
3
  class Wilsons
4
- def self.carve(grid:)
4
+ def initialize
5
+ # nothing to do
6
+ end
7
+
8
+ def carve(grid:, seed: nil)
9
+ srand(seed || Kernel.srand)
10
+
5
11
  unvisited = []
6
12
  grid.each_cell { |cell| unvisited << cell }
7
13
 
@@ -5,7 +5,7 @@ module Knossos
5
5
  # nothing to do
6
6
  end
7
7
 
8
- def render(grid:)
8
+ def render(grid:, distances: nil)
9
9
  output = "+" + "---+" * grid.columns + "\n"
10
10
 
11
11
  grid.each_row do |row|
@@ -19,7 +19,7 @@ module Knossos
19
19
  south = grid.south(cell)
20
20
  south_border = cell.linked?(south) ? " " : "---"
21
21
 
22
- body << " " << east_border
22
+ body << decorate(cell, distances) << east_border
23
23
  bottom << south_border << "+"
24
24
  end
25
25
 
@@ -29,6 +29,13 @@ module Knossos
29
29
 
30
30
  output
31
31
  end
32
+
33
+ private
34
+
35
+ def decorate(cell, distances)
36
+ label = distances && distances[cell] ? distances[cell].to_s(36) : ' '
37
+ " #{label} "
38
+ end
32
39
  end
33
40
  end
34
41
  end
@@ -0,0 +1,6 @@
1
+ require_relative 'serializer/bitmask'
2
+
3
+ module Knossos
4
+ module Serializer
5
+ end
6
+ end
@@ -0,0 +1,65 @@
1
+ module Knossos
2
+ module Serializer
3
+ class Bitmask
4
+ NORTH = 1
5
+ EAST = 2
6
+ SOUTH = 4
7
+ WEST = 8
8
+
9
+ def serialize(grid:)
10
+ encoded_maze = Array.new(grid.columns) { Array.new(grid.rows, 0) }
11
+
12
+ grid.each_row do |cells, row|
13
+ cells.each_with_index do |cell, column|
14
+ link_mask = 0
15
+
16
+ cell.links.each do |link|
17
+ link_mask |=
18
+ case column <=> link.column
19
+ when -1
20
+ EAST
21
+ when 0
22
+ 0
23
+ when 1
24
+ WEST
25
+ end
26
+
27
+ link_mask |=
28
+ case row <=> link.row
29
+ when -1
30
+ SOUTH
31
+ when 0
32
+ 0
33
+ when 1
34
+ NORTH
35
+ end
36
+ end
37
+
38
+ encoded_maze[row][column] = link_mask
39
+ end
40
+ end
41
+
42
+ encoded_maze
43
+ end
44
+
45
+ def deserialize(encoded_maze:)
46
+ rows = encoded_maze.length
47
+ columns = encoded_maze.first.length
48
+ grid = Knossos::Grid.new({rows: rows, columns: columns})
49
+
50
+ encoded_maze.each_with_index do |cells, row|
51
+ cells.each_with_index do |cell, column|
52
+ current_cell = grid[row, column]
53
+
54
+ grid.build_passage(current_cell, grid[row - 1, column]) if (cell & NORTH > 0)
55
+ grid.build_passage(current_cell, grid[row, column + 1]) if (cell & EAST > 0)
56
+ grid.build_passage(current_cell, grid[row + 1, column]) if (cell & SOUTH > 0)
57
+ grid.build_passage(current_cell, grid[row, column - 1]) if (cell & WEST > 0)
58
+ end
59
+ end
60
+
61
+ grid
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'solver/dijkstra'
2
+ require_relative 'solver/distances'
3
+
4
+ module Knossos
5
+ module Solver
6
+ end
7
+ end
@@ -0,0 +1,61 @@
1
+ module Knossos
2
+ module Solver
3
+ class Dijkstra
4
+ def initialize
5
+ # nothing to do
6
+ end
7
+
8
+ def distances_from(start:)
9
+ frontier = [start]
10
+ distances = Distances.new(root: start)
11
+
12
+ while frontier.any?
13
+ new_frontier = []
14
+
15
+ frontier.each do |cell|
16
+ cell.links.each do |linked_cell|
17
+ next if distances[linked_cell]
18
+ distances[linked_cell] = distances[cell] + 1
19
+ new_frontier << linked_cell
20
+ end
21
+ end
22
+
23
+ frontier = new_frontier
24
+ end
25
+
26
+ distances
27
+ end
28
+
29
+ def shortest_path(start:, goal:, distances:)
30
+ current = goal
31
+
32
+ path = Distances.new(root: start)
33
+ path[current] = distances[current]
34
+
35
+ until current == start
36
+ current.links.each do |neighbor|
37
+ if distances[neighbor] < distances[current]
38
+ path[neighbor] = distances[neighbor]
39
+ current = neighbor
40
+ break
41
+ end
42
+ end
43
+ end
44
+
45
+ path
46
+ end
47
+
48
+ def longest_path(distances:)
49
+ # Find the cell that is furthest from any given starting cell. Then find
50
+ # the cell that is furthest from that one. This is guaranteed to yield
51
+ # the longest path in a perfect maze.
52
+
53
+ new_start, _ = distances.max
54
+ new_distances = distances_from(start: new_start)
55
+ goal, _ = new_distances.max
56
+
57
+ shortest_path(start: new_start, goal: goal, distances: new_distances)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,37 @@
1
+ module Knossos
2
+ module Solver
3
+ class Distances
4
+ def initialize(root:)
5
+ @root = root
6
+ @cells = {}
7
+ @cells[@root] = 0
8
+ end
9
+
10
+ def [](cell)
11
+ @cells[cell]
12
+ end
13
+
14
+ def []=(cell, distance)
15
+ @cells[cell] = distance
16
+ end
17
+
18
+ def cells
19
+ @cells.keys
20
+ end
21
+
22
+ def max
23
+ max_distance = 0
24
+ max_cell = @root
25
+
26
+ @cells.each do |cell, distance|
27
+ if distance > max_distance
28
+ max_cell = cell
29
+ max_distance = distance
30
+ end
31
+ end
32
+
33
+ [max_cell, max_distance]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module Knossos
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knossos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter J. Hinckley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-10 00:00:00.000000000 Z
11
+ date: 2020-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chunky_png
@@ -81,6 +81,7 @@ files:
81
81
  - lib/knossos/algorithm.rb
82
82
  - lib/knossos/algorithm/aldous_broder.rb
83
83
  - lib/knossos/algorithm/binary_tree.rb
84
+ - lib/knossos/algorithm/hunt_and_kill.rb
84
85
  - lib/knossos/algorithm/recursive_backtracker.rb
85
86
  - lib/knossos/algorithm/sidewinder.rb
86
87
  - lib/knossos/algorithm/wilsons.rb
@@ -91,6 +92,11 @@ files:
91
92
  - lib/knossos/renderer/image.rb
92
93
  - lib/knossos/renderer/png_adapter.rb
93
94
  - lib/knossos/renderer/text.rb
95
+ - lib/knossos/serializer.rb
96
+ - lib/knossos/serializer/bitmask.rb
97
+ - lib/knossos/solver.rb
98
+ - lib/knossos/solver/dijkstra.rb
99
+ - lib/knossos/solver/distances.rb
94
100
  - lib/knossos/version.rb
95
101
  homepage: https://github.com/petejh/knossos
96
102
  licenses: