[QUIZ] Lost Cities (#51)

Just had 10 minutes to look at this, and created a quick test harness to
compare players before I go poking around trying to figure out what kind
of logic to use.

Pass in the list of ruby files containing players, optionally preceeded
with a count for the number of games to play, and also a random seed (to
keep consistent results between runs).

All players will be given the same game to play with (the same set of
random seeds will be used).

Make sure you call "super" in your player's init to get it setup
properly.

Hope this encourages more entries.

require 'lost_cities'
require 'nano/enumerable/each_combination'

class Player
  @@players = []
  def self.inherited( subclass )
    @@players << subclass unless subclass == SocketPlayer
  end
  def self.players
    @@players
  end
end
class Game
    public :score
end

count = 10
count = ARGV.shift.to_i if /^\d*$/=== ARGV[0]
srand(ARGV.shift.to_i) if /^\d*$/=== ARGV[0]

ARGV.each { |file| require file } if ARGV.length > 0

RecordStruct = Struct.new( :type, :wins, :scores )
records = Hash.new { |h,k| h[k] = RecordStruct.new(k.to_s, 0, []) }

seeds = Array.new(count) { rand }

play_proc = lambda do |players|
    p "Playing #{players.inspect}"
    players.map! { |x| x.new }
    seeds.each do |seed|
        srand(seed)
        game = Game.new(*players)
        until game.over?
            game.rotate_player
            game.play
            game.draw
        end
        scores = players.map{ |player| game.score(player.lands)
}.zip(players.map {|player| player.class})
        scores = scores.sort_by { |a,b| a }
        scores.each { |a,b| records[b].scores << a }
        records[scores[-1][1]].wins += 1
    end
end

#~ if(Player.players.size > 1)
    #~ Player.players.each_combination(2, &play_proc)
#~ else
    Player.players.map{ |x| [x,x] }.each(&play_proc)
#~ end

puts
printf("%-30s %4s %5s %5s %5s", "Class", "Wins", "Avg.", "Min.",
"Max.")
puts
records.values.sort_by {|r| -r.wins}.each { |record|
    printf("%-30s %4d %2.2f %2.2f %2.2f",
        record.type,
        record.wins,
        record.scores.inject(0) { |s,x| s + x }
/record.scores.size.to_f,
        record.scores.min,
        record.scores.max)
}

···

#####################################################################################
This email has been scanned by MailMarshal, an email content filter.
#####################################################################################

Daniel Sheppard wrote:

Just had 10 minutes to look at this, and created a quick test harness to
compare players before I go poking around trying to figure out what kind
of logic to use.

Daniel, this is awesome! I'm learning a ton just studying your code and James'.

There's a small bug in James' lost_cities.rb that affects your program. Game.new does not clear the player's hand (it does clear everything else). So if a player object is reused to play a second game, he ends up with 16 cards in his hand, and the game is messed up.

As a quick fix, I just added:

   players.map { |x| x.hand.clear }

before the call to Game.new() in your code.

Daniel Sheppard wrote:

Just had 10 minutes to look at this, and created a quick test harness to
compare players before I go poking around trying to figure out what kind
of logic to use.

OK, one more minor glitch with the harness. The random number seeding doesn't work correctly. I get the exact same deck with every game.

seeds = Array.new(count) { rand }

play_proc = lambda do |players|
    p "Playing #{players.inspect}"
    players.map! { |x| x.new }
    seeds.each do |seed|
        srand(seed)

seeds is intialized with a list of nubers 0 <= n < 1. These are then later passed to Kernel.srand. However, srand's argument is converted using to_i, so all these calls are equivalent to srand(0).

My fix is:

   seeds = Array.new(count) { rand(1 << (8 * 1.size))}

I'm trying to generate random seeds over the range of a Fixnum. Is there a more appropriate way to do that?

I've found another glitch that affects the automation of players with Daniel's harness.

In James' dumb_player.rb, the following line captures the cards in the players hand:

  if @plays.nil? and @data =~ /Hand: (.+?)\s*$/

The regex captures up to the end of the line containing "Hand:". When you play this in the client-server mode, each line passed to show() end with a newline (since :show is delegated to :puts), and so the regex works.

When you play this under Daniel's harness, the lines passed to show() don't have newlines added, since each player directly calls the opponent's show(). The last card is immediately followed by the next line starting with "Score:", and so the regex captures extraneous "cards" (and the eighth card is incorrectly parsed as "8VScore:", for example).

One way to resolve this is to change the dumb_player.rb line:

   @data << game_data

To:

   @data << game_data.chomp << "\n"

That way, you get the same data when you play through the client-server mode or through the harness.

I wonder how this would best be handled inside lost_cities.rb or the harness itself?

Yes, Game.new() expects to be given fresh players. Good catch.

James Edward Gray II

···

On Oct 17, 2005, at 9:24 AM, Bob Showalter wrote:

Daniel Sheppard wrote:

Just had 10 minutes to look at this, and created a quick test harness to
compare players before I go poking around trying to figure out what kind
of logic to use.

Daniel, this is awesome! I'm learning a ton just studying your code and James'.

There's a small bug in James' lost_cities.rb that affects your program. Game.new does not clear the player's hand (it does clear everything else). So if a player object is reused to play a second game, he ends up with 16 cards in his hand, and the game is messed up.

As a quick fix, I just added:

  players.map { |x| x.hand.clear }

before the call to Game.new() in your code.

Let's not go changing the protocol on people. It's expected to be a line oriented network protocol, so each line should end in a "\n".

James Edward Gray II

···

On Oct 17, 2005, at 10:15 AM, Bob Showalter wrote:

I've found another glitch that affects the automation of players with Daniel's harness.

In James' dumb_player.rb, the following line captures the cards in the players hand:

if @plays.nil? and @data =~ /Hand: (.+?)\s*$/

The regex captures up to the end of the line containing "Hand:". When you play this in the client-server mode, each line passed to show() end with a newline (since :show is delegated to :puts), and so the regex works.

When you play this under Daniel's harness, the lines passed to show() don't have newlines added, since each player directly calls the opponent's show(). The last card is immediately followed by the next line starting with "Score:", and so the regex captures extraneous "cards" (and the eighth card is incorrectly parsed as "8VScore:", for example).

One way to resolve this is to change the dumb_player.rb line:

  @data << game_data

To:

  @data << game_data.chomp << "\n"

That way, you get the same data when you play through the client-server mode or through the harness.

I wonder how this would best be handled inside lost_cities.rb or the harness itself?