From: Ruby Quiz Date: 2006-03-23T22:53:03+09:00 Subject: [SUMMARY] Space Merchant (#71) 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 Bamford 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 Bennett: # ... 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 Bennett'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...