[QUIZ] Lost Cities (#51)

R

Ruby Quiz

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!
 
B

Benedikt Heinen

(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)
 
B

Bob Showalter

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 :eek:p_hand

# Hash by land for Opponent. Each entry is an Array of
# Game::Card's played to that land.
attr_reader :eek:p_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+):/ && @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
 
J

James Edward Gray II

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__
 
B

Bob Showalter

Bob said:
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 :eek:p_hand

# Hash by land for Opponent. Each entry is an Array of
# Game::Card's played to that land.
attr_reader :eek:p_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+):/ && @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
 
J

James Edward Gray II

#!/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
 
A

Anthony Moralez

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 =3D ""
super
end

def show( game_data )
@data << game_data
end

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

Anthony Moralez

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

Adam Shelly

------=_Part_8308_33115134.1129741947794
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

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

------=_Part_8308_33115134.1129741947794
Content-Type: application/x-ruby; name=ads_lc_player.rb
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="ads_lc_player.rb"

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

#ADS_lc_player.rb
# -Adam Shelly 10.17.05
#
# Player for 'lost_cities.rb' from Ruby Quiz #51
#
# runs a whole bunch of rules classifying cards.
# multiplies rules by weights to rank ranks cards in hand along 2 axis :
# keep->discard(1..-1) and play->hold(1..-1)
# makes best play from the rankings.

require 'Matrix'
require 'Yaml'

class Array
def max_index
index max
end
end


class ADS_LC_Player < Player
S = {?D=>0,?O=>1,?M=>2,?J=>3,?V=>4}
attr_reader :winner
def initialize
super
@name="1a"
@myhand =[]
@land=Array.new(5){Array.new}
@opplands=Array.new(5){Array.new}
@dpile = Array.new(5){Array.new}
@data = ""
@deckcount = 12*5-16
@discarded = 0
#rules affecting play/hold decision :
#positive values mean play, negative mean hold
@prules = {:rule_inSequence=>[0.6,0.8],
:rule_lowCard=>[0.1,0.0],
:rule_lowCards=>[0.2,0.0],
:rule_highCard=>[-0.3,0.1],
:rule_highCards=>[-0.2,0.2],
:rule_investments=>[0.1,-0.2],
:rule_onInvestments=>[0.5,0.7],
:rule_holdingInvestments=>[-0.2,0.0],
:rule_investmentWithHope=>[0.5,0.3],
:rule_investmentWithoutHope=>[-0.6,-1.0],
:rule_group10=>[0.5,-0.4],
:rule_group15=>[0.6,-0.3],
:rule_group20=>[0.7,-0.2],
:rule_group25=>[0.9,-0.1],
:rule_total20 =>[0.35,1.0],
:rule_total25 =>[0.6,1.0],
:rule_suitStarted=>[0.7,0.9],
:rule_closeToPrevious=>[0.4,0.5],
:rule_multiplier2=>[0.4,0.8],
:rule_multiplier3=>[0.5,0.9],
:rule_onUnplayed=>[-0.5,-1.0],
:rule_heHasPlayed=>[-0.1,0.0],
:rule_heHasPlayed10=>[-0.2,0.0],
:rule_heHasPlayed20=>[-0.3,0.0],
:rule_handNegative=>[0.5,0.9],
:rule_mustPlays=>[-0.3,1.0],
:rule_lowerInHand=>[-0.5,-0.4],
:rule_highestInHand=>[-0.1,-0.01],
:rule_2followsInvest=>[0.3,0.5],
:rule_finishGame=>[0.0,2.0],
:rule_possibleBelow=>[-0.2,-0.05],
:rule_possibleManyBelow=>[-0.4,-0.1]}
#rules affecting keep/discard decision :
#positive values mean keep, negative mean discard
@drules = {:rule_useless2me=>[-0.5, 0.1],
:rule_useless2him=>[-0.2,0.1],
:rule_useful2him=>[0.4,0.5],
:rule_useful2me=>[0.3,0.3],
:rule_heHasPlayed=>[0.1,0.3],
:rule_singleton=>[-0.2,-0.1],
:rule_noPartners=>[-0.3,-0.3],
:rule_wantFromDiscard=>[0.3,0.5],
:rule_belowLowestPlayable=>[-0.2,0.0],
:rule_dontDiscardForever=>[0.5,1]}
end

def load filename=nil
if (filename)
g = Gene.load(filename)
@prules = g.prules.merge(@prules)
@drules = g.drules.merge(@drules)
@name = g.name
end
end

def show( game_data )
#replace Inv w/ '0' and 10 with ':' (= ?9+1)
game_data.gsub!(/Inv/,'0')
game_data.gsub!(/10(\w)/,':\1')
case game_data
when /Hand: (.+?)\s*$/
@oldhand = @myhand
@myhand = $1.split
when /^Your opponent plays the (.*)\./
push @opplands, $1
when /^Your opponent discards the (.*)\./
push @dpile, $1
when /opponent draws/
@deckcount-=1
when /opponent picks up the (.*)\./
@dpile[suit($1)].pop
when /Final Score:(.*)\(Y.*vs.(.*)\(Op/
p "Game Over, #{$1} vs #{$2}"
@Winner = $1.to_i > $2.to_i
#Gene.new(@prules,@drules,@name).dump "#{@name}.yaml"
end
@data << game_data
end

def move
if @data.include?("Draw from?")
draw_card
else
make_move.sub(/0/, "I").sub(":","10")
end
ensure
@data = ""
end

private

def suit card
S[card[-1]]
end
def val card
card[-2]-?0
end
def push pile,card
pile[suit(card)]<<val(card)
end

def draw_card
@dwanted.each_with_index{|w,i|
if w
@dpile.pop
return [S.index(i)].pack("C")
end
}
@deckcount-=1
"n"
end

def calc_statistics
# find out interesting facts about cards
@set_held=Array.new(5){Array.new}
@sumheld = Array.new(5){0}
@multiples = Array.new(5){1}
@iplayed = Array.new(5){0}
@lowest_playable = Array.new(5)
@unseen = Array.new(5){(2..10).to_a}

@myhand.each(){|c|
@set_held[suit(c)] << val(c)
@sumheld[suit(c)]+=val(c)
@multiples[suit(c)]*=2 if val(c)==0
}
@land.each_with_index {|l,i| l.each{|v|
@multiples*=2 if v==0
@iplayed+=1 if v==0
@unseen.delete v
}}
@opplands.each_with_index{|l,i| l.each{|v|
@unseen.delete v
}}
@dpile.each_with_index{|l,i| l.each{|v|
@unseen.delete v
}}
@sumplayed = @land.map{|l| l.inject(0){|sum,v|sum+=v}}
@opplayed = @opplands.map{|l| l.inject(0){|sum,v|sum+=v}}
5.times {|i|
@lowest_playable = [@land[-1]||0,@opplands[-1]||0].min
}

#we must play any valid cards we are holding in a suit we have started
@mustplay = @myhand.find_all{|c| val(c) >= (@land[suit(c)][-1]||11) }
#time running out?
@tight = ((@deckcount /2)-1 <= @mustplay.size) ? 1 : 0
@supertight = ((@deckcount) <= @mustplay.size)
end

def check_discards
#prevent endless loop of discards
return @dwanted = [nil]*5 if @discarded > 5
i=-1
# find cards we can play, or cards we can probably use
@dwanted = @dpile.map do |p|
i+=1
(card = p[-1]) &&
if (l = @land[-1])
card && (card >= l) #we can use for sure
else
card + @sumheld > 15 && @tight==0 #we can probably use
end
end
#if we need more time for 'mustplay' cards, force draw from discard
if @supertight && (@dwanted.find_all{|d|d}==[])
@dwanted = @dpile.map{|p| p[-1] }
end
end

def make_move
calc_statistics
check_discards
p @opplands,@dpile,@land,@myhand if $DEBUG

#Rank Play<->Hold and Keep<->Discard
pmoves,dmoves = Moveset.new,Moveset.new
@prules.each {|rule,weights| pmoves += apply(rule) * weights[@tight]}
@drules.each {|rule,weights| dmoves += apply(rule) * weights[@tight]}

#We want to play the ones with high Play and low Keep values
possible_plays = pmoves - dmoves*0.5
#we want to discard the ones with high Discard and low Hold values
possible_discards = (pmoves*0.5 - dmoves)
p possible_plays, possible_discards if $DEBUG

while 1
if possible_plays.max >= possible_discards.max
play= @myhand[possible_plays.max_index]
p "#{possible_plays.max} vs #{possible_discards.max} => #{play}" if $DEBUG
if play_valid?(play)
push @land, play
@discarded = 0
return play
else #take invalid plays out of the running
mi = possible_plays.max_index
(possible_plays = possible_plays.to_a)[mi]=-100
next
end
else
play= @myhand[possible_discards.max_index]
end
p "discarding #{play}" if $DEBUG
push @dpile, play
@dwanted[suit(play)]=nil #we can't draw from here
@discarded += 1
return "d#{play}"
end
end

def play_valid? play
hi =@land[suit(play)][-1]
!hi || (val(play) >=hi)
end

def apply rule
a = @myhand.map {|c| self.send(rule,c) ? 1 : 0 }
p "#{a.inspect} <#{rule}: " if $DEBUG
Moveset.new(a)
end

#All the ways to classify a card.
def rule_inSequence c
val(c) == ((@land[suit(c)][-1]||-1) +1 )
end
def rule_2followsInvest c
val(c) == 2 && @iplayed[suit(c)] > 0
end
def rule_lowCard c
c == @myhand.min
end
def rule_lowCards c
val(c) < 5
end
def rule_highCard c
c == @myhand.max
end
def rule_highCards c
val(c) > 5
end
def rule_investments c
val(c) == 0
end
def rule_onInvestments c
@land[suit(c)].include?(0)
end
def rule_holdingInvestments c
@set_held[suit(c)].include?(0) && val(c) != 0
end
def rule_group10 c
@sumheld[suit(c)] > 10
end
def rule_group15 c
@sumheld[suit(c)] > 15
end
def rule_group20 c
@sumheld[suit(c)] > 25
end
def rule_group25 c
@sumheld[suit(c)] > 25
end
def rule_total20 c
@sumheld[suit(c)]+@sumplayed[suit(c)] > 21
end
def rule_total25 c
@sumheld[suit(c)]+@sumplayed[suit(c)] > 25
end
def rule_investmentWithHope c
val(c) == 0 && ( @sumheld[suit(c)] > (5 + 5*@multiples[suit(c)]))
end
def rule_investmentWithoutHope c
!rule_investmentWithHope c
end
def rule_suitStarted c
@sumplayed[suit(c)]+ @iplayed[suit(c)] > 0
end
def rule_closeToPrevious c
(val(c) - (@land[suit(c)][-1]||0)) < 3
end
def rule_useless2me c
val(c) < ( @land[suit(c)][-1] || 0)
end
def rule_useful2me c
val(c) >= ( @land[suit(c)][-1] || 10)
end
def rule_useless2him c
val(c) < ( @opplands[suit(c)][-1] || 0 )
end
def rule_useful2him c
val(c) >= ( @opplands[suit(c)][-1] || 0 )
end
def rule_possibleBelow c
@unseen[suit(c)].find_all{|v| v < val(c)}.size > 0
end
def rule_possibleManyBelow c
@unseen[suit(c)].find_all{|v| v < val(c)}.size > 3
end
def rule_multiplier2 c
@multiples[suit(c)]>2
end
def rule_multiplier3 c
@multiples[suit(c)]>4
end
def rule_onUnplayed c
@land[suit(c)].empty?
end
def rule_heHasPlayed c
!@opplands[suit(c)].empty?
end
def rule_heHasPlayed10 c
@opplayed[suit(c)] > 10
end
def rule_heHasPlayed20 c
@opplayed[suit(c)] > 20
end
def rule_singleton c
@set_held[suit(c)].size == 1
end
def rule_noPartners c
@land[suit(c)].empty?
end
def rule_handNegative c
!@land[suit(c)].empty? &&@sumplayed[suit(c)] < 20
end
def rule_wantFromDiscard c
@dwanted[suit(c)]
end
def rule_mustPlays c
@mustplay.include?(c)
end
def rule_belowLowestPlayable c
val(c) < @lowest_playable[suit(c)]
end
def rule_lowerInHand c
val(c) > @set_held[suit(c)].min
end
def rule_highestInHand c
val(c) == @set_held[suit(c)].max
end
def rule_finishGame c
#tag the ones we need to play
@supertight && @mustplay.include?(c)
end
def rule_dontDiscardForever c
@discarded > 5
end


end

# A moveset is simply a set of rankings for each move.
# They can be added, multiplied by scalars, etc.
# This probably should have been a subclass of Vector, instead of containing one...
class Moveset
def initialize source = nil
@moves = source ? Vector[*(source.to_a)] : Vector[*Array.new(8){0}]
end
def * other
Moveset.new(@moves * other)
end
def + other
Moveset.new(@moves + Vector[*(other.to_a)])
end
def - other
Moveset.new(@moves - Vector[*(other.to_a)])
end
def [] idx
@moves[index]
end
def []= idx,val
@moves[index] = val
end
def max
@moves.to_a.max
end
def max_index
@moves.to_a.index max
end
def to_a
@moves.to_a
end
def to_s
@moves.to_s
end
end

#A container for all the rules.
# Allows easy yamlization.
class Gene
attr_reader :prules, :drules
attr_accessor :name, :parent
def initialize p,d,n,par=nil
@prules = p
@drules = d
@name = n
@parent = par
end
def dump filename
File.open( filename, 'w' ) {|f| f<<self.to_yaml}
end
def Gene.load filename
File.open(filename){|f| YAML::load(f)}
end
end

------=_Part_8308_33115134.1129741947794--
 
A

Adam Shelly

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.

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
=09def initialize
=09=09super
=09=09load "gene.yaml"
=09end

#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[-1] > (@land[-1]||0))
@dpile.pop
return [S.index(i)].pack("C")
end
}
@deckcount-=3D1
"n"
end

end
-------

-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


-----
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,020
Latest member
GenesisGai

Latest Threads

Top