Permalink
Browse files

move to Mongo, add the a_star algorithm in Path, store the result

  • Loading branch information...
1 parent b506bbc commit 1ee6b99fcefd06ea5141a29e20a42d5924f731c0 @towski committed Dec 22, 2009
Showing with 462 additions and 123 deletions.
  1. +1 −0 .gitignore
  2. +14 −0 background.rb
  3. +8 −0 config/database.yml
  4. +16 −1 lib/animal.rb
  5. +15 −10 lib/map.rb
  6. +115 −0 lib/node.rb
  7. +111 −0 lib/path.rb
  8. +12 −2 lib/terrain.rb
  9. +7 −2 lib/user.rb
  10. +21 −12 main.rb
  11. +1 −2 server.rb
  12. +20 −10 test/map_test.rb
  13. +36 −0 test/path_test.rb
  14. +1 −74 test/test_helper.rb
  15. +50 −0 tmp/astar_file
  16. +15 −0 tmp/old_main.rb
  17. +19 −10 window.rb
View
@@ -0,0 +1 @@
+.DS_Store
View
@@ -0,0 +1,14 @@
+require 'main'
+require 'benchmark'
+loop do
+ realtime = Benchmark.realtime do
+ Animal.all.each do |animal|
+ animal.step
+ animal.save
+ end
+ end
+ sleep_time = 0.2 - realtime
+ if sleep_time > 0
+ sleep sleep_time
+ end
+end
View
@@ -0,0 +1,8 @@
+development: &global_settings
+ database: textual_development
+ host: 127.0.0.1
+ port: 27017
+
+test:
+ database: textual_test
+ <<: *global_settings
View
@@ -1,2 +1,17 @@
-class Animal < ActiveRecord::Base
+class Animal
+ include MongoMapper::Document
+
+ key :x, Integer
+ key :y, Integer
+ key :species, String
+
+ def step
+ case rand(4)
+ when 0: self.x = x + 1;
+ when 1: self.x = x - 1 unless x == 1;
+ when 2: self.y = y + 1
+ when 3: self.y = y - 1 unless y == 1;
+ else
+ end
+ end
end
View
@@ -2,27 +2,32 @@ class Map
include DRb::DRbUndumped
WIDTH = 120
HEIGHT = 40
- attr_reader :animals, :terrain
+ attr_reader :animals, :terrain, :x, :y
def initialize(x, y)
- @terrain = Terrain.all(:conditions => ["x >= ? and x < ? and y >= ? and y < ?", x, x + WIDTH, y, y + HEIGHT])
- @animals = Animal.all(:conditions => ["x >= ? and x < ? and y >= ? and y < ?", x, x + WIDTH, y, y + HEIGHT])
+ @x, @y = x, y
+ @terrain = Terrain.all("$where" => "(this.x >= #{x} || this.x <= #{x + WIDTH}) && (this.y >= #{y} || this.y <= #{y + HEIGHT})")
+ @animals = Animal.all("$where" => "this.x >= #{x} && this.x <= #{x + WIDTH} && this.y >= #{y} && this.y <= #{y + HEIGHT}")
end
def grid
@grid ||= begin
- grid = []
- HEIGHT.times {|i| grid[i] = [] }
+ the_grid = []
+ HEIGHT.times {|i| the_grid[i] = [] }
@terrain.each do |terrain|
- terrain.height.times do |x|
- terrain.width.times do |y|
- grid[x + (terrain.x - 1)][y + (terrain.y - 1)] = terrain.kind
+ terrain.height.times do |y|
+ terrain.width.times do |x|
+ potential_y = y + (terrain.y - 1) - (@y - 1)
+ next if potential_y < 0 || potential_y > HEIGHT - 1
+ potential_x = x + (terrain.x - 1) - (@x - 1)
+ next if potential_x < 0 || potential_x > WIDTH - 1
+ the_grid[potential_y][potential_x] = terrain.kind
end
end
end
@animals.each do |animal|
- grid[animal.x - 1][animal.y - 1] = animal.species
+ the_grid[animal.y - 1 - @y][animal.x - @x] = animal.species
end
- grid
+ the_grid
end
end
end
View
@@ -0,0 +1,115 @@
+# An (x, y) position on the map
+class Position
+ attr_accessor :x, :y
+
+ def initialize(x, y)
+ @x, @y = x, y
+ end
+
+ def ==(other)
+ return false unless Position===other
+ @x == other.x and @y == other.y
+ end
+
+ # Manhattan
+ def distance(other)
+ (@x - other.x).abs + (@y - other.y).abs
+ end
+
+ # Get a position relative to this
+ def relative(xr, yr)
+ Position.new(x + xr, y + yr)
+ end
+end
+
+# A map contains a two-dimensional array of nodes
+# A node represents a tile in the game
+class Node
+ include MongoMapper::EmbeddedDocument
+ include Comparable # by total_cost
+
+ class << self
+ # each node class is defined by a "map letter" and a cost (1, 2, 3)
+ attr_accessor :cost, :letter
+
+ def other_subclasses
+ @@other_subclasses
+ end
+
+ def inherited(klass)
+ super
+ @@other_subclasses ||= []
+ @@other_subclasses << klass
+ end
+
+ def by_name(name)
+ other_subclasses.find{|klass| klass.name == name.titleize }
+ end
+ end
+
+ attr_accessor :parent, :cost, :cost_estimated
+ key :on_path, Boolean
+ key :x, Integer
+ key :y, Integer
+
+ def position
+ @position ||= Position.new(x,y)
+ end
+
+ def initialize(*args)
+ super
+ @cost = 0
+ @cost_estimated = 0
+ @parent = nil
+ end
+
+ def mark_path
+ self.on_path = true
+ @parent.mark_path if @parent
+ end
+
+ def walkable?
+ true # except Water
+ end
+
+ def total_cost
+ cost + cost_estimated
+ end
+
+ def <=> other
+ total_cost <=> other.total_cost
+ end
+
+ def == other
+ position == other.position
+ end
+
+ def to_s
+ on_path ? '#' : self.class.letter
+ end
+end
+
+class Flatland < Node
+ self.cost = 1
+end
+
+class Start < Flatland
+end
+
+class Goal < Flatland
+end
+
+class Water < Node
+ def walkable?
+ false
+ end
+end
+
+class Forest < Node
+ self.cost = 2
+end
+
+class Mountain < Node
+ self.cost = 3
+end
+
View
@@ -0,0 +1,111 @@
+class Path
+ include MongoMapper::Document
+ include Enumerable # for find
+
+ #many :nodes
+ many :nodes
+
+ def build_nodes
+ @terrain = Terrain.all("$where" => "(this.x >= #{start.x} || this.x <= #{goal.x}) && (this.y >= #{start.y} || this.y <= #{goal.y})")
+ @node_grid = []
+ @width = goal.x - start.x + 1
+ @height = goal.y - start.y + 1
+ @height.times do |y|
+ @node_grid[y] = []
+ @width.times do |x|
+ terrain = @terrain.find {|t| t.contains?(Position.new(start.x + x, start.y + y)) }
+ new_node = Node.by_name(terrain.kind).new(:x => x, :y => y)
+ @node_grid[y] << new_node
+ nodes << new_node
+ end
+ end
+ @node_grid[0][0] = Start.new(:x => 0, :y => 0)
+ @node_grid[@height - 1][@width - 1] = Goal.new(:x => @width - 1, :y => @height - 1)
+ end
+
+ # Returns true if the given position is on the map
+ def contains?(pos)
+ pos.x >= 0 and pos.x < @width and pos.y >= 0 and pos.y < @height
+ end
+
+ # Return node at position
+ def at(pos)
+ @node_grid[pos.y][pos.x]
+ end
+
+ # Iterate all nodes
+ def each
+ @node_grid.each do |row|
+ row.each do |node|
+ yield(node)
+ end
+ end
+ end
+
+ # Iterates through all adjacent nodes
+ def each_neighbour(node)
+ pos = node.position
+ yield_it = lambda{|p| yield(at(p)) if contains? p} # just a shortcut
+ yield_it.call(pos.relative(-1, -1))
+ yield_it.call(pos.relative( 0, -1))
+ yield_it.call(pos.relative( 1, -1))
+ yield_it.call(pos.relative(-1, 0))
+ yield_it.call(pos.relative( 1, 0))
+ yield_it.call(pos.relative(-1, 1))
+ yield_it.call(pos.relative( 0, 1))
+ yield_it.call(pos.relative( 1, 1))
+ end
+
+ def calculate
+ build_nodes
+ map = self
+ start = map.find{|node| Start === node}
+ goal = map.find{|node| Goal === node}
+ open_set = [start] # all nodes that are still worth examining
+ closed_set = [] # nodes we have already visited
+
+ loop do
+ current = open_set.min # find node with minimum cost
+ raise "There is no path from #{start} to #{goal}" unless current
+ map.each_neighbour(current) do |node|
+ if node == goal # we made it!
+ node.parent = current
+ node.mark_path
+ return
+ end
+ next unless node.walkable?
+ next if closed_set.include? node
+ cost = current.cost + node.class.cost
+ if open_set.include? node
+ if cost < node.cost # but it's cheaper from current node!
+ node.parent = current
+ node.cost = cost
+ end
+ else # we haven't seen this node
+ open_set << node
+ node.parent = current
+ node.cost = cost
+ node.cost_estimated = node.position.distance(goal.position)
+ end
+ end
+ # move "current" from open to closed set:
+ closed_set << open_set.delete(current)
+ end
+ self.goal_nodes = nodes
+ end
+
+ def to_s
+ @node_grid.collect{|row|
+ row.collect{|node| node.to_s}.join('')
+ }.join("\n")
+ end
+end
+
+# see http://www.policyalmanac.org/games/aStarTutorial.htm
+#abort "usage: #$0 <mapfile>" unless ARGV.size == 1
+#path = Path.new(File.open(ARGV[0]))
+#path.calculate
+#puts path
+#finder = PathFinder.new
+#finder.find_path(map)
+#puts map
View
@@ -1,3 +1,13 @@
-class Terrain < ActiveRecord::Base
- self.table_name = 'terrain'
+class Terrain
+ include MongoMapper::Document
+
+ key :x, Integer
+ key :y, Integer
+ key :width, Integer
+ key :height, Integer
+ key :kind, String
+
+ def contains?(pos)
+ pos.x >= x and pos.x <= width and pos.y >= y and pos.y <= height
+ end
end
View
@@ -1,5 +1,10 @@
-class User < ActiveRecord::Base
- has_many :animals
+class User
+ include MongoMapper::Document
+
+ key :x, Integer
+ key :y, Integer
+
+ many :animals
def map
Map.new(x,y)
View
@@ -1,14 +1,23 @@
-APP_ROOT = "#{File.dirname(__FILE__)}"
-require 'drb/drb'
require 'rubygems'
-require 'activerecord'
require 'ruby-debug'
-ActiveRecord::Base.logger = Logger.new(STDOUT)
-ActiveRecord::Base.send :include, DRb::DRbUndumped
-ActiveRecord::Base.establish_connection("adapter" => "mysql",
- "database" => "drb",
- "host" => "localhost",
- "username" => "root",
- "password" => "",
- "socket" => "/private/tmp/mysql.sock")
-Dir['lib/*'].each {|file| require file }
+require 'drb/drb'
+require 'mongo_mapper'
+APP_ROOT = "#{File.dirname(__FILE__)}"
+APP_ENV = ENV["APP_ENV"] || "development"
+config = YAML.load_file(APP_ROOT + "/config/database.yml")[APP_ENV]
+
+MongoMapper.connection = Mongo::Connection.new(config['host'], config['port'], {
+ :logger => Logger.new(STDOUT)
+})
+
+MongoMapper.database = config['database']
+if config['username'].present?
+ MongoMapper.database.authenticate(config['username'], config['password'])
+end
+
+Dir[APP_ROOT + '/lib/*.rb'].each do |model_path|
+ require model_path
+#Dir['lib/animal.rb', 'lib/user.rb', 'lib/terrain.rb', 'lib/map.rb'].each do |model_path|
+# File.basename(model_path, '.rb').classify.constantize
+end
+#MongoMapper.ensure_indexes!
View
@@ -1,5 +1,4 @@
require 'main'
-URI="druby://localhost:8787"
class Server
def new_session
@@ -13,6 +12,6 @@ def new_terrain
$SAFE = 0 # disable eval() and friends
-DRb.start_service(URI, Server.new)
+DRb.start_service("druby://localhost:8787", Server.new)
# Wait for the drb server thread to finish before exiting.
DRb.thread.join
Oops, something went wrong.

0 comments on commit 1ee6b99

Please sign in to comment.