Space Merchant (#71)

On Tue, 2006-03-21 at 02:07 +0900, Joel VanderWerf wrote:

The setup I had in the end was: (server) Galaxy as front object,
Sectors, Planets and Stations DRbUndumped so they stay on that side too,
with Player marshalled. I wanted to have Player unmarshalled too, but on
the client side, but I couldn’t figure out how to make that work (I
wanted to keep that client side and route output through a method on
it).

Anyway, the problem with that was that, since Sector handles most of the
output (and will probably just call puts) the output all goes to the
server’s terminal :frowning:

I guess I just don’t know DRb all that well yet…

On Tue, 2006-03-21 at 02:48 +0900, James Edward G. II wrote:

On Mar 20, 2006, at 11:41 AM, Ross B. wrote:

Anyway, the problem with that was that, since Sector handles most
of the
output (and will probably just call puts) the output all goes to the
server’s terminal :frowning:

Could you have the client pass you $stdout and $stdin, then set them
on the server?

I had a go with this, but couldn’t get it to work. However, in the
process I figured out what I was doing wrong and got it working (I
think), so thanks :slight_smile:

Two disclaimers: This is only basic, and untested, and I’ve not used DRb
until this so it’s probably not as well put together as it might be…

On Mar 20, 2006, at 11:10 AM, Timothy B. wrote:

I was thinking that they’d be stored in player[:cargo] and player
[:passengers]

I haven’t had time to introduce ships yet, so I went with pretty much
what you said. Here’s a very basic Station class to get this sucker
feeling more like a game:

#!/usr/local/bin/ruby -w

begin
require “highline/import”
rescue LoadError
begin
require “rubygems”
require “highline/import”
rescue LoadError
puts “#{FILE} requires HighLine be installed.”
exit 1
end
end

class Numeric
def commify
to_s.reverse.gsub(/(\d\d\d)(?=\d)(?!\d*.)/, ‘\1,’).reverse
end
end

module SpaceMerchant
class Station
Good = Struct.new(:name, :cost)
GOODS = [ Good.new(:plants, 0.5),
Good.new(:animals, 0.8),
Good.new(:food, 1),
Good.new(:luxuries, 1.2),
Good.new(:medicine, 2),
Good.new(:technology, 3) ]

 def initialize( sector, name )
   @sector, @name = sector, name

   @goods = GOODS.sort_by { rand }[0..2].sort_by { |good|

good.cost }.
map { |good| good.dup }.
map { |g| [g, [:buy, :sell][rand(2)], rand
(10_000) + 1] }
@goods.each { |good| good.first.cost *= rand + 0.5 }
end

 attr_reader :sector, :name

 def handle_event( player )
   player[:cargo_space] ||= 20
   player[:cargo]       ||= Array.new

   puts "Welcome pilot.  Come to do some trading?  What'll it be?

\n\n"

   credits = player[:credits].commify.sub(/\.(\d+)$/) { |d| d

[0…2] }
puts “Credits: #{credits}”
if player[:cargo].empty?
puts " Cargo: none\n\n"
else
cargo = player[:cargo].map do |g|
“#{g.first.to_s.capitalize} (#{g.last})”
end.join(", “)
puts " Cargo: #{cargo}\n\n”
end

   choose do |menu|
     menu.index = :none
     menu.shell = true
     menu.case  = :capitalize

     menu.prompt = "Make an offer or blast off?  "

     printf "%10s %7s %5s %6s\n", "Item".center(10),

“Trade”.center(7),
“Price”, “Amount”
puts “---------- ------- ----- ------”
@goods.each do |good|
if good.include? :buy
menu.choice( sprintf( “%-10s Buying %1.2f”,
good.first.name.to_s.capitalize,
good.first.cost ) ) do |good,
details|
sell_goods(
player,
@goods.find { |g| g.first.name == good[/\w
+/].downcase.to_sym },
details.split
)

           puts "You unload the goods and blast off from the

station…"
player[:location] = sector
end
else
menu.choice( sprintf( “%-10s Selling %1.2f %6s”,
good.first.name.to_s.capitalize,
good.first.cost,
good.last.commify ) ) do |good,
details|
buy_goods(
player,
@goods.find { |g| g.first.name == good[/\w
+/].downcase.to_sym },
details.split
)

           puts "You load up the goods and blast off from the

station…"
player[:location] = sector
end
end
end

     menu.choice("Blast off") { player[:location] = sector }
   end
 end

 private

 def buy_goods( player, good, details )
   can_afford = [ good.last,
                  (player[:credits] * good.first.cost).to_i,
                  player[:cargo_space] -
                  player[:cargo].inject(0) { |sum, item|

item.last } ].min
if can_afford == 0
puts “I don’t think you are in any position to be buyin’.”
return
end

   amount = if details.first.nil? or details.first.to_i > can_afford
     ask("How much?  ", Integer) { |q| q.in = (1..can_afford) }
   else
     details.shift.to_i
   end

   player[:credits] -= good.first.cost * amount
   if add_on = player[:cargo].find { |g| g.first ==

good.first.name }
add_on[-1] += amount
else
player[:cargo] << [good.first.name, amount]
end

   reset_good(good, amount)
 end

 def sell_goods( player, good, details )
   begin
     max_sale = player[:cargo].find { |g| g.first ==

good.first.name }.last
rescue
puts “Uh, you don’t have any of that to sell Mac.”
return
end

   amount = if details.first.nil? or details.first.to_i > max_sale
     ask("How much?  ", Integer) { |q| q.in = (1..max_sale) }
   else
     details.shift.to_i
   end

   player[:credits] += good.first.cost * amount
   player[:cargo].find { |g| g.first == good.first.name }[-1] -=

amount

   reset_good(good, amount)
 end

 def reset_good( good, amount )
   if (good[-1] -= amount) <= 0
     good[1..2] = [([:buy, :sell] - [good[1]]).first, rand

(10_000) + 1]
end
end
end
end

if FILE == $PROGRAM_NAME
player = {:credits => 1000}

loop do
if player[:location].nil?
player[:location] = SpaceMerchant::Station.new(nil, “Test”)
end

 player[:location].handle_event(player)

end
end

END

James Edward G. II

On Mar 20, 2006, at 3:34 PM, James Edward G. II wrote:

On Mar 20, 2006, at 11:10 AM, Timothy B. wrote:

I was thinking that they’d be stored in player[:cargo] and player
[:passengers]

I haven’t had time to introduce ships yet, so I went with pretty
much what you said.

Ok, great, I’ll work with that then.

Tim

On Mar 20, 2006, at 3:00 PM, Ross B. wrote:

I had a go with this, but couldn’t get it to work.

Bummer. Here is where I got the idea, just FYI:

http://ian.blenke.com/drb

James Edward G. II

On Tue, 2006-03-21 at 08:45 +0900, James Edward G. II wrote:

Could you have the client pass you $stdout and $stdin, then set them
on the server?

I had a go with this, but couldn’t get it to work.

Bummer. Here is where I got the idea, just FYI:

http://ian.blenke.com/drb

Ahh, I see. It looks like we had pretty similar concepts of how it
should work, but my stdout/stdin references kept on turning into
DRb::DRbUnknown instances after I made the first call on them. The other
problem was that, because we have to handle multiple players and sectors
use ‘puts’ for output, I had to keep the right bits for each player
somewhere and redefine puts to take account of that. Fortuntely DRb
seems to do thread-per-client so I could store it there.

Doing that, though, I realised it might be nicer to route everything
through the player after all - the output setup is currently completely
transparent from the game classes’ point of view, so could be easily
changed e.g. to use a different output style, or for
internationalization (instead of marshalling the messages, give each
message a number that is sent over the wire, and looked up on the client
to get the right message).

Also, since the player is provided by the client, different clients
could be using different output formats or whatever with the server
having no knowledge of that :slight_smile:

On Tue, 2006-03-21 at 17:56 +0900, Ross B. wrote:

should work, but my stdout/stdin references kept on turning into
DRb::DRbUnknown instances after I made the first call on them.

Okay, I have this working now after a fashion, but still can’t get it to
work with the station implementation - I don’t know highline well, but
looking through it seems to use readline, termios, or something else on
Windows and I’m not sure we can do anything about that…

So currently it can play multiplayer, as long as you don’t dock with a
station (which causes the server to wait for input!) :frowning:

Have to say, though, that playing in one-player is actually pretty good,
it’s coming together well guys :slight_smile: I think I need to make some
adjustments to the galaxy generation, not just because it sometimes
generates those galaxies with a couple of interlinked, unreachable
sectors, but also because it feels a bit sparse in terms of planets and
stations…

On Tue, 2006-03-21 at 13:18 +0900, Timothy B. wrote:

Later, also, if people are interested, we can extend Jacob’s
GalaxyLoader to include item definitions.

Oh, and I also added a description attribute for the planet, which
should probably also be incorporated into GalaxyLoader.

Maybe we could work it with a block on planet/station initialize, so
that in the .glx file it’d look like:

planet(“HUY38M”) do
description = “whatever”
something = “whichever”
end

(GalaxyLoader planet and GalaxyLoader station would need to pass the
block, of course)

To make it work both ways, I think we’d need to extend the current
Galaxy#dump to call through to a new method, #dump, on each planet and
station (may as well do sector to, and have the whole dump handled this
way?). This method is responsible for outputting the ruby code above
at dump time. Thus, planets/stations can support whatever they like by
generating as they need to.

?

On Mar 21, 2006, at 2:56 AM, Ross B. wrote:

Also, since the player is provided by the client, different clients
could be using different output formats or whatever with the server
having no knowledge of that :slight_smile:

This is a great point. Good ideas all around. I believe you’re
right that this would be the correct way to handle multi-player.

James Edward G. II

Ok, here’s my initial planet.rb. I had meant to put people on the
planet, but I didn’t finish that. I did, however, set up a mechanism
for finding items which can later be used (a block is attached to the
item’s .new, which determines the item’s effect). This is incomplete
still (only really works if there’s only one item of each type), but
I wanted to post this since it should still be fun for you guys to
see. Hopefully I’ll have time to add some more to this tomorrow.

By the way, the test (activated by running planet.rb by itself - but
it still depends on Sector), includes an item Omega. Explore for a
while (chances of finding it are about 1 in 10), and use it :slight_smile:

Later, also, if people are interested, we can extend Jacob’s
GalaxyLoader to include item definitions.

Oh, and I also added a description attribute for the planet, which
should probably also be incorporated into GalaxyLoader.

Tim

On Mar 21, 2006, at 8:47 AM, Ross B. wrote:

Have to say, though, that playing in one-player is actually pretty
good,
it’s coming together well guys :slight_smile:

Yeah, this was a fun little experiment. I’m glad we were able to get
it going.

James Edward G. II

On Thu, 2006-03-23 at 00:22 +0900, Mauricio F. wrote:

On Mon, Mar 20, 2006 at 05:41:56PM +0900, Ross B. wrote:

# This WeakHash implementation is by Mauricio F.. 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 

I found your message indirectly via my httpd logs :slight_smile:

I meant to put a thanks in the message body after I decided the code was
too long to post inline, but I forgot (sorry about that). Glad you saw
it, and thanks for the wicked implementation :slight_smile:

class WeakHash #:nodoc: all

[…]

  def []( key )
    value_id = @cache[key.hash]
                        ========

This will fail when you have hash collisions; I think it should be ‘key’
as in the original code (same goes for []=).

Ahh, of course. I think collisions are less likely given the specific
usage of WeakHash in this case (arrays with strictly object_id based
hashes, of objects that outlive the hash), but of course still possible
so I’ll fix that up later today.

Cheers,

On Mon, Mar 20, 2006 at 05:41:56PM +0900, Ross B. wrote:

# This WeakHash implementation is by Mauricio F.. 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 

I found your message indirectly via my httpd logs :slight_smile:

class WeakHash #:nodoc: all

[…]

  def []( key )
    value_id = @cache[key.hash]
                        ========

This will fail when you have hash collisions; I think it should be ‘key’
as in the original code (same goes for []=).

Forcing collisions is not as difficult as one could believe:

class WeakHash #:nodoc: all
attr_reader :cache
def initialize( cache = Hash.new, &initializer )
@cache = cache
@initializer = initializer
@key_map = {}
@rev_cache = Hash.new{|h,k| h[k] = {}}
@reclaim_value = lambda 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 = lambda 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 = key
  else
key2 = key.hash
  end
  @rev_cache[value.object_id][key2] = true
  @cache[key2] = value.object_id
  @key_map[key.object_id] = key2

  ObjectSpace.define_finalizer(value, @reclaim_value)
  ObjectSpace.define_finalizer(key, @reclaim_key)
end

end

RUBY_VERSION # => “1.8.4”
hash = WeakHash.new
hash[[“a”]] = “some value”
hash[[“a”]] # => “some value”
hash[“c”] # => “some value”

On Thu, 2006-03-23 at 01:08 +0900, Ross B. wrote:

[…] I’ll fix that up later today.
For completeness, here is the final galaxy.rb with this bug fixed. I
still didn’t get time to redo the generation though but there you go…

I know this is 11th-hour and all, but I thought I’d mention it
anyways. When I read this quiz, I immediately thought of using a
Dependency Injection framework to solve it. I also wanted to add
elements of the Firefly `Verse to the gameplay, so I’d describe it in
a YAML file. Unfortunately, I haven’t had the time to do more than
just a mock-up:

registry = Needle::Registry.new do |reg|

reg.register( :galaxy ) { Galaxy.new( 'firefly.yaml' ) }
reg.intercept( :galaxy ).with! { logging_interceptor }
reg.register( :sector, :model => :multiton ) do |c,p,name,location|
  Sector.new( name, location )
end
reg.intercept( :sector ).with! { logging_interceptor }
reg.register( :planet, :model => :multiton ) do |c,p,name,sector|
  Planet.new( name, sector )
end
reg.intercept( :planet ).with! { logging_interceptor }
reg.register( :station, :model => :multiton ) do |c,p,name,sector|
  Station.new( name, sector )
end
reg.intercept( :station ).with! { logging_interceptor }
reg.register( :player, :model => :multiton ) do |c,p,name|
  Player.new( name )
end
reg.intercept( :player ).with! { logging_interceptor }

end

galaxy = registry.galaxy

firefly.yaml (partial):


galaxy: firefly
player:
credits : 1000
reputation : 100
sectors:

  • name : 1
    location : central
    planets:
    • name : Ariel
      description : “Planet. Home to a major medical center, Saint
      Lucy’s.”
  • name : 2
    location : central
    planets:
    • name : Persephone
      description : Planet. Heavily stratified societal structure.
      Resupply port for Serenity.

I had envisioned this as a webapp, where players could register and
all play within the same universe. The Authenticator and Presenter
aren’t shown above. Each player would then be instatiated like so:

= registry.player("")

They could save the game state to a YAML file, which would then be
transportable across SpaceMerchant implementations.

So, what do you all think? Anybody with actual DI/IoC experience care
to chime in? (As you can probably tell, this would be my first time
using DI.) Are there any other Browncoats out there? Could this
maybe be a Part 2 to this quiz?

  • Dimitri

This is awsome. I’ve been thinking about writing a tradewars server for
a month or so now. Unfortunately with school and everything else, I
haven’t had much time to devote to the idea. I hope people come up with
neat solutions. I wish I had the time to devote to this quiz, (I don’t
unfortunately), but if this quiz results in a project being spun off, I
would love to become a part of it.

My original idea was to try to write a multi-threaded telnet server in
ruby to power the game, and allow players to play the game
simultaneously, allowing for in game chat and ship to ship
communications and whatnot. (much like the latest version of tradewars
2002 game server from Epic Interactive www.eisonline.com/twgs/.

At any rate, good luck to everyone, I’m definately interested in the
results.

I would be willing to help. I have been thinking for months on a way to
implement a game in the verse. Let me know what’s going on, until then
I will just drift out here in the black.

On Mar 22, 2006, at 1:18 PM, Gary W. wrote:

My original idea was to try to write a multi-threaded telnet server in
ruby to power the game, and allow players to play the game
simultaneously, allowing for in game chat and ship to ship
communications and whatnot. (much like the latest version of
tradewars
2002 game server from Epic Interactive www.eisonline.com/twgs/.

At any rate, good luck to everyone, I’m definately interested in the
results.

On Mar 22, 2006, at 9:43 PM, Dimitri A. wrote:

maybe be a Part 2 to this quiz?
I dunno about continuing with this as a quiz, but I think it’d be
cool to see a project spin off of this. Anyone want to take the lead
and set up a project on RubyForge? I personally won’t have a great
deal of time to put into this (and I’m still rather wet behind the
ears as a Ruby programmer), but I still wouldn’t mind setting it up.

Tim

On Mar 22, 2006, at 11:43 PM, Dimitri A. wrote:

I had envisioned this as a webapp, where players could register and
all play within the same universe.

I almost made the quiz like this, but changed my mind to keep
things a little easier. It’s a good idea.

James Edward G. II