Forum: Ruby Space Merchant (#71)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
James G. (Guest)
on 2006-03-23 15:53
(Received via mailing list)
The primary focus of this quiz, for me, was to see how well a handful of
developers could quickly throw something together, without much
knowledge of
what the other guys were doing.  I think it went very well.  I forgot
several
basic things in my specification (like how Planet and Station needed a
name()
accessor), but convention and common sense seemed to get us through with
little
trouble.  Too cool.

Obviously, I can't show all the code that was written this week.
Instead, I
will try to hit on some highlights.

	Development in Isolation

Since we were each just building a part of the whole, one of the big
questions
became, how do I test my part?  I built the Station, which doesn't
really
require much from the other pieces.  Sector and Galaxy help you move
from
Station to Station, but that turns out to be trivial to bypass.  I
really just
needed to pretend I was docking at Station after Station.  To do that, I
added
the following code to the end of station.rb:

	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

The idea here is that when you lift off from a Station, you will go back
into
the Sector.  So if we pass Station some Sector substitute that is easy
to watch
for, nil for example, we can just replace that object with a newly
constructed
Station whenever we see it.  This simulates flying from Sector to
Sector,
docking at Stations.

Some pieces depended on the others more heavily though, requiring more
complete
solutions for testing.  Ross B. built Galaxy, which requires at
least a
minimal representations of the other celestial objects.  Ross solved
this by
mocking the other objects with the needed functionality:

	  if $0 == __FILE__
	    # The comparable stuff is needed only by the tests,
	    # not the Galaxy impl itself.
	    class Named #:nodoc: all
	      def initialize(sector, name); @name = name.to_s; end
	      def name; @name; end
	      alias :to_s :name
	      def inspect; "#{self.class.name}:#{@name}"; end
	      def ==(o); name == o.name; end
	      def <=>(o); name <=> o.to_s; end
	    end

	    class Sector < Named #:nodoc: all
	      def initialize(name, location = nil)
	        super(nil, name)
	        @location, @planets, @stations, @links = location, [], [], []
	      end
	      attr_accessor :location, :planets, :stations, :links
	      def add_planet(planet); @planets << planet; end
	      def add_station(station); @stations << station; end
	      def link(o); @links << o; end
	      def ==(o)
	        begin
	          name == o.name &&
	            planets == o.planets &&
	            stations == o.stations &&
	            links.length == o.links.length
	        rescue NoMethodError
	          false
	        end
	      end
	    end

	    class Planet < Named #:nodoc: all
	    end

	    class Station < Named #:nodoc: all
	    end

	    # ...

Ross started with the minimal Named functionality that all objects
share.  I
would have provided this in the quiz, if I was as smart as Ross.  From
there,
Ross just adds in the functionality Galaxy requires.  Note how unused
details
(like the sector parameter to new()) are just casually ignored.  The
goal is to
build only what is needed to test the Galaxy implementation.

	The Singleton Shortcut

I know how we all love a good method_missing() trick, so here's my
favorite for
this week, again from Ross:

	  class Galaxy
	    include Singleton

	    # ...

	    class << self
	      # tired of writing 'Galaxy.instance' in tests...
	      def method_missing(sym, *args, &blk) #:nodoc:
	        instance.send(sym, *args, &blk)
	      end
	    end

	    # ...

Obviously, there are other solutions to the problem the comment
describes, but
this particular trick made for a nice interface, I thought.  Observe:

	Galaxy.instance.find_planets { |planet| ... }
	# ... becomes...
	Galaxy.find_planets { |planet| ... }

That might come in handy with other uses of Singleton, I think.

	The Big Event

Another detail of this quiz the solvers had to work with was how do
handle
events.  Here's a handle_event() method for Sector, by Timothy B.:

	    # ...

	    def handle_event ( player )
	      player[:visited_sectors] ||= []
	      player[:visited_sectors] << self \
	        unless player[:visited_sectors].find { |sector| sector == self
}
	      print_menu
	      choice = gets.chomp
	      case choice
	      when /d/i: choose_station
	      when /l/i: choose_planet
	      when /p/i: plot_course
	      when /q/i: throw(:quit)
	      when /\d+/: warp player, choice
	      else invalid_choice
	      end
	    end

	    # ...

Aside from the elegant menu dispatch at the end of the method, the main
point of
interest is the first line.  We all had to add our individual elements
to the
Player object as needed, which required a little defensive programming.
When
the Player first arrives in a Sector, there is no :visited_sectors key
(the game
script doesn't create one).  This is probably a sign that I should have
provided
an initialization hook in the quiz, but optional assignments like the
above
still might have been needed for things not known in advance.  Luckily
the ||=
operator is just perfect for this kind of work.

I won't show all the all of the event methods used above, but here is
one of
them:

		  # ...

		  def choose_station
		    player = Player.instance
		    puts "There are no stations to dock with!" if @stations.empty?
		    if @stations.size == 1
		      dock @stations[0], player
		    else
		      @stations.each_with_index do |station, index|
		        puts "(#{index + 1}) #{station.name}"
		      end
		      puts "Enter the number of the station to dock with: "

		      station_index = gets.chomp.to_i - 1
		      if @stations[station_index]
		        dock @stations[station_index], player
		      else
		        puts "Invalid station."
		      end
		    end
		  end

		  # ...

I really liked how this method would just intelligently make the choice,
if
there was only one, or prompt the user when a decision needed to be
made.  This
made for a better playing experience for sure.

	Manufacturing Fun and Destruction

The final aspect of this quiz was, of course, innovation.  I left the
specification very open in the hopes that someone would grab the ball
and run...

	  class UsableItem
	    attr_reader :rarity, :name, :description

	    def initialize (name, description = "", rarity = 0.7, &block)
	      @effect = block if block_given?
	      @name = name
	      @description = description
	      @rarity = rarity
	    end

	    def use (player)
	      if @effect
	        @effect.call player
	      else
	        puts "#{name} has no effect."
	      end
	    end

	    def to_s
	      name
	    end
	  end

Obviously, that is just a name, description, and rarity attached to a
block
(from Timothy B.'s planet.rb), but just look at this example of the
earth-shattering fun to be had with an object like this:

	  # ...

	  omega = SpaceMerchant::UsableItem.new( "Omega",
	                                         "Don't push that button.
Please.",
	                                         0.9 ) do |player|
	    planet = player[:location]
	    player[:location] = planet.sector
	    puts
	    puts "You hear a terrible rumbling as the Vogon constructor fleet"
	    puts "descends upon #{planet.name}.  You scramble to your"
	    puts "ship and launch just in time to avoid becoming space dust."
	    puts
	    player[:location].planets.slice!(player[:location].planets.index(planet))
	  end

	  # ...

I love it.

A big thank you to all who played with my pet project.  Hopefully you
didn't
blow up your planet doing so.

Tomorrow we will continue our focus on essential Ruby programming skills
with
Breaking and Entering 101...
Ross B. (Guest)
on 2006-03-23 17:15
(Received via mailing list)
On Thu, 2006-03-23 at 22:53 +0900, Ruby Q. wrote:
> The primary focus of this quiz, for me, was to see how well a handful of
> developers could quickly throw something together, without much knowledge of
> what the other guys were doing.  I think it went very well.

Definitely agree :) Thanks for another fun and (for me, at least)
educational quiz, and for the nice write-up. This one fair took me back
to my days as a sysop (though I don't actually recall running TradeWars,
only the enormous phone bills that came of all that echomail :)).
This topic is locked and can not be replied to.