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 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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in gastar.gemspec
4
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ module Gastar
2
+ VERSION = "1.0.0"
3
+ end
@@ -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: