[QUIZ] Lost Cities (#51)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

My wife and I love to play a card game called Lost Cities. It's an easy two
player game.

There are five "suits" representing locations in the world: Deserts, Oceans,
Mountains, Jungles, and Volcanoes. Each suit contains three "investment" cards
and one each of the numbers 2 through 10.

Eight cards are dealt to each player, then play alternates turns. On your turn,
you must do exactly two things: Play a card and draw a card, in that order.
When the last card is drawn from the deck, the game ends immediately.

Each play gets an "expedition" pile for each of the five suits, and the players
share five discard piles, again one for each suit. To play a card, you may add
it to your expedition pile for that suit or place it on the discard pile for
that suit.

Your expedition piles must go in order. You may only play a higher card than
the last one you played on that pile. Investment cards are low and you may play
up to all three of them, even though they are the same card. They must still
come before the number cards, of course.

You have two choices when drawing a card. You may take the top card from the
deck or the last card played on any of the five discard piles. You may not
however, discard a card and then draw it again in the same turn.

When the deck is exhausted, both player's scores are calculated based on the
expedition piles. High score wins. (Players generally play a few hands and
keep a running tally.)

An expeditions score is calculated by the following formula:

  points = (total_of_all_number_cards - 20) * (1 + investment_cards_count) +
           bonus_20_points_if_expedition_has_8_cards_or_more

For example, if you had two investment cards and the 6, 8, and 10 in the oceans
suit, that expedition is worth 12 points ((24 - 20) * (1 + 2) + 0). Expeditions
in which you didn't play a card don't count for or against you. They are
disregarded.

That's the whole game. Let's see a few rounds played out, as an example.
First, I'm shown my hand and I can select a card to play:

  Deserts:
    Opponent:
    Discards:
         You:
  Oceans:
    Opponent:
    Discards:
         You:
  Mountains:
    Opponent:
    Discards:
         You:
  Jungles:
    Opponent:
    Discards:
         You:
  Volcanoes:
    Opponent:
    Discards:
         You:
  Deck: ############################################ (44)
  Hand: InvD 2D 3D 5D 2O 5O 9J 5V
  Score: 0 (You) vs. 0 (Opponent). Your play?
  id
  You play the InvD.

Then I am asked where I would like to draw from. I don't really have a choice
yet though, since the discard piles are empty:

  Deserts:
    Opponent:
    Discards:
         You: Inv (-40)
  Oceans:
    Opponent:
    Discards:
         You:
  Mountains:
    Opponent:
    Discards:
         You:
  Jungles:
    Opponent:
    Discards:
         You:
  Volcanoes:
    Opponent:
    Discards:
         You:
  Deck: ############################################ (44)
  Hand: 2D 3D 5D 2O 5O 9J 5V
  Score: -40 (You) vs. 0 (Opponent). Draw from?
  n
  You draw a card from the deck.

Then my opponent gets a turn:

  Your opponent plays the InvD.
  Your opponent draws a card from the deck.

And I get another turn:

  Deserts:
    Opponent: Inv (-40)
    Discards:
         You: Inv (-40)
  Oceans:
    Opponent:
    Discards:
         You:
  Mountains:
    Opponent:
    Discards:
         You:
  Jungles:
    Opponent:
    Discards:
         You:
  Volcanoes:
    Opponent:
    Discards:
         You:
  Deck: ########################################## (42)
  Hand: 2D 3D 5D 2O 5O 6M 9J 5V
  Score: -40 (You) vs. -40 (Opponent). Your play?
  2d
  You play the 2D.
  Deserts:
    Opponent: Inv (-40)
    Discards:
         You: Inv 2 (-36)
  Oceans:
    Opponent:
    Discards:
         You:
  Mountains:
    Opponent:
    Discards:
         You:
  Jungles:
    Opponent:
    Discards:
         You:
  Volcanoes:
    Opponent:
    Discards:
         You:
  Deck: ########################################## (42)
  Hand: 3D 5D 2O 5O 6M 9J 5V
  Score: -36 (You) vs. -40 (Opponent). Draw from?
  n
  You draw a card from the deck.

We continue on like that until the deck is exhausted.

You can get the code I'm using above at:

  http://rubyquiz.com/lost_cities.rb

That code functions as a trivial line-oriented client and server. To play a
card just feed it a card value and suit in the form (?:[i2-9]|10)[domjv]. Add a
"d" to the front of that if you wish to discard instead. To draw, just name a
pile or ask for a "n"ew card from the deck: [domjvn].

This week's Ruby Quiz? To build an AI for playing Lost Cities, of course!

You can tie into my server's very simple API buy defining a subclass of Player
with a show() and move() method. show() is called for each line of data the
server sends to you, and move() is called when the server expects a response.
Here's a very DumbPlayer to get you going:

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

  class DumbPlayer < Player
    def initialize
      @data = ""

      @plays = nil
      @discard = nil
    end

    def show( game_data )
      if game_data =~ /^You (?:play|discard)/
        @plays = nil
        @discard = nil
      end

      @data << game_data
    end

    def move
      if @data.include?("Draw from?")
        draw_card
      else
        make_move
      end
    ensure
      @data = ""
    end

    private

    def draw_card
      "n"
    end

    def make_move
      if @plays.nil? and @data =~ /Hand: (.+?)\s*$/
        @plays = $1.split.map { |card| card.sub(/Inv/, "I") }
        @discard = "d#{@plays.first}"
      end

      if @plays.empty?
        @discard
      else
        @plays.shift
      end
    end
  end

If you save that as dumb_player.rb, you could play against it with something
like:

  $ ruby lost_cities.rb 9016
  
  ... then in a different terminal ...
  
  $ ruby lost_cities.rb localhost 9016 dumb_player.rb

Let the games begin!

(partially) related to that:

   There is some more info about the game (including rules [incl. italian and
   spanish translations]) at:

     http://www.boardgamegeek.com/viewitem.php3?gameid=50

Benedikt

   ALLIANCE, n. In international politics, the union of two thieves who
     have their hands so deeply inserted in each other's pockets that
     they cannot separately plunder a third.
       (Ambrose Bierce, The Devil's Dictionary)

I've developed a "helper" module to assist those working on Lost Cities AI's. When you include this module in your Player class, it adds a number of attributes and methods to help you with the current game state. It automatically determines what cards are in your hand, which cards have been played, which cards are known to be in your opponents hand (because they were picked up from the discard piles). It can tell which cards are playable or not (by you or by your opponent), and more.

It's designed to work seamlessly with James' lost_cities.rb game engine and with Daniel Sheppard's harness.

player_helper.rb:
# = PlayerHelper

···

#
# include this module in your player class to provide
# parsing of the game data provided through the show
# method.
#
# Your player class needs to provide two methods:
#
# play_card - called when it's your turn to play a card.
# return the card to play, or 'd' + card to
# discard a card.
# draw_card - called when it's your turn to draw a card.
# return the pile to draw from [domjv], or 'n'
# to draw from the deck.
#
# The default methods implement the DumbPlayer logic, so the
# simplest player would be:
#
# require 'player_helper'
# class SimplePlayer < Player
# include PlayerHelper
# end
#

module PlayerHelper

   # Last error message returned from engine, or nil if no error
   attr_reader :error

   # Hash by land. Each entry is an Array of Game::Card's discarded
   # for that land.
   attr_reader :discards

   # Array of "unseen" Game::Card's. These are either in the deck or in
   # the opponents hand (but not seen by the current player)
   attr_reader :unseen

   # Number of cards still available in the deck
   attr_reader :deck

   # Current player's hand (Array of Game::Card's)
   attr_reader :my_hand

   # Hash by land for current player. Each entry is an Array of
   # Game::Card's played to that land.
   attr_reader :my_lands

   # Cards *known* to be in opponent's hand (Array of Game::Card's).
   # These are determined by the discards the opponent picks up. Cards
   # that the opponent was initially dealt or have drawn from the deck
   # will appear in :unseen
   attr_reader :op_hand

   # Hash by land for Opponent. Each entry is an Array of
   # Game::Card's played to that land.
   attr_reader :op_lands

   @@echo = false

   def self.included(klass)

     # enables echoing of game data from engine
     def klass.echo_on
       @@echo = true
     end

     # disables echoing of game data from engine
     def klass.echo_off
       @@echo = false
     end

   end

   # intializes game state data
   def initialize
     super
     @op_hand = Array.new
     @my_hand = Array.new
     @unseen = Array.new
     @op_lands = Hash.new
     @discards = Hash.new
     @my_lands = Hash.new
     Game::LANDS.each do |land|
       @op_lands[land] = Array.new
       @discards[land] = Array.new
       @my_lands[land] = Array.new
     end
     moveover
     gameover
   end

   # draws one or more cards in readable format
   def draw_cards(*cards)
     cards.flatten.map {|c| c.to_s}.join(' ')
   end

   # clears some game state data when game ends. helpful when the
   # same player object is used for multiple games.
   def gameover
     op_hand.clear
   end

   def show( game_data )
     puts game_data.chomp if @@echo
     game_data.strip!
     if game_data =~ /^(\S+):confused: && @my_lands.has_key?($1.downcase)
       @land = $1.downcase
       return
     end
     case game_data
       when /Hand:\s+(.+?)\s*$/
         my_hand.replace($1.split.map { |c| Game::Card.parse(c) })
       when /Opponent:(.*?)(?:\(|$)/
         op_lands[@land].replace($1.split.map { |c|
           Game::Card.parse("#{c}#{@land[0,1]}") })
       when /Discards:(.*?)(?:\(|$)/
         discards[@land].replace($1.split.map { |c|
           Game::Card.parse("#{c}#{@land[0,1]}") })
       when /You:(.*?)(?:\(|$)/
         my_lands[@land].replace($1.split.map { |c|
           Game::Card.parse("#{c}#{@land[0,1]}") })
       when /Your opponent (?:plays|discards) the (\w+)/
         c = Game::Card.parse($1)
         i = op_hand.index(c)
         op_hand.delete_at(i) if i
       when /Your opponent picks up the (\w+)/
         op_hand << Game::Card.parse($1)
       when /Draw from\?/
         @action = :draw_card
       when /Your play\?/
         @action = :play_card
       when /^Error:/
         @error = game_data
       when /Deck:.*?(\d+)/
         @deck = $1
       when /Game over\./
         gameover
       else
         #puts "Unhandled game_data: #{game_data}"
     end
   end

   def move
     find_unseen if error.nil?
     send(@action)
   ensure
     moveover
   end

   # returns a full deck of cards
   def full_deck
     Game::LANDS.collect do |land|
       (['Inv'] * 3 + (2 .. 10).to_a).collect do |value|
         Game::Card.new(value, land)
       end
     end.flatten
   end

   # after all the board data has been received, determines
   # which cards from the deck have not yet been seen. these
   # are either in the deck or known to be in the opponent's hand.
   def find_unseen
     unseen.replace(full_deck)
     (my_hand + op_hand + my_lands.values +
       op_lands.values + discards.values).flatten.each do |c|
       i = unseen.index(c) or next
       unseen.delete_at(i)
     end
   end

   def moveover
     @error = nil
   end

   # naive draw method: always draws from deck
   # (override this in your player)
   def draw_card
     "n"
   end

   # naive play method: plays first playable card in hand,
   # or if no legal play, just discards the first card in
   # the hand.
   # (override this in your player)
   def play_card
     card = @my_hand.find { |c| live?(c) }
     return card.to_play if card
     "d" + @my_hand.first.to_play
   end

   # returns true if card is playable on given lands. cards
   # that are not live can never be played, so are just dead
   # weight in your hand (although they may be useful to your
   # opponent; you can check this with live?(card, op_lands).)
   def live?(card, lands = @my_lands)
     lands[card.land].empty? or lands[card.land].last <= card
   end

end

# extend the Game::Card class with some helpers
class Game::Card

   # define a comparison by rank and land.
   # useful for sorting hands, etc.
   include Comparable
   def <=>(other)
     result = value.to_i <=> other.value.to_i
     if result == 0
       result = land <=> other.land
     end
     result
   end

   # returns true if two cards have same land
   def same_land?(other)
     land == other.land
   end

   # parse a card as shown by Game#draw_cards back to a
   # Game::Card object. Investment cards can be specified
   # as 'I' or 'Inv'.
   def self.parse(s)
     value, land = s.strip.downcase.match(/(.+)(.)/).captures
     if value =~ /^i(nv)?$/
       value = 'Inv'
     else
       value = value.to_i
       value.between?(2,10) or raise "Invalid value"
     end
     land = Game::LANDS.detect {|l| l[0,1] == land} or
       raise "Invalid land"
     new(value, land)
   end

   # converts a card to its string representation (value + land)
   def to_s
     "#{value}#{land[0,1].upcase}"
   end

   # converts a card to its play representation
   def to_play
     "#{value.is_a?(String) ? value[0,1] : value}#{land[0,1]}".downcase
   end

end

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

    class DumbPlayer < Player
      def initialize

         super

        @data = ""

        @plays = nil
        @discard = nil
      end

# ...

The above addition allows DumbPlayer to play from the server side as well as the client. This is an oversight on my part. Sorry.

James Edward Gray II

···

On Oct 14, 2005, at 7:34 AM, Ruby Quiz wrote:

A really simple idea occurred to me. Just discard the whole time. I
know it's pretty silly but against other AI's it might work. Against a
human not terribly challenging but you do have to score more than 0.

discard_player.rb:

class DiscardPlayer < Player
  def initialize
    @data = ""
    super
  end

  def show( game_data )
    @data << game_data
  end

  def move
    if @data.include?("Draw from?")
      "n"
    else
      if @data =~ /Hand: (.+?)\s*$/
        "d#{$1.split.first.sub(/nv/,"")}"
      end
    end
  ensure
    @data = ""
  end
end

Well, it's not brilliant yet, but I've run out of time to keep tweaking the risk analysis. Here's my passible first crack at a solution.

Hopefully someone will step in with a player that slaughters him...

James Edward Gray II

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

class RiskPlayer < Player
   def self.card_from_string( card )
     value, land = card[0..-2], card[-1, 1].downcase
     Game::Card.new( value[0] == ?I ? value : value.to_i,
                     Game::LANDS.find { |l| l[0, 1] == land } )
   end

   def initialize
     @piles = Hash.new do |piles, player|
       piles[player] = Hash.new { |pile, land| pile[land] = Array.new }
     end

     @deck_size = 60
     @hand = nil

     @last_dicard = nil

     @action = nil
   end

   def show( game_data )
     if game_data =~ /^(Your?)(?: opponent)? (play|discard)s? the (\w+)/
       card = self.class.card_from_string($3)
       if $2 == "play"
         if $1 == "You"
           @piles[:me][card.land] << card
         else
           @piles[:them][card.land] << card
         end
       else
         @piles[:discards][card.land] << card
       end

       @last_discard = nil if $1 == "Your"
     end

     if game_data =~ /^\s*Deck:\s+#+\s+\((\d+)\)/
       @deck_size = $1.to_i
     end
     if game_data =~ /^\s*Hand:((?:\s+\w+)+)/
       @hand = $1.strip.split.map { |c| self.class.card_from_string(c) }
     end

     if game_data.include?("Your play?")
       @action = :play_card
     elsif game_data.include?("Draw from?")
       @action = :draw_card
     end
   end

   def move
     send(@action)
   end

   private

   def play_card
     plays, discards = @hand.partition { |card| playable? card }

     if plays.empty?
       discard_card(discards)
     else
       risks = analyze_risks(plays)
       risk = risks.max { |a, b| a.last <=> b.last }

       return discard_card(@hand) if risk.last < 0

       land = risks.max { |a, b| a.last <=> b.last }.first.land
       play = plays.select { |card| card.land == land }.
                       sort_by { |c| c.value.is_a?(String) ? 0 : c.value }.first
       "#{play.value}#{play.land[0, 1]}".sub("nv", "")
     end
   end

   def discard_card( choices )
     discard = choices.sort_by do |card|
       [ playable?(card) ? 1 : 0, playable?(card, :them) ? 1 : 0,
         card.value.is_a?(String) ? 0 : card.value ]
     end.first

     @last_discard = discard
     "d#{discard.value}#{discard.land[0, 1]}".sub("nv", "")
   end

   def draw_card
     want = @piles[:discards].find do |land, cards|
       not @piles[:me][land].empty? and
       cards.last != @last_discard and cards.any? { |card| playable?(card) }
     end
     if want
       want.first[0, 1]
     else
       "n"
     end
   end

   def analyze_risks( plays )
     plays.inject(Hash.new) do |risks, card|
       risks[card] = 0

       me_total = ( @piles[:me][card.land] +
                    plays.select { |c| c.land == card.land }
                    ).inject(0) do |total, c|
         if c.value.is_a? String
           total
         else
           total + c.value
         end
       end
       risks[card] += 20 - me_total

       them_total = @piles[:them][card.land].inject(0) do |total, c|
         if c.value.is_a? String
           total
         else
           total + c.value
         end
       end
       high = card.value.is_a?(String) ? 2 : card.value
       risks[card] += ( (high..10).inject { |sum, n| sum + n }
                        - (me_total + them_total) ) / 2

       if @piles[:me][card.land].empty?
         lands_played = @piles[:me].inject(0) do |count, (land, cards)|
           if cards.empty?
             count
           else
             count + 1
           end
         end

         risks[card] -= (lands_played + 1) * 5
       end

       risks
     end
   end

   def playable?( card, who = :me )
     @piles[who][card.land].empty? or
     @piles[who][card.land].last.value.is_a?(String) or
     ( not card.value.is_a?(String) and
       @piles[who][card.land].last.value < card.value )
   end
end

__END__

Bob Showalter wrote:

I've developed a "helper" module to assist those working on Lost Cities AI's.

Erm, that echo_on/echo_off thing didn't work they way I wanted it to. Here's an attempt to fix it. What I'm trying to do is to let you do this:

    class MyPlayer < Player
      include PlayerHelper

      echo_on # enable echoing

      def play_card
         ...blah blah
      end
    end

So I want echo_on to be a class or module method, and have a class variable in MyPlayer that tracks the echo flag. My first version had a single echo flag shared across all classes that included PlayerHelper. Being a clueless Ruby noob, I'm probably going about it the wrong way.

I don't think I'm smart enought to create an actual AI, but this much has been fun...

Here's the new version:

player_helper.rb:
# = PlayerHelper

···

#
# include this module in your player class to provide
# parsing of the game data provided through the show
# method.
#
# Your player class needs to provide two methods:
#
# play_card - called when it's your turn to play a card.
# return the card to play, or 'd' + card to
# discard a card.
# draw_card - called when it's your turn to draw a card.
# return the pile to draw from [domjv], or 'n'
# to draw from the deck.
#
# The default methods implement the DumbPlayer logic, so the
# simplest player would be:
#
# require 'player_helper'
# class SimplePlayer < Player
# include PlayerHelper
# end
#

module PlayerHelper

   # Last error message returned from engine, or nil if no error
   attr_reader :error

   # Hash by land. Each entry is an Array of Game::Card's discarded
   # for that land.
   attr_reader :discards

   # Array of "unseen" Game::Card's. These are either in the deck or
   # in the opponents hand (but not seen by the current player)
   attr_reader :unseen

   # Number of cards still available in the deck
   attr_reader :deck

   # Current player's hand (Array of Game::Card's)
   attr_reader :my_hand

   # Hash by land for current player. Each entry is an Array of
   # Game::Card's played to that land.
   attr_reader :my_lands

   # Cards *known* to be in opponent's hand (Array of Game::Card's).
   # These are determined by the discards the opponent picks up. Cards
   # that the opponent was initially dealt or have drawn from the deck
   # will appear in :unseen
   attr_reader :op_hand

   # Hash by land for Opponent. Each entry is an Array of
   # Game::Card's played to that land.
   attr_reader :op_lands

   def self.included(klass)

     # enables echoing of game data from engine
     def klass.echo_on
       @echo = true
     end

     # disables echoing of game data from engine
     def klass.echo_off
       @echo = false
     end

   end

   # intializes game state data
   def initialize
     super
     @op_hand = Array.new
     @my_hand = Array.new
     @unseen = Array.new
     @op_lands = Hash.new
     @discards = Hash.new
     @my_lands = Hash.new
     Game::LANDS.each do |land|
       @op_lands[land] = Array.new
       @discards[land] = Array.new
       @my_lands[land] = Array.new
     end
     moveover
     gameover
   end

   # draws one or more cards in readable format
   def draw_cards(*cards)
     cards.flatten.map {|c| c.to_s}.join(' ')
   end

   # clears some game state data when game ends. helpful when the
   # same player object is used for multiple games.
   def gameover
     op_hand.clear
   end

   def show( game_data )
     puts game_data.chomp if self.class.class_eval "@echo"
     game_data.strip!
     if game_data =~ /^(\S+):confused: && @my_lands.has_key?($1.downcase)
       @land = $1.downcase
       return
     end
     case game_data
       when /Hand:\s+(.+?)\s*$/
         my_hand.replace($1.split.map { |c| Game::Card.parse(c) })
       when /Opponent:(.*?)(?:\(|$)/
         op_lands[@land].replace($1.split.map { |c|
           Game::Card.parse("#{c}#{@land[0,1]}") })
       when /Discards:(.*?)(?:\(|$)/
         discards[@land].replace($1.split.map { |c|
           Game::Card.parse("#{c}#{@land[0,1]}") })
       when /You:(.*?)(?:\(|$)/
         my_lands[@land].replace($1.split.map { |c|
           Game::Card.parse("#{c}#{@land[0,1]}") })
       when /Your opponent (?:plays|discards) the (\w+)/
         c = Game::Card.parse($1)
         i = op_hand.index(c)
         op_hand.delete_at(i) if i
       when /Your opponent picks up the (\w+)/
         op_hand << Game::Card.parse($1)
       when /Draw from\?/
         @action = :draw_card
       when /Your play\?/
         @action = :play_card
       when /^Error:/
         @error = game_data
       when /Deck:.*?(\d+)/
         @deck = $1
       when /Game over\./
         gameover
       else
         #puts "Unhandled game_data: #{game_data}"
     end
   end

   def move
     find_unseen if error.nil?
     send(@action)
   ensure
     moveover
   end

   # returns a full deck of cards
   def full_deck
     Game::LANDS.collect do |land|
       (['Inv'] * 3 + (2 .. 10).to_a).collect do |value|
         Game::Card.new(value, land)
       end
     end.flatten
   end

   # after all the board data has been received, determines
   # which cards from the deck have not yet been seen. these
   # are either in the deck or known to be in the opponent's hand.
   def find_unseen
     unseen.replace(full_deck)
     (my_hand + op_hand + my_lands.values +
       op_lands.values + discards.values).flatten.each do |c|
       i = unseen.index(c) or next
       unseen.delete_at(i)
     end
   end

   def moveover
     @error = nil
   end

   # naive draw method: always draws from deck
   # (override this in your player)
   def draw_card
     "n"
   end

   # naive play method: plays first playable card in hand,
   # or if no legal play, just discards the first card in
   # the hand.
   # (override this in your player)
   def play_card
     card = @my_hand.find { |c| live?(c) }
     return card.to_play if card
     "d" + @my_hand.first.to_play
   end

   # returns true if card is playable on given lands. cards
   # that are not live can never be played, so are just dead
   # weight in your hand (although they may be useful to your
   # opponent; you can check this with live?(card, op_lands).)
   def live?(card, lands = @my_lands)
     lands[card.land].empty? or lands[card.land].last <= card
   end

end

# extend the Game::Card class with some helpers
class Game::Card

   # define a comparison by rank and land.
   # useful for sorting hands, etc.
   include Comparable
   def <=>(other)
     result = value.to_i <=> other.value.to_i
     if result == 0
       result = land <=> other.land
     end
     result
   end

   # returns true if two cards have same land
   def same_land?(other)
     land == other.land
   end

   # parse a card as shown by Game#draw_cards back to a
   # Game::Card object. Investment cards can be specified
   # as 'I' or 'Inv'.
   def self.parse(s)
     value, land = s.strip.downcase.match(/(.+)(.)/).captures
     if value =~ /^i(nv)?$/
       value = 'Inv'
     else
       value = value.to_i
       value.between?(2,10) or raise "Invalid value"
     end
     land = Game::LANDS.detect {|l| l[0,1] == land} or
       raise "Invalid land"
     new(value, land)
   end

   # converts a card to its string representation (value + land)
   def to_s
     "#{value}#{land[0,1].upcase}"
   end

   # converts a card to its play representation
   def to_play
     "#{value.is_a?(String) ? value[0,1] : value}#{land[0,1]}".downcase
   end

end

To work in Daniel's harness, RiskPlayer needs to call super in the
initialize method also.

···

On 10/19/05, James Edward Gray II <james@grayproductions.net> wrote:

On Oct 14, 2005, at 7:34 AM, Ruby Quiz wrote:

> #!/usr/local/bin/ruby -w
>
> class DumbPlayer < Player
> def initialize

         super

> @data = ""
>
> @plays = nil
> @discard = nil
> end

# ...

The above addition allows DumbPlayer to play from the server side as
well as the client. This is an oversight on my part. Sorry.

James Edward Gray II

Ok, here's my best effort. It's not particularly pretty code...

My first idea was to build a bunch of rules based on notes I took
while playing the game:
"save high runs. play sequential cards right away. play inv early,
or hold til have more cards.
check opponent plays to recognize cards you shouldn't wait for"

But the rules were getting more and more complicated to code. So I
simplified and made a bunch of rules that assigned 1 or 0 to each card
based on simple facts
inSequence, lowCard, holding10points, useless2me..

Then I added weights for each rule, and used them to rank the cards
along 2 axes: Play..Hold and Keep..Discard. The card in the hand
with the biggest value is then played or discarded.
There are actually 2 sets of weights, one for early in the game, and
one for late in the game.

Then I played a bunch of games, and hand tuned the rules to try to
prevent stupid choices.

My next goal was to fill an arena with players and have an
evolutionary process - winners replace losers with a child with a
randomly modified weight; repeat until one dominates. But I haven't
had made much progress this way yet. So here's my original
hand-tuned version. It consistently beats risk_player and
discard_player. It beat me once or twice. But it can only beat
dumb_player 2/3rds of the time...

-Adam

ads_lc_player.rb (12.9 KB)

I saw Daniel Sheppard's solution after I posted mine. I'm impressed.
I modified my arena based on some ideas from his breeder and tried
to evolve my ruleset all day. I'm not sure it got much better, but I
think it beats me more often.

To run, add these 2 files to the same directory as the other one

-EvolvedPlayer.rb------

require 'ads_lc_player'

class EvolvedPlayer < ADS_LC_Player
  def initialize
    super
    load "gene.yaml"
  end

    #fix for bug in ADS_LC_Player where it would draw from a discard
    #right after playing on that suit and making the wanted card useless
    def draw_card
        @dwanted.each_with_index{|w,i|
            if w && (@dpile[i][-1] > (@land[i][-1]||0))
                @dpile[i].pop
                return [S.index(i)].pack("C")
            end
        }
        @deckcount-=1
        "n"
    end

end

···

On 10/19/05, Adam Shelly <adam.shelly@gmail.com> wrote:

My next goal was to fill an arena with players and have an
evolutionary process - winners replace losers with a child with a
randomly modified weight; repeat until one dominates.

-------

-gene.yaml-----

--- !ruby/object:Gene
drules:
  :rule_noPartners:
    - -0.3
    - -0.3
  :rule_useless2me:
    - -0.5
    - 0.1
  :rule_wantFromDiscard:
    - 0.3
    - 0.5
  :rule_useless2him:
    - -0.2
    - 0.1
  :rule_belowLowestPlayable:
    - -0.2
    - 0.0
  :rule_useful2him:
    - 0.4
    - 0.5
  :rule_dontDiscardForever:
    - 0.5
    - 1
  :rule_useful2me:
    - 0.3
    - 0.424953141133301
  :rule_singleton:
    - -0.269603526452556
    - -0.1
  :rule_heHasPlayed:
    - 0.1
    - 0.3
name: hgklnqu
parent: hgklnq
prules:
  :rule_highestInHand:
    - -0.1
    - -0.0132171112811193
  :rule_investments:
    - 0.10915231165709
    - -0.214938909909688
  :rule_suitStarted:
    - 0.7
    - 0.9
  :rule_group15:
    - 0.6
    - -0.338061462133191
  :rule_inSequence:
    - 0.654255492263474
    - 0.905814079008997
  :rule_heHasPlayed10:
    - -0.2
    - 0.0
  :rule_2followsInvest:
    - 0.3
    - 0.5
  :rule_onInvestments:
    - 0.5
    - 0.7
  :rule_closeToPrevious:
    - 0.571291934791952
    - 0.5
  :rule_group20:
    - 0.7
    - -0.24812678352464
  :rule_lowCard:
    - 0.1
    - 0.0
  :rule_heHasPlayed20:
    - -0.3
    - 0.0
  :rule_multiplier2:
    - 0.4
    - 0.8
  :rule_holdingInvestments:
    - -0.458828900489379
    - 0.0
  :rule_finishGame:
    - 0.0
    - 2.06754154443775
  :rule_handNegative:
    - 0.5
    - 0.9
  :rule_group25:
    - 0.9
    - -0.17002231310729
  :rule_lowCards:
    - 0.253758069175279
    - 0.0
  :rule_possibleBelow:
    - -0.2
    - -0.0697354671487119
  :rule_multiplier3:
    - 0.649637156224344
    - 1.07591888221214
  :rule_investmentWithHope:
    - 0.726016686472576
    - 0.425265068525914
  :rule_total20:
    - 0.35
    - 1.0
  :rule_mustPlays:
    - -0.32988673124928
    - 1.0
  :rule_highCard:
    - -0.3
    - 0.1
  :rule_investmentWithoutHope:
    - -0.6
    - -1.0
  :rule_possibleManyBelow:
    - -0.46776403458789
    - -0.1
  :rule_onUnplayed:
    - -0.819403649667397
    - -1.0
  :rule_total25:
    - 0.6
    - 1.0
  :rule_highCards:
    - -0.2
    - 0.2
  :rule_lowerInHand:
    - -0.88520639540273
    - -0.4
  :rule_heHasPlayed:
    - -0.14072827748023
    - 0.0
  :rule_group10:
    - 0.61925460502971
    - -0.4

-----