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+):/ && @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