[SOLUTION] Lost Cities (#51)

Hmmm.... tried running your client through my tester, and it ends up
trying to draw cards from a pile that it can't, resulting in an infinite
loop of "There are no cards there" errors. This could be due to
line-wrapping in the email though - I already had to fix up this code:

       cards.last != @last_discard and cards.any? { |card| playable?
(card) }

So that it was all on one line, otherwise it tries to call playable with
no args.

(to make your player work with my tester, I also changed all references
of "@hand" to "@my_hand").

You player works successfully against the dumb_player - I guess that's
how you tested it. But it doesn't play against itself or my player
(which I'll post in a couple of hours).

When you designed your game, you probably should have kept the player
intelligence separate from the player record keeping. I probably would
have also put the human-readable rendering of the output in the client
and made the server just spit out the data - but I'm guessing the
original focus of your game was PvP, and these suggestions only really
affect the design in the AI situation.

···

-----Original Message-----
From: James Edward Gray II [mailto:james@grayproductions.net]
Sent: Wednesday, 19 October 2005 8:18 AM
To: ruby-talk ML
Subject: Re: [SOLUTION] Lost Cities (#51)

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__

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

Hmmm.... tried running your client through my tester, and it ends up
trying to draw cards from a pile that it can't, resulting in an infinite
loop of "There are no cards there" errors.

I didn't test my player using your tester, so it may not work there.

You player works successfully against the dumb_player - I guess that's
how you tested it.

Na, I played against it.

When you designed your game, you probably should have kept the player
intelligence separate from the player record keeping.

Well, I did design it for the players to be at opposite ends of a socket, communicating through a protocol.

I do agree that I made a few mistakes in the design though. Sorry about that.

James Edward Gray II

···

On Oct 18, 2005, at 7:18 PM, Daniel Sheppard wrote:

Hi Daniel and James-

Hmmm.... tried running your client through my tester, and it ends up
trying to draw cards from a pile that it can't, resulting in an infinite
loop of "There are no cards there" errors. This could be due to
line-wrapping in the email though - I already had to fix up this code:

       cards.last != @last_discard and cards.any? { |card| playable?
(card) }

So that it was all on one line, otherwise it tries to call playable with
no args.

(to make your player work with my tester, I also changed all references
of "@hand" to "@my_hand").

Thanks for those fixes. I found the other problem. Risk player keeps
track of the discards and his last discard but never notices if he
picks one up. Below are the fixes I used to get it to run. But I
think I might have changed it's behavior.

> From: James Edward Gray II [mailto:james@grayproductions.net]
> Sent: Wednesday, 19 October 2005 8:18 AM
> To: ruby-talk ML
> Subject: Re: [SOLUTION] Lost Cities (#51)
>
> 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

@piles[:discard][discard.land].delete 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 (not cards.find { |card| @my_hand.include? card } ) and

···

On 10/18/05, Daniel Sheppard <daniels@pronto.com.au> wrote:

> -----Original Message-----
> cards.last != @last_discard and cards.any? { |card| playable?
> (card) }
> end
> if want
> want.first[0, 1]
> else
> "n"
> end
> end

Oh, good find. That's a bug.

Actually, he never notices if ANYONE picks up a discard. Oops.

Attached is a fixed version, zipped to prevent wrapping issues.

My thanks to you both, for helping me fix this.

James Edward Gray II

risk_player.zip (1.41 KB)

···

On Oct 19, 2005, at 9:18 AM, Anthony Moralez wrote:

Thanks for those fixes. I found the other problem. Risk player keeps
track of the discards and his last discard but never notices if he
picks one up.