gastar 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.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/.rspec +0 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +81 -0
- data/Rakefile +1 -0
- data/example/example_runner.rb +79 -0
- data/gastar.gemspec +23 -0
- data/lib/gastar.rb +120 -0
- data/lib/gastar/version.rb +3 -0
- data/spec/gastar_spec.rb +46 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
OTgxNmZlMjM1OWRiNjI3NTU4MDViNDJhYWE0MzQ3YWZmODRiNzcwOA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NWZhMWUxMmYxYTYzMDE5NDBkM2YzNjU3MThjYjljNGZlN2ZjNGFmZA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NTNmNGU5OGFiODBmODA4MDUxYTFlNGYxZmVlN2Y0NDRjNDc3NWNlMGNmY2Vh
|
10
|
+
MmQ0NTFhODExNzJhNjNlNzk1ZDVhNWUzMzVhZDVkNTJiNWNhYjk4ODI4ZGJm
|
11
|
+
OWRiMjJmMGI4NThkNzdhODVmZTNlYmFkYzc4YWY4ZjIxNjU3MDk=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZmYwMzViMjAzMjllNWRmNGY1ZmIyZGVjOTU1ODAyNzk4YmI5Yjc2NDExZTBm
|
14
|
+
NzYzNWQzOTk1ZDdlZTg1YmQyMWQ1OTIwNTYwMTFkOTE4NTJmMjQ0NGU0NmQ3
|
15
|
+
MmZiYzc1Y2RiZDVjNTQ0M2Q0N2EzMjVmMDY3ZTczMDJjNzkzZWM=
|
data/.gitignore
ADDED
data/.rspec
ADDED
File without changes
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jonas Tingeborn
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Gastar
|
2
|
+
|
3
|
+
Generic A* (A-Star) search implementation for Ruby
|
4
|
+
|
5
|
+
It is more or less a straight Ruby implementation of Justin Poliey's Python
|
6
|
+
implementation.
|
7
|
+
|
8
|
+
To read more about the fine algoritm for finding the shortest path between
|
9
|
+
nodes (cities, trade stops etc), read Justin's [blog post]
|
10
|
+
(http://scriptogr.am/jdp/post/pathfinding-with-python-graphs-and-a-star)
|
11
|
+
on the matter and check out the rather fine [Wikipedia article]
|
12
|
+
(http://en.wikipedia.org/wiki/A*_search_algorithm).
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'gastar'
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install gastar
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
Example use:
|
31
|
+
|
32
|
+
require 'gastar'
|
33
|
+
|
34
|
+
# Implement the abstract node class.
|
35
|
+
# Note: All attributes below are custom for this implementation and none are
|
36
|
+
# needed nor used by the actual AStar seach algorithm. They're my domain atts.
|
37
|
+
class Node < AStarNode
|
38
|
+
attr_reader :name, :x, :y
|
39
|
+
def initialize(name, x, y)
|
40
|
+
super()
|
41
|
+
@name, @x, @y = name, x, y
|
42
|
+
end
|
43
|
+
def move_cost(other) 1 end
|
44
|
+
def to_s() name end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Also implement an algorithm for estimating cost of reaching the destination
|
48
|
+
class Space < AStar
|
49
|
+
def heuristic(node, start, goal)
|
50
|
+
Math.sqrt( (goal.x - node.x)**2 + (goal.y - node.y)**2 )
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Create a graph as an ordinary hashmap, with the key being a node and the
|
55
|
+
# value a list of other nodes that can be reached from the key-node.
|
56
|
+
|
57
|
+
sun = Node.new "Sundsvall", 9, 10
|
58
|
+
upp = Node.new "Uppsala", 9, 6
|
59
|
+
sth = Node.new "Stockholm", 10, 5
|
60
|
+
jon = Node.new "Jonkoping", 4, 3
|
61
|
+
got = Node.new "Goteborg", 1, 3
|
62
|
+
mal = Node.new "Malmo", 2, 1
|
63
|
+
|
64
|
+
cities = {
|
65
|
+
sun => [upp],
|
66
|
+
sth => [sun,jon,upp],
|
67
|
+
jon => [sth,got,mal],
|
68
|
+
upp => [sth,sun],
|
69
|
+
mal => [jon],
|
70
|
+
got => [jon]
|
71
|
+
}
|
72
|
+
|
73
|
+
puts Space.new(cities).search(sun, mal)
|
74
|
+
|
75
|
+
## Contributing
|
76
|
+
|
77
|
+
1. Fork it
|
78
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
79
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
80
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
81
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative "../lib/gastar"
|
2
|
+
|
3
|
+
##########################################################
|
4
|
+
# Example implementations
|
5
|
+
##########################################################
|
6
|
+
|
7
|
+
class AStarGrid < AStar
|
8
|
+
def heuristic(node, start, goal)
|
9
|
+
Math.sqrt((goal.x - node.x)**2 + (goal.y - node.y)**2)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class AStarGridNode < AStarNode
|
14
|
+
attr_reader :x, :y
|
15
|
+
def initialize(x, y)
|
16
|
+
super()
|
17
|
+
@x, @y = x, y
|
18
|
+
end
|
19
|
+
def move_cost(other)
|
20
|
+
diagonal = (self.x - other.x).abs == 1 and (self.y - other.y).abs == 1
|
21
|
+
diagonal ? 14 : 10
|
22
|
+
end
|
23
|
+
def to_s
|
24
|
+
"(%d,%d)" % [x,y]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
##########################################################
|
29
|
+
# Example graph generator and use of the search algoritm
|
30
|
+
##########################################################
|
31
|
+
|
32
|
+
def make_graph(width, height)
|
33
|
+
nodes = width.times.map{|x| height.times.map{|y| AStarGridNode.new(x, y) } }
|
34
|
+
graph = {}
|
35
|
+
width.times.to_a.product(height.times.to_a).each do |x, y|
|
36
|
+
node = nodes[x][y]
|
37
|
+
graph[node] = []
|
38
|
+
[-1, 0, 1].product([-1, 0, 1]).each do |i, j|
|
39
|
+
next unless 0 <= x + i && x + i < width
|
40
|
+
next unless 0 <= y + j && y + j < height
|
41
|
+
graph[nodes[x][y]] << nodes[x+i][y+j]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
[graph, nodes]
|
45
|
+
end
|
46
|
+
|
47
|
+
def render(width,height,start, goal, path)
|
48
|
+
vertices = path.map{|step| [step.x, step.y] }
|
49
|
+
height.times.each do |y|
|
50
|
+
width.times.each do |x|
|
51
|
+
if [start.x, start.y] == [x,y]
|
52
|
+
print "S"
|
53
|
+
elsif [goal.x, goal.y] == [x,y]
|
54
|
+
print "G"
|
55
|
+
elsif vertices.include?([x,y])
|
56
|
+
print "+"
|
57
|
+
else
|
58
|
+
print "."
|
59
|
+
end
|
60
|
+
end
|
61
|
+
puts
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def main
|
66
|
+
width, height = 16,10
|
67
|
+
graph, nodes = make_graph(width, height)
|
68
|
+
paths = AStarGrid.new(graph)
|
69
|
+
start, goal = nodes[1][2], nodes[ 12][7]
|
70
|
+
path = paths.search(start, goal)
|
71
|
+
unless path
|
72
|
+
puts "No path found"
|
73
|
+
else
|
74
|
+
puts "Path found:", path.join(" ")
|
75
|
+
render(width,height,start, goal, path)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
main if __FILE__ == $0
|
data/gastar.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'gastar/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "gastar"
|
8
|
+
spec.version = Gastar::VERSION
|
9
|
+
spec.authors = ["Jonas Tingeborn"]
|
10
|
+
spec.email = ["tinjon@gmail.com"]
|
11
|
+
spec.description = %q{A generic A* implementation}
|
12
|
+
spec.summary = %q{A generic A* implementation}
|
13
|
+
spec.homepage = "https://github.com/jojje/gastar"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/gastar.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require "set"
|
2
|
+
require_relative "gastar/version"
|
3
|
+
|
4
|
+
# A generic implementation of the A* search algoritm.
|
5
|
+
#
|
6
|
+
# A* is a search algoritm and this is a ruby implementation of it.
|
7
|
+
# The implementation is such that you wrap the objects you want to find
|
8
|
+
# paths between, add them to a Hash where the keys are nodes and values
|
9
|
+
# neighbours of the corresponding nodes. You then provide this graph (hash)
|
10
|
+
# when you instantiate the +AStar+ search class.
|
11
|
+
#
|
12
|
+
# To find the shortest path, or most profitable between two nodes, you supply
|
13
|
+
# the start and goal nodes to the +#search+ method and if a path can be found
|
14
|
+
# a list of the nodes making up the journey will be provided back. If no path
|
15
|
+
# can be found, nil is returned.
|
16
|
+
#
|
17
|
+
# For implementation examples, refer to the rspec and test runner in the spec
|
18
|
+
# directory.
|
19
|
+
#
|
20
|
+
# Credits:
|
21
|
+
# The gem is mostly a rubification of Justin Poliey's Python implementation.
|
22
|
+
# Check out his excellent post on the subject at:
|
23
|
+
# http://scriptogr.am/jdp/post/pathfinding-with-python-graphs-and-a-star
|
24
|
+
#
|
25
|
+
|
26
|
+
class AStar
|
27
|
+
# Populate the search space and indicate whether searching should maximize
|
28
|
+
# estimated cost for moving between nodes, or whether it should be
|
29
|
+
# minimized.
|
30
|
+
#
|
31
|
+
# Arguments:
|
32
|
+
# +graph+ is a the search space on which the path finding should operate.
|
33
|
+
# It's a plain hash with AStar nodes as keys and also for neighbor as the
|
34
|
+
# values. The value of each hash entry should be a list of neighbors.
|
35
|
+
# E.g. {node => [neighbour node, neighbour node]}
|
36
|
+
#
|
37
|
+
# +maximize_cost+ indicates that higher heuristic values and costs are better,
|
38
|
+
# such as higher profits. Normally it's less that's desired, for example for
|
39
|
+
# distance routing in euclidean space. This an optional argument and the
|
40
|
+
# default is to minimize cost (false).
|
41
|
+
#
|
42
|
+
def initialize(graph, maximize_cost=false)
|
43
|
+
@graph = graph
|
44
|
+
@maximize_cost = maximize_cost
|
45
|
+
end
|
46
|
+
|
47
|
+
# Abstract method that an implementor must provide in a derived class.
|
48
|
+
# This function should return the estimated cost (a number) for getting to
|
49
|
+
# the goal node. In eucledian space (2D/3D), most likely the trigonometric
|
50
|
+
# distance (Pythagorean theorem). For other uses some other function may be
|
51
|
+
# appropriate.
|
52
|
+
#
|
53
|
+
# Arguments:
|
54
|
+
# +node+ is the current node for which cost to the goal should be estimated.
|
55
|
+
# +start+ is the starting node. May or may not be useful to you, but is
|
56
|
+
# provided all the same.
|
57
|
+
# +goal+ is the final destination node towards which the distance / cost
|
58
|
+
# should be estimated.
|
59
|
+
def heuristic(node, start, goal)
|
60
|
+
raise NotImplementedError
|
61
|
+
end
|
62
|
+
|
63
|
+
# Performs the actual path-finding. Takes two AStarNodes.
|
64
|
+
# +start+, is the origin for the search and +goal+ where we want to get to.
|
65
|
+
# Returns the nodes (steps) including the start and goal nodes, that should
|
66
|
+
# be traversed if we were to follow the path.
|
67
|
+
def search(start, goal)
|
68
|
+
openset = Set.new
|
69
|
+
closedset = Set.new
|
70
|
+
current = start
|
71
|
+
openset_min_max = @maximize_cost ? openset.method(:max_by) : openset.method(:min_by)
|
72
|
+
|
73
|
+
openset.add(current)
|
74
|
+
while not openset.empty?
|
75
|
+
current = openset_min_max.call{|o| o.g + o.h }
|
76
|
+
if current == goal
|
77
|
+
path = []
|
78
|
+
while current.parent
|
79
|
+
path << current
|
80
|
+
current = current.parent
|
81
|
+
end
|
82
|
+
path << current
|
83
|
+
return path.reverse
|
84
|
+
end
|
85
|
+
openset.delete(current)
|
86
|
+
closedset.add(current)
|
87
|
+
@graph[current].each do |node|
|
88
|
+
next if closedset.include? node
|
89
|
+
|
90
|
+
if openset.include? node
|
91
|
+
new_g = current.g + current.move_cost(node)
|
92
|
+
if node.g > new_g
|
93
|
+
node.g = new_g
|
94
|
+
node.parent = current
|
95
|
+
end
|
96
|
+
else
|
97
|
+
node.g = current.g + current.move_cost(node)
|
98
|
+
node.h = heuristic(node, start, goal)
|
99
|
+
node.parent = current
|
100
|
+
openset.add(node)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
return nil
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
# Abstract class where you implement the move_cost function
|
110
|
+
# which specifies how expensive it is to get from the
|
111
|
+
# current node to the +other+ node.
|
112
|
+
class AStarNode
|
113
|
+
attr_accessor :g, :h, :parent
|
114
|
+
def initialize
|
115
|
+
@g, @h, @parent = 0, 0, nil
|
116
|
+
end
|
117
|
+
def move_cost(other)
|
118
|
+
raise NotImplementedError
|
119
|
+
end
|
120
|
+
end
|
data/spec/gastar_spec.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative '../lib/gastar'
|
2
|
+
|
3
|
+
class Node < AStarNode
|
4
|
+
attr_reader :name, :x, :y
|
5
|
+
def initialize(name, x, y)
|
6
|
+
super()
|
7
|
+
@name, @x, @y = name, x, y
|
8
|
+
end
|
9
|
+
def move_cost(other) 1 end
|
10
|
+
def to_s() name end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Space < AStar
|
14
|
+
def heuristic(node, start, goal)
|
15
|
+
Math.sqrt( (goal.x - node.x)**2 + (goal.y - node.y)**2 )
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "AStar implementation" do
|
20
|
+
|
21
|
+
it "finds the shorted path between cities" do
|
22
|
+
|
23
|
+
sun = Node.new "Sundsvall", 9, 10
|
24
|
+
upp = Node.new "Uppsala", 9, 6
|
25
|
+
sth = Node.new "Stockholm", 10, 5
|
26
|
+
jon = Node.new "Jonkoping", 4, 3
|
27
|
+
got = Node.new "Gothenburg", 1, 3
|
28
|
+
mal = Node.new "Malmo", 2, 1
|
29
|
+
|
30
|
+
cities = {
|
31
|
+
sun => [upp],
|
32
|
+
sth => [sun,jon,upp],
|
33
|
+
jon => [sth,got,mal],
|
34
|
+
upp => [sth,sun],
|
35
|
+
mal => [jon],
|
36
|
+
got => [jon]
|
37
|
+
}
|
38
|
+
|
39
|
+
expected = [sun, upp, sth, jon, mal]
|
40
|
+
actual = Space.new(cities).search(sun, mal)
|
41
|
+
|
42
|
+
actual.should eql(expected)
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gastar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonas Tingeborn
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: A generic A* implementation
|
42
|
+
email:
|
43
|
+
- tinjon@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- .rspec
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- example/example_runner.rb
|
55
|
+
- gastar.gemspec
|
56
|
+
- lib/gastar.rb
|
57
|
+
- lib/gastar/version.rb
|
58
|
+
- spec/gastar_spec.rb
|
59
|
+
homepage: https://github.com/jojje/gastar
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.0.6
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: A generic A* implementation
|
83
|
+
test_files:
|
84
|
+
- spec/gastar_spec.rb
|
85
|
+
has_rdoc:
|