-- Wf1wk1t+okM/SedwJ7S
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
Here is my Galaxy implementation. It seems to do everything it needs to
do, but I didn't go overboard with extra stuff since I don't know what
kind of stuff might be useful for the Galaxy to do...
The easiest way to use this with the GalaxyLoader seems to be to remove
the 'require galaxy' from GalaxyLoader and do it the other way around.
Then you can run the tests including the dump test with:
ruby -rgalaxy_loader galaxy.rb
There are some naive benchmarks I used to make the obvious
optimizations, run them with:
ruby galaxy.rb -bm
One additional thing I did consider was multiplayer - it's fairly easy
to get a galaxy working over DRb, and I did work up a basic multiplayer
version like this but had one overriding problem - all output ends up on
the server, or the game ends up useless because of constantly
marshalling sectors to send down the wire. I toyed with routing output
through the Player but still had problems... Ho hum.
Finally, I need to thank a couple of people: Dominik Bathon's solution
to Ruby Quiz 31 ( see
http://www.ruby-talk.org/cgi-bin/scat.rb/ruby/ruby-talk/141783 ) gave me
a much-needed start with Dijkstra's shortest path algorithm (in fact
it's only very slightly modified from his original) and Mauricio
Fernandez's WeakHash class let's me cache stuff irresponsibly (see
http://eigenclass.org/hiki.rb?weakhash+and+weakref )
--
Ross Bamford - rosco / roscopeco.REMOVE.co.uk
-- Wf1wk1t+okM/SedwJ7S
Content-Disposition: attachment; filename=galaxy.rb
Content-Type: text/plain; name=galaxy.rb; charset=utf-8
Content-Transfer-Encoding: 7bit
require 'singleton'
unless $0 __FILE__
require 'sector'
require 'planet'
require 'station'
end
# The implementation of Dijkstra's shortest path algorithm I use for
# find_path and find_reachable was lifted directly from Dominik Bathon's
# solution to quiz 31 (thanks!) and only modified very slightly to suit
# our purpose. This Hash modification comes with that.
class Hash #:nodoc: all
# find the key for with the smallest value, delete it and return it
def delete_min_value
return nil if empty?
minkey n l
each { |k, v|
min, minkey k if !min || v<min
}
delete(minkey)
minkey
end
end
module SpaceMerchant #:nodoc:
# alaxy for Ruby Quiz 71.
#
# Requires:
# * Sector.new(name, location il)
# * Sector#link(to_sector)
# * Sector#links [Sector:1, ..., Sector:N]
# * Sector#planets [Planet:1, ..., Planet:N]
# * Sector#stations [Station:1, ..., Station:N]
# * Sector#add_planet(planet)
# * Sector#add_station(station)
#
# * Planet.new(name, location il)
# * Station.new(name, location il)
#
# * All classes need #name String
# * All classes should define in terms of name *at* *least*
#
# Extra features:
# * Simplistic galaxy generation
# * Galaxy codes allow whole galaxy to be regenerated
# * Support for long-distance route availablility by distance
# * Compatible with Jacob Fugal's GalaxyLoader
# * Able to dump galaxies in GalaxyLoader format
#
class Galaxy
include Singleton
# internal stuff
TRUEBLOCK ambda { true } #:nodoc:
ATOZ 'A'..'Z').to_a #:nodoc:
class << self
# tired of writing 'Galaxy.instance' in tests...
def method_missing(sym, *args, &blk) #:nodoc:
instance.send(sym, *args, &blk)
end
end
# This WeakHash implementation is by Mauricio Fernandez. I
# just added support for a default, and full key reclaimation
# to suit our need to have the default block run as and when
# (but only when) necessary.
#
# See: http://eigenclass.org/hiki.rb?weakhash+and+weakref
class WeakHash #:nodoc: all
attr_reader :cache
def initialize( cache ash.new, &initializer )
@cache ache
@initializer nitializer
@key_map }
@rev_cache ash.new{|h,k| h[k] }}
@reclaim_value ambda do |value_id|
if @rev_cache.has_key? value_id
@rev_cache[value_id].each_key{|key| @cache.delete key}
@rev_cache.delete value_id
end
end
@reclaim_key ambda do |key_id|
if @key_map.has_key? key_id
@cache.delete @key_map[key_id]
@key_map.delete key_id
end
end
end
def []( key )
value_id cache[key.hash]
if value_id.nil? && @initializer
self[key] initializer.call(self, key)
value_id cache[key.hash]
end
return ObjectSpace._id2ref(value_id) unless value_id.nil?
nil
end
def [] key, value )
case key
when Fixnum, Symbol, true, false
key2 ey
else
key2 ey.hash
end
@rev_cache[value.object_id][key2] rue
@cache[key2] alue.object_id
@key_map[key.object_id] ey2
ObjectSpace.define_finalizer(value, @reclaim_value)
ObjectSpace.define_finalizer(key, @reclaim_key)
end
end
attr_reader :sectors, :code
def initialize #:nodoc:
@sectors ]
@pathcache eakHash.new do |h, key|
start, avoid ey
h[key.freeze] uild_prev_hash(start, avoid)
end
@code il
end
# Change the code for the galaxy. This will cause the
# whole galaxy to be regenerated, from the specified code,
# with the current galaxy lost, so be careful.
#
# Pretty much equivalent to +generate+
def code
ode)
generate(code)
end
# Add a sector.
def add_sector(sector)
sectors << sector
end
# Get the starting location for this galaxy. If a galaxy
# hasn't been generated (or loaded), this will have a
# random one generated.
def starting_location
generate if @sectors.empty?
sectors.first
end
# select sectors with the supplied block
def find_sectors(&blk)
sectors.select(&(blk || TRUEBLOCK)) || []
end
# select stations with the supplied block
def find_stations(&blk)
sectors.inject([]) do |a, e|
a.push(*e.stations.select(&(blk || TRUEBLOCK)))
end
end
# select planets with the supplied block
def find_planets(&blk)
sectors.inject([]) do |a, e|
a.push(*e.planets.select(&(blk || TRUEBLOCK)))
end
end
# find the shortest path between start and finish (sectors).
# Optionally supply an array of sectors to avoid. Returns
# an array of sectors or nil if no path can be found.
def find_path(start, finish, *avoid_sectors)
shortest_path(start, finish, avoid_sectors.flatten)
end
# find all sectors reachable from start, optionally avoiding
# specified sectors, and imposing a maximum journey 'cost'
# (in terms of number of steps from start).
def find_reachable(start, avoid_sectors il, maxcost il)
all_paths(start,avoid_sectors,maxcost)
end
# Generation is very simple, basically making up a tangled
# mess of randomly connected sectors. You can pass in a code
# that specifies some parameters used for the generator,
# allowing the same galaxy to be generated again by supplying
# the same code.
#
# You should call this manually before using the galaxy, or
# let +starting_location+ handle that for you if you don't
# want to load a galaxy or anything.
#
# Returns the new galaxy code.
def generate(code il)
# bit odd but gives us a good seed - one call to randomize,
# one to get the value of that previous random seed, and
# then set to that.
size, max_links, max_planets, seed decode(code) || [(500 + rand(500)), 5, 5, srand && srand]
clear
srand(seed)
@sectors en_bodies(max_planets, gen_sectors(size, max_links))
# seed rand again, don't want to affect rand in game
srand
@code ncode(size, max_links, max_planets, seed)
end
# Vape the galaxy, clears everything.
def clear
initialize
end
# Dump this galaxy to a string in the same format as used by
# Jacob Fugal's GalaxyLoader.
def dump
locs ectors.inject({}) { |h,s| (h[s.location || nil] || ]) << s; h }
dump galaxy {\n"
locs.each do |loc, sectors|
dump << " region(#{loc.inspect}) {\n" if loc
sectors.each do |sector|
dump << " sector(#{SpaceMerchant::Galaxy.sectors.index(sector) + 1}) {\n"
sector.planets.each do |planet|
dump << " planet #{planet.name.inspect}\n"
end
sector.stations.each do |station|
dump << " station #{station.name.inspect}\n"
end
unless sector.links.empty?
dump << " neighbors "
dump << sector.links.map { |s| Galaxy.sectors.index(s) + 1 }.join(', ')
dump << "\n"
end
dump << " }\n"
end
dump << " }\n" if loc
end
dump << "}\n"
end
alias :to_a :sectors
private
# these handle the galaxy codes
def decode(code)
if code /^(\d{4})(\d)(\d)([\da-fA-F]+)$/
[$1.to_i, $2.to_i, $3.to_i, $4.hex]
end
end
def encode(size, max_links, max_planets, seed)
size.to_s.rjust(4,'0') <<
max_links.to_s[0,1] <<
max_planets.to_s[0,1] <<
sprintf('%x',seed)
end
# This whole generation bit is currently sloppy and slow
def gen_sectors(size, max_links)
sectors 0...size).map { |i| Sector.new("Sector #{i}") }
avail ectors.dup
sectors.each do |sector|
rand(max_links).times do
to vail[rand(avail.length)]
sector.link(to)
to.link(sector)
avail.delete(to) if to.links.length > ax_links
end
end
unlinked ectors.select { |e| e.links.empty? }
# just makes sure the last few sectors are reachable. Gives
# a nice varying density to space I think :)
unless unlinked.empty?
remain ectors - unlinked
unlinked.each do |sector|
break unless to emain[rand(remain.length)] # to hell with it then
sector.link(to)
to.link(sector)
end
end
sectors
end
def gen_body_name
(0..(rand(3)+1)).inject("") { |s,i| s << ATOZ[rand(26)] } +
rand(5000).to_s +
ATOZ[rand(26)]
end
def gen_bodies(max_planets, sectors)
avail ectors.dup
((avail.length / 3) + rand(avail.length / 2)).to_i.times do
sect vail.delete_at(rand(avail.length))
station and > 0.5
planets and(max_planets)
sect.add_station(Station.new(sect, gen_body_name)) if station
planets.times { sect.add_planet(Planet.new(sect, gen_body_name)) }
end
sectors
end
# returns a hash that associates each reachable (from start)
# position p, with the previous position on the shortest path
# from start to p and the length of that path.
# example: if the shortest path from 0 to 2 is [0, 1, 2], then
# prev[2] 1, 2], prev[1] 0, 1] and prev[0] nil, 0].
# so you can get all shortest paths from start to each reachable
# position out of the returned hash.
# stop_at isn't taken into account here, since it's better for us
# to build full sets we can cache for future use, rather than
# cutting short and having a lot of less useful path sets.
def build_prev_hash(start, avoid_sectors il)
avoid_sectors || ]
prev tart nil, 0]} # hash to be returned
# positions which we have seen, but we are not yet sure about
# the shortest path to them (the value is length of the path,
# for delete_min_value):
active tart }
until active.empty?
# get the position with the shortest path from the
# active list
curive.delete_min_value
newlength ev[cur][1]+1 # path to cur length + 1
# for all reachable neighbors of cur, check if we found
# a shorter path to them
cur.links.each { |n|
# skip sectors we're avoiding
next if avoid_sectors.include?(n)
if old ev[n] # was n already visited
# if we found a longer path, ignore it
next if newlength>