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