pathfinding 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 10c0aa6e36e9928d826330a689ad3cf9767d35da2cdbd7549d166d39dd773cae
4
+ data.tar.gz: 0ffca62280a953128a6dce12fcf770327c047d0e34643a69bede0b6fadc8aa96
5
+ SHA512:
6
+ metadata.gz: c2f8885e510048fb258f89001c9a00279180d39fd5b088464f45764660e6e25f4ba0646276d37a9e02551800c8742bef6fa44f279d698ad3a0f32c1e24f82c38
7
+ data.tar.gz: 953698ca4662703997497c94977ce312ed16ebe1a38e06955c4c794d36588a70d2e20ab1e345b04f874ba4508de95a38dd2139260021c3029afc273e0a3e8cab
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Quentin DESCHAMPS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,105 @@
1
+ # Pathfinding in Ruby
2
+
3
+ A pathfinding library in Ruby based on A* algorithm.
4
+
5
+ Inpired by [python-pathfinding](https://github.com/brean/python-pathfinding)
6
+
7
+ ## Install
8
+ To install the library, use the `gem` command:
9
+ ```
10
+ gem install pathfinding
11
+ ```
12
+
13
+ ## Usage
14
+ This is a simple example to find a path using A*.
15
+
16
+ 1. Import the library:
17
+ ```ruby
18
+ require 'pathfinding'
19
+ ```
20
+ 2. Create a map using a 2D-list. Any value smaller or equal to 0 describes a walkable node. Any number bigger than 0 describes an obstacle. In this example, we added an obstacle in the middle.
21
+ ```ruby
22
+ matrix = [
23
+ [0, 0, 0],
24
+ [0, 1, 0],
25
+ [0, 0, 0]
26
+ ]
27
+ ```
28
+ Note: you can use negative values to describe different types of obstacles.
29
+
30
+ 3. Create a new grid from this map representation. This will create Nodes instances for every element of the map.
31
+ ```ruby
32
+ grid = Grid.new(matrix)
33
+ ```
34
+
35
+ 4. Set the start and end point from the map. In this example, the start point is on top-left and the end point is on bottom-right.
36
+ ```ruby
37
+ start_node = grid.node(0, 0)
38
+ end_node = grid.node(2, 2)
39
+ ```
40
+
41
+ 5. Create a new instance of the finder and run the `find_path` method. If a path from start to the end point exists, this method returns the list of nodes of the path. Else, it returns an empty list.
42
+ ```ruby
43
+ finder = AStarFinder.new()
44
+ path = finder.find_path(start_node, end_node, grid)
45
+ ```
46
+ Note:
47
+
48
+ - You can choose the heuristic function to use using the `heuristic` argument.
49
+ - You can also choose the diagonal movements allowed using the `diagonal_movement` argument.
50
+ ```ruby
51
+ finder = AStarFinder.new(Heuristic::method(:manhattan), DiagonalMovement::NEVER)
52
+ ```
53
+ See the [documentation](https://www.rubydoc.info/gems/pathfinding/0.0.1) for more details.
54
+
55
+ 6. Print the result (or do something else with it).
56
+ ```ruby
57
+ puts grid.to_s(path, start_node, end_node)
58
+ ```
59
+ The result should look like this:
60
+ ```
61
+ +---+
62
+ |sxx|
63
+ | #x|
64
+ | e|
65
+ +---+
66
+ ```
67
+ * +, - and | characters show the border around the map
68
+ * the blank space is a free field
69
+ * 's' marks the start
70
+ * 'e' marks the end
71
+ * '#' is the obstacle
72
+ * the 'x' characters mark the path from start to end
73
+
74
+ This is the whole example:
75
+ ```ruby
76
+ require 'pathfinding'
77
+
78
+ matrix = [
79
+ [0, 0, 0],
80
+ [0, 1, 0],
81
+ [0, 0, 0]
82
+ ]
83
+ grid = Grid.new(matrix)
84
+
85
+ start_node = grid.node(0, 0)
86
+ end_node = grid.node(2, 2)
87
+
88
+ finder = AStarFinder.new()
89
+ path = finder.find_path(start_node, end_node, grid)
90
+
91
+ puts grid.to_s(path, start_node, end_node)
92
+ ```
93
+
94
+ Take a look at the `examples/` folder for more examples.
95
+
96
+ ## Links
97
+ - GitHub: https://github.com/Quentin18/pathfinding.rb
98
+ - RubyGems: https://rubygems.org/gems/pathfinding
99
+ - Documentation: https://www.rubydoc.info/gems/pathfinding/0.0.1
100
+
101
+ ## Author
102
+ [Quentin Deschamps](mailto:quentindeschamps18@gmail.com)
103
+
104
+ ## License
105
+ [MIT](https://choosealicense.com/licenses/mit/)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathfinding/core/diagonal_movement'
4
+ require 'pathfinding/core/grid'
5
+ require 'pathfinding/core/heuristic'
6
+ require 'pathfinding/core/node'
7
+ require 'pathfinding/finder/astar'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # File: diagonal_movement.rb
5
+ # Author: Quentin Deschamps
6
+ # Date: August 2020
7
+ #
8
+
9
+ #
10
+ # Constants to set diagonal movements.
11
+ #
12
+ module DiagonalMovement
13
+ # Always accept diagonal movements.
14
+ ALWAYS = 1
15
+
16
+ # Never accept diagonal movements.
17
+ NEVER = 2
18
+
19
+ # Accept a diagonal movement if there is at most one obstacle.
20
+ IF_AT_MOST_ONE_OBSTACLE = 3
21
+
22
+ # Accept a diagonal movement only when there is no obstacle.
23
+ ONLY_WHEN_NO_OBSTACLE = 4
24
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # File: grid.rb
5
+ # Author: Quentin Deschamps
6
+ # Date: August 2020
7
+ #
8
+
9
+ require 'pathfinding/core/node'
10
+ require 'pathfinding/core/diagonal_movement'
11
+
12
+ #
13
+ # Represents the grid as a 2d-list of nodes.
14
+ #
15
+ class Grid
16
+ include Enumerable
17
+
18
+ #
19
+ # Creates a grid from a matrix:
20
+ # * 0 (or less) represents a walkable node
21
+ # * A number greater than 0 does not represents a walkable node
22
+ # The +width+ represents the number of columns whereas the +height+
23
+ # is the number of rows of the grid. The +node+ attribute
24
+ # is the list of nodes.
25
+ #
26
+ def initialize(matrix)
27
+ @height = matrix.length
28
+ @width = matrix[0].length
29
+ @nodes = Grid.build_nodes(@width, @height, matrix)
30
+ end
31
+
32
+ #
33
+ # Gets the node at position (+x+, +y+).
34
+ #
35
+ def node(x, y)
36
+ @nodes[y][x]
37
+ end
38
+
39
+ #
40
+ # Yields all nodes of the grid.
41
+ #
42
+ def each_node
43
+ @height.times do |y|
44
+ @width.times do |x|
45
+ yield node(x, y)
46
+ end
47
+ end
48
+ end
49
+
50
+ #
51
+ # Creates a printable string from the grid using ASCII characters.
52
+ # Params:
53
+ # +path+:: list of nodes that show the path
54
+ # +start_node+:: start node
55
+ # +end_node+:: end node
56
+ # +border+:: create a border around the grid
57
+ # +start_chr+:: character for the start (default "s")
58
+ # +end_chr+:: character for the end (default "e")
59
+ # +path_chr+:: character for the path (default "x")
60
+ # +empty_chr+:: character for the empty fields (default " ")
61
+ # +block_chr+:: character for the blocking elements (default "#")
62
+ #
63
+ def to_s(
64
+ path = nil, start_node = nil, end_node = nil, border = true,
65
+ start_chr = 's', end_chr = 'e', path_chr = 'x', empty_chr = ' ', block_chr = '#'
66
+ )
67
+ data = []
68
+ data << '+' + '-' * @width + '+' if border
69
+ @height.times do |y|
70
+ line = ''
71
+ line += '|' if border
72
+ @width.times do |x|
73
+ current = node(x, y)
74
+ if current == start_node
75
+ line += start_chr
76
+ elsif current == end_node
77
+ line += end_chr
78
+ elsif path&.include?(current)
79
+ line += path_chr
80
+ elsif current.walkable
81
+ line += empty_chr
82
+ else
83
+ line += block_chr
84
+ end
85
+ end
86
+ line += '|' if border
87
+ data << line
88
+ end
89
+ data << '+' + '-' * @width + '+' if border
90
+ data.join("\n")
91
+ end
92
+
93
+ #
94
+ # Returns if the (+x+, +y+) position is in the grid.
95
+ #
96
+ def inside?(x, y)
97
+ x >= 0 && x < @width && y >= 0 && y < @height
98
+ end
99
+
100
+ #
101
+ # Returns if a node at position (+x+, +y+) is walkable.
102
+ #
103
+ def walkable?(x, y)
104
+ inside?(x, y) && node(x, y).walkable
105
+ end
106
+
107
+ #
108
+ # Get all neighbors of a node.
109
+ #
110
+ def neighbors(node, diagonal_movement=DiagonalMovement::NEVER)
111
+ x = node.x
112
+ y = node.y
113
+ neighbors = []
114
+ s0 = d0 = s1 = d1 = s2 = d2 = s3 = d3 = false
115
+
116
+ # ↑
117
+ if walkable?(x, y - 1)
118
+ neighbors << node(x, y - 1)
119
+ s0 = true
120
+ end
121
+
122
+ # →
123
+ if walkable?(x + 1, y)
124
+ neighbors << node(x + 1, y)
125
+ s1 = true
126
+ end
127
+
128
+ # ↓
129
+ if walkable?(x, y + 1)
130
+ neighbors << node(x, y + 1)
131
+ s2 = true
132
+ end
133
+
134
+ # ←
135
+ if walkable?(x - 1, y)
136
+ neighbors << node(x - 1, y)
137
+ s3 = true
138
+ end
139
+
140
+ return neighbors if diagonal_movement == DiagonalMovement::NEVER
141
+
142
+ if diagonal_movement == DiagonalMovement::ONLY_WHEN_NO_OBSTACLE
143
+ d0 = s3 && s0
144
+ d1 = s0 && s1
145
+ d2 = s1 && s2
146
+ d3 = s2 && s3
147
+ elsif diagonal_movement == DiagonalMovement::IF_AT_MOST_ONE_OBSTACLE
148
+ d0 = s3 || s0
149
+ d1 = s0 || s1
150
+ d2 = s1 || s2
151
+ d3 = s2 || s3
152
+ elsif diagonal_movement == DiagonalMovement::ALWAYS
153
+ d0 = d1 = d2 = d3 = true
154
+ else
155
+ raise 'Incorrect value of diagonal_movement'
156
+ end
157
+
158
+ # ↖
159
+ neighbors << node(x - 1, y - 1) if d0 && walkable?(x - 1, y - 1)
160
+
161
+ # ↗
162
+ neighbors << node(x + 1, y - 1) if d1 && walkable?(x + 1, y - 1)
163
+
164
+ # ↘
165
+ neighbors << node(x + 1, y + 1) if d2 && walkable?(x + 1, y + 1)
166
+
167
+ # ↙
168
+ neighbors << node(x - 1, y + 1) if d3 && walkable?(x - 1, y + 1)
169
+
170
+ neighbors
171
+ end
172
+
173
+ #
174
+ # Builds and returns the nodes.
175
+ #
176
+ def self.build_nodes(width, height, matrix)
177
+ nodes = []
178
+ height.times do |y|
179
+ nodes << []
180
+ width.times do |x|
181
+ walkable = matrix[y][x] <= 0
182
+ nodes[y] << Node.new(x, y, walkable)
183
+ end
184
+ end
185
+ nodes
186
+ end
187
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # File: heuristic.rb
5
+ # Author: Quentin Deschamps
6
+ # Date: August 2020
7
+ #
8
+
9
+ #
10
+ # A collection of heuristic functions.
11
+ #
12
+ module Heuristic
13
+ #
14
+ # Manhattan distance.
15
+ #
16
+ def self.manhattan(dx, dy)
17
+ dx + dy
18
+ end
19
+
20
+ #
21
+ # Euclidean distance.
22
+ #
23
+ def self.euclidean(dx, dy)
24
+ Math.sqrt(dx * dx + dy * dy)
25
+ end
26
+
27
+ #
28
+ # Octile distance.
29
+ #
30
+ def self.octile(dx, dy)
31
+ f = Math.sqrt(2) - 1
32
+ dx < dy ? f * dx + dy : f * dy + dx
33
+ end
34
+
35
+ #
36
+ # Chebyshev distance.
37
+ #
38
+ def self.chebyshev(dx, dy)
39
+ [dx, dy].max
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # File: node.rb
5
+ # Author: Quentin Deschamps
6
+ # Date: August 2020
7
+ #
8
+
9
+ #
10
+ # Represents a node in the grid.
11
+ #
12
+ class Node
13
+ # Gets the x coordinate in the grid.
14
+ attr_reader :x
15
+
16
+ # Gets the y coordinate in the grid.
17
+ attr_reader :y
18
+
19
+ # Gets whether the node is walkable.
20
+ attr_reader :walkable
21
+
22
+ #
23
+ # Creates a node.
24
+ #
25
+ def initialize(x, y, walkable = true)
26
+ @x = x
27
+ @y = y
28
+ @walkable = walkable
29
+ end
30
+
31
+ #
32
+ # Makes the string format of a node.
33
+ #
34
+ def to_s
35
+ "(#{@x}, #{@y})"
36
+ end
37
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # File: astar.rb
5
+ # Author: Quentin Deschamps
6
+ # Date: August 2020
7
+ #
8
+ require 'pathfinding/core/grid'
9
+ require 'pathfinding/core/diagonal_movement'
10
+ require 'pathfinding/core/heuristic'
11
+
12
+ #
13
+ # A* path-finder.
14
+ #
15
+ class AStarFinder
16
+ #
17
+ # Initializes the A* path-finder. Params:
18
+ # * +heuristic+: heuristic function (see the +Heuristic+ module)
19
+ # * +diagonal_movement+: set diagonal movements (see the +DiagonalMovement+ module)
20
+ #
21
+ def initialize(
22
+ heuristic = Heuristic::method(:manhattan),
23
+ diagonal_movement = DiagonalMovement::NEVER
24
+ )
25
+ @diagonal_movement = diagonal_movement
26
+ if diagonal_movement == DiagonalMovement::NEVER
27
+ @heuristic = heuristic
28
+ else
29
+ @heuristic = Heuristic::method(:octile)
30
+ end
31
+ end
32
+
33
+ #
34
+ # Finds and returns the path as a list of node objects.
35
+ #
36
+ def find_path(start_node, end_node, grid)
37
+ open_set = [start_node]
38
+ came_from = {}
39
+ g_score = {}
40
+ f_score = {}
41
+ grid.each_node do |node|
42
+ g_score[node] = Float::INFINITY
43
+ f_score[node] = Float::INFINITY
44
+ end
45
+ g_score[start_node] = 0
46
+ f_score[start_node] = @heuristic.call(
47
+ (start_node.x - end_node.x).abs, (start_node.y - end_node.y).abs)
48
+
49
+ until open_set.empty?
50
+ current = open_set[0]
51
+ open_set.each do |node|
52
+ current = node if f_score[node] < f_score[current]
53
+ end
54
+
55
+ if current == end_node
56
+ return reconstruct_path(came_from, current)
57
+ end
58
+
59
+ current = open_set.delete_at(open_set.index(current))
60
+
61
+ grid.neighbors(current, @diagonal_movement).each do |neighbor|
62
+ tentative_g_score = g_score[current] + d(current, neighbor)
63
+ next if tentative_g_score >= g_score[neighbor]
64
+
65
+ came_from[neighbor] = current
66
+ g_score[neighbor] = tentative_g_score
67
+ f_score[neighbor] = g_score[neighbor] + @heuristic.call(
68
+ (neighbor.x - end_node.x).abs, (neighbor.y - end_node.y).abs)
69
+ unless open_set.include?(neighbor)
70
+ open_set << neighbor
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ #
77
+ # Returns the distance between two nodes.
78
+ #
79
+ def d(n1, n2)
80
+ (n1.x == n2.x || n1.y == n2.y) ? 1 : Math.sqrt(2)
81
+ end
82
+
83
+ #
84
+ # Reconstructs the path from the current node.
85
+ #
86
+ def reconstruct_path(came_from, current)
87
+ total_path = [current]
88
+ while came_from.include?(current)
89
+ current = came_from[current]
90
+ total_path << current
91
+ end
92
+ total_path.reverse
93
+ end
94
+ end
@@ -0,0 +1,32 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'pathfinding'
3
+ s.version = '0.0.1'
4
+ s.license = 'MIT'
5
+ s.summary = 'A pathfinding library based on A* algorithm.'
6
+ s.description = <<-DESCRIPTION
7
+ A pathfinding library based on A* algorithm.
8
+ Different options about heuristic and diagonal movements can be used.
9
+ The grid can be printed in the command prompt.
10
+ DESCRIPTION
11
+ s.author = 'Quentin Deschamps'
12
+ s.email = 'quentindeschamps18@gmail.com'
13
+ s.homepage = 'https://github.com/Quentin18/pathfinding.rb'
14
+
15
+ s.require_paths = ['lib']
16
+ s.files = [
17
+ 'lib/pathfinding.rb',
18
+ 'lib/pathfinding/core/diagonal_movement.rb',
19
+ 'lib/pathfinding/core/grid.rb',
20
+ 'lib/pathfinding/core/heuristic.rb',
21
+ 'lib/pathfinding/core/node.rb',
22
+ 'lib/pathfinding/finder/astar.rb',
23
+ 'LICENSE',
24
+ 'README.md',
25
+ 'pathfinding.gemspec'
26
+ ]
27
+
28
+ s.rdoc_options = ['--main', 'README.md']
29
+ s.extra_rdoc_files = ['LICENSE', 'README.md']
30
+
31
+ s.post_install_message = 'Thanks for installing!'
32
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pathfinding
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Quentin Deschamps
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2
14
+ A pathfinding library based on A* algorithm.
15
+ Different options about heuristic and diagonal movements can be used.
16
+ The grid can be printed in the command prompt.
17
+ email: quentindeschamps18@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files:
21
+ - LICENSE
22
+ - README.md
23
+ files:
24
+ - LICENSE
25
+ - README.md
26
+ - lib/pathfinding.rb
27
+ - lib/pathfinding/core/diagonal_movement.rb
28
+ - lib/pathfinding/core/grid.rb
29
+ - lib/pathfinding/core/heuristic.rb
30
+ - lib/pathfinding/core/node.rb
31
+ - lib/pathfinding/finder/astar.rb
32
+ - pathfinding.gemspec
33
+ homepage: https://github.com/Quentin18/pathfinding.rb
34
+ licenses:
35
+ - MIT
36
+ metadata: {}
37
+ post_install_message: Thanks for installing!
38
+ rdoc_options:
39
+ - "--main"
40
+ - README.md
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.0.3
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: A pathfinding library based on A* algorithm.
58
+ test_files: []