# \[SUMMARY\] Lost Cities (#51)

**URL:** <https://rubytalk.org/t/summary-lost-cities-51/21743>\
**Category:** ruby-talk\
**Created:** [20 October 2005 13:01 UTC](https://rubytalk.org/t/summary-lost-cities-51/21743 "2005-10-20T13:01:07Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![James\_Edward\_Gray\_II](https://avatars.discourse-cdn.com/v4/letter/j/ea5d25/32.png) [@James\_Edward\_Gray\_II](https://rubytalk.org/u/James_Edward_Gray_II)\
**Post date:** [20 October 2005 13:01 UTC](https://rubytalk.org/t/summary-lost-cities-51/21743/1 "2005-10-20T13:01:07Z")

</div>

There's nothing too tough in this quiz, but it turned out to be pretty time  
consuming for me. Not because it required a ton of code for a solution, but  
because I kept playing against my solution and tweaking its behavior. Of  
course, as Daniel Sheppard pointed out, I should have tried him against  
DumbPlayer a little more:

&nbsp;&nbsp;$ ruby lost\_cities.rb localhost 61676 risk\_player.rb  
&nbsp;&nbsp;Final Score: -21 (You) vs. -60 (Opponent).  
&nbsp;&nbsp;Congratulations, you win.  
&nbsp;&nbsp;$ ruby lost\_cities.rb localhost 61676 risk\_player.rb  
&nbsp;&nbsp;Final Score: -32 (You) vs. -1 (Opponent).  
&nbsp;&nbsp;I'm sorry, you lose.  
&nbsp;&nbsp;$ ruby lost\_cities.rb localhost 61676 risk\_player.rb  
&nbsp;&nbsp;Final Score: -43 (You) vs. -43 (Opponent).  
&nbsp;&nbsp;The game is a draw.  
&nbsp;&nbsp;$ ruby lost\_cities.rb localhost 61676 risk\_player.rb  
&nbsp;&nbsp;Final Score: -51 (You) vs. 5 (Opponent).  
&nbsp;&nbsp;I'm sorry, you lose.

"You" above would be RiskPlayer and "Opponent" is DumbPlayer. I guess  
DumbPlayer isn't so dumb and RiskPlayer doesn't take enough risks.

There were also some issues with the server and DumbPlayer that may have made it  
harder for people to mess around with this problem. I apologize for that.

Daniel's own solution is very interesting, but quite a bit of code to show here.  
Let me see if I can hit a highlight or two. First, I'll let Daniel explain how  
the code was built:

&nbsp;&nbsp;Designing an AI for a game which you've never played is a pretty  
&nbsp;&nbsp;daunting task... So I let the computer do the work with a little bit  
&nbsp;&nbsp;of genetics.  
&nbsp;&nbsp;  
&nbsp;&nbsp;The rule\_player.rb file contains the basic framework for parsing the  
&nbsp;&nbsp;input and storing the knowledge and also the rules, as well as an  
&nbsp;&nbsp;extra player named "MultiplierPlayer" which allows the rules to be  
&nbsp;&nbsp;given different weightings  
&nbsp;&nbsp;  
&nbsp;&nbsp;The breeder.rb file contains the breeding system. It creates a bunch  
&nbsp;&nbsp;of MultiplierPlayers with different weightings and plays them against  
&nbsp;&nbsp;each other round-robin style. The 2 players with the most wins under  
&nbsp;&nbsp;their belts get bred to form extra players and the worst players get  
&nbsp;&nbsp;dropped. The players are then saved off in a yaml file. Being yaml,  
&nbsp;&nbsp;it's easy to edit, so if you think you know better than the breeder,  
&nbsp;&nbsp;it's easy to throw your figures into the race.  
&nbsp;&nbsp;  
&nbsp;&nbsp;...

That YAML file is really neat. Here's a peek at a small slice of it:

> **···**
>
> ---  
> &nbsp;&nbsp;-  
> &nbsp;&nbsp;&nbsp;&nbsp;-  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- 1  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- 1  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- 0  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- 0  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- 0  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- 0  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- 0  
> &nbsp;&nbsp;# ...  
> &nbsp;&nbsp;-  
> &nbsp;&nbsp;&nbsp;&nbsp;- Rules::PlayLowestFirst  
> &nbsp;&nbsp;&nbsp;&nbsp;- Rules::MaximumScoreEndGame  
> &nbsp;&nbsp;&nbsp;&nbsp;- Rules::IgnoreUnusable  
> &nbsp;&nbsp;&nbsp;&nbsp;- Rules::DiscardUnusable  
> &nbsp;&nbsp;&nbsp;&nbsp;- Rules::DepriveOpponent  
> &nbsp;&nbsp;&nbsp;&nbsp;- Rules::AvoidLateInvestment  
> &nbsp;&nbsp;&nbsp;&nbsp;- Rules::ExpectedInvestmentValue
> 
> The bottom section has the rules that the genetics system is considering. The  
> top Array lists the weights the winner settled on, favoring the first two rules  
> clearly.
> 
> The player that puts the data to use is trivial:
> 
> &nbsp;&nbsp;require 'rule\_player'  
> &nbsp;&nbsp;require 'yaml'  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;class BredPlayer \< MultiplierPlayer  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;def initialize  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;raise "No Data File" unless File.exist?('data2.yaml')  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;multipliers, rule\_names = File.open('data2.yaml','r') do |f|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;YAML::load(f)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;super(rule\_names, multipliers[0])  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;end
> 
> This is all designed to work with a testing framework Daniel provided, of  
> course. The really interesting part of Daniel's code, to me, was breeder.rb.  
> If you're at all interested in genetic programming, do look around in there. It  
> even has comments to drive you through the evolution process. Neat stuff.
> 
> Though my solution turns out to be a pretty bad player, it does have the  
> elements any smarter solution would need. Its flaw is in the logic, which is  
> just me being a bad teacher for computer strategy. Let's look past that and  
> examine the code anyway:
> 
> &nbsp;&nbsp;#!/usr/local/bin/ruby -w  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;class RiskPlayer \< Player  
> &nbsp;&nbsp;&nbsp;&nbsp;def self.card\_from\_string( card )  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;value, land = card[0..-2], card[-1, 1].downcase  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Game::Card.new( value[0] == ?I ? value : value.to\_i,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Game::LANDS.find { |l| l[0, 1] == land } )  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;def initialize  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@piles = Hash.new do |piles, player|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;piles[player] = Hash.new { |pile, land| pile[land] = Array.new }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@deck\_size = 60  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@hand = nil  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@last\_dicard = nil  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@action = nil  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@done = false  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;# ...
> 
> There's nothing tricky in there. The class method is a helper for converting a  
> String like "InvV" into and actual card object.
> 
> Then initialize() just prepares the instance data this class needs to track.  
> The @piles variable looks a little ugly because I wanted it to invent the data  
> structure as needed. It's just a Hash containing three keys: :me, :them, and  
> :discards. Each of those is a Hash that contains an Array pile for each land  
> type.
> 
> Now the quiz requires a parser for the protocol. Here's the easiest approach I  
> could come up with:
> 
> &nbsp;&nbsp;&nbsp;&nbsp;def show( game\_data )  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if @done  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts game\_data  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if game\_data =~ /^(Your?)(?: opponent)? (play|discard)s? the (\w+)/  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;card = self.class.card\_from\_string($3)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if $2 == "play"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if $1 == "You"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@piles[:me][card.land] \<\< card  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@piles[:them][card.land] \<\< card  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@piles[:discards][card.land] \<\< card  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@last\_discard = nil if $1 == "Your"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if game\_data =~ /^You(?:r opponent)? picks? up the (\w+)/  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@piles[:discards][self.class.card\_from\_string($1).land].pop  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if game\_data =~ /^\s\*Deck:\s+#+\s+\((\d+)\)/  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@deck\_size = $1.to\_i  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if game\_data =~ /^\s\*Hand:((?:\s+\w+)+)/  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@hand = $1.strip.split.map { |c| self.class.card\_from\_string(c) }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if game\_data.include?("Your play?")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@action = :play\_card  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;elsif game\_data.include?("Draw from?")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@action = :draw\_card  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@done = true if game\_data.include?("Game over.")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;end
> 
> The server notifies you when everything happens and I think it's easier to just  
> track those notifications than it is to track your own moves and/or parse the  
> game board to figure out where everything is. The messages are easy to break  
> down with light Regexp usage.
> 
> The else branch of that top level if statement handles the parsing. First it  
> breaks down play/discard messages and places the card on the indicated pile. It  
> pops cards off the discard piles too, as needed. The next bit reads the deck  
> size and your hand from the game board. The third section tracks whether you  
> were asked to play or draw and the final line watches for a "Game over." which  
> causes the code to print the final score (if branch at the top of the method).
> 
> With that in place, we can move our focus to responding to play requests:
> 
> &nbsp;&nbsp;&nbsp;&nbsp;# ...  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;def move  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;send(@action)  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;private  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;def play\_card  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plays, discards = @hand.partition { |card| playable? card }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if plays.empty?  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;discard\_card(discards)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;risks = analyze\_risks(plays)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;risk = risks.max { |a, b| a.last \<=\> b.last }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return discard\_card(@hand) if risk.last \< 0  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;land = risks.max { |a, b| a.last \<=\> b.last }.first.land  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;play = plays.select { |card| card.land == land }.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sort\_by { |c| c.value.is\_a?(String) ? 0 : c.value }.first  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"#{play.value}#{play.land[0, 1]}".sub("nv", "")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;def discard\_card( choices )  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;discard = choices.sort\_by do |card|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[ playable?(card) ? 1 : 0, playable?(card, :them) ? 1 : 0,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;card.value.is\_a?(String) ? 0 : card.value ]  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end.first
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@last\_discard = discard  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"d#{discard.value}#{discard.land[0, 1]}".sub("nv", "")  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;def draw\_card  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;want = @piles[:discards].find do |land, cards|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;not @piles[:me][land].empty? and  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cards.last != @last\_discard and cards.any? { |card| playable?(card) }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if want  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;want.first[0, 1]  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"n"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;def playable?( card, who = :me )  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@piles[who][card.land].empty? or  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@piles[who][card.land].last.value.is\_a?(String) or  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;( not card.value.is\_a?(String) and  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@piles[who][card.land].last.value \< card.value )  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;# ...
> 
> First, move() calls the correct action method based on what the server last  
> requested (:play\_card or :draw\_card).
> 
> The main action method, play\_card(), splits hands into playable cards and  
> discards. It analyzes the risks of each play and tries to find something fairly  
> safe. If it has no plays or doesn't like its choices, a handoff is made to  
> discard\_card().
> 
> The discard method just sorts available discards by some general criteria: Can  
> I play this? Can my opponent? How much is it worth? It tries to find  
> something it can't play, the opponent can't play, and that is low in value. It  
> tosses that.
> 
> The other action method, draw\_card(), just scans the discard piles for cards it  
> wants. If it sees a goody, it will pull from that pile. Otherwise it defaults  
> to a draw from the deck.
> 
> Finally, playable?() is just a tool that will tell you if a card can be played  
> currently, by the indicated player.
> 
> The missing piece is the risk analysis method:
> 
> &nbsp;&nbsp;&nbsp;&nbsp;# ...  
> &nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;def analyze\_risks( plays )  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plays.inject(Hash.new) do |risks, card|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;risks[card] = 0  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;me\_total = ( @piles[:me][card.land] +  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plays.select { |c| c.land == card.land }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;).inject(0) do |total, c|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if c.value.is\_a? String  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;total  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;total + c.value  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;risks[card] += 20 - me\_total  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;them\_total = @piles[:them][card.land].inject(0) do |total, c|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if c.value.is\_a? String  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;total  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;total + c.value  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;high = card.value.is\_a?(String) ? 2 : card.value  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;risks[card] += ( (high..10).inject { |sum, n| sum + n }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- (me\_total + them\_total) ) / 2  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if @piles[:me][card.land].empty?  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;lands\_played = @piles[:me].inject(0) do |count, (land, cards)|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if cards.empty?  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;count  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;count + 1  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;risks[card] -= (lands\_played + 1) \* 5  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;risks  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;end
> 
> WARNING: The above code is what needs tweaking to make the AI player smarter.
> 
> This method could do whatever thinking is required. It's just expected to  
> return a Hash of playable cards (keys) and their ratings (values). The  
> play\_card() method will play the highest rated card, as long as it isn't  
> negative.
> 
> I tried to distill a little of how I play down into computer terms here. The  
> method takes into account how many points it has in a given pile and how many  
> more it could play from its hand. It also adds up the total of the cards still  
> at large (above the highest play it can make), and assumes it could luck into  
> half of those. Finally, it adds a penalty to the rating for each new pile  
> started. It's usually a mistake to play too many piles in Lost Cities, because  
> you don't have time to finish them all off. Again, this logic needs further  
> refinement.
> 
> For yet another spin on rules, Adam Shelly sent in a solution yesterday that has  
> a whole bunch of criteria it bases decisions on. Check out this list:
> 
> &nbsp;&nbsp;# ...  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;#rules affecting play/hold decision :  
> &nbsp;&nbsp;#positive values mean play, negative mean hold  
> &nbsp;&nbsp;@prules = {:rule\_inSequence=\>[0.6,0.8],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_lowCard=\>[0.1,0.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_lowCards=\>[0.2,0.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_highCard=\>[-0.3,0.1],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_highCards=\>[-0.2,0.2],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_investments=\>[0.1,-0.2],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_onInvestments=\>[0.5,0.7],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_holdingInvestments=\>[-0.2,0.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_investmentWithHope=\>[0.5,0.3],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_investmentWithoutHope=\>[-0.6,-1.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_group10=\>[0.5,-0.4],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_group15=\>[0.6,-0.3],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_group20=\>[0.7,-0.2],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_group25=\>[0.9,-0.1],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_total20 =\>[0.35,1.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_total25 =\>[0.6,1.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_suitStarted=\>[0.7,0.9],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_closeToPrevious=\>[0.4,0.5],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_multiplier2=\>[0.4,0.8],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_multiplier3=\>[0.5,0.9],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_onUnplayed=\>[-0.5,-1.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_heHasPlayed=\>[-0.1,0.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_heHasPlayed10=\>[-0.2,0.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_heHasPlayed20=\>[-0.3,0.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_handNegative=\>[0.5,0.9],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_mustPlays=\>[-0.3,1.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_lowerInHand=\>[-0.5,-0.4],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_highestInHand=\>[-0.1,-0.01],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_2followsInvest=\>[0.3,0.5],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_finishGame=\>[0.0,2.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_possibleBelow=\>[-0.2,-0.05],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_possibleManyBelow=\>[-0.4,-0.1]}  
> &nbsp;&nbsp;#rules affecting keep/discard decision :  
> &nbsp;&nbsp;#positive values mean keep, negative mean discard  
> &nbsp;&nbsp;@drules = {:rule\_useless2me=\>[-0.5, 0.1],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_useless2him=\>[-0.2,0.1],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_useful2him=\>[0.4,0.5],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_useful2me=\>[0.3,0.3],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_heHasPlayed=\>[0.1,0.3],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_singleton=\>[-0.2,-0.1],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_noPartners=\>[-0.3,-0.3],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_wantFromDiscard=\>[0.3,0.5],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_belowLowestPlayable=\>[-0.2,0.0],  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:rule\_dontDiscardForever=\>[0.5,1]}  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;# ...
> 
> Those numbers are weights, one set for early in the game and another for late.  
> Cards are ranked by these criteria which allows plays/discards to be found.  
> These weights are hand tuned, from Adam's experience.
> 
> Many thanks to all who fiddled with the game and especially to those who even  
> tried to build a player.
> 
> Tomorrow we have the very core of what makes programmers into programmers...  
> Talking barnyard animals, of course.
