From: Bob Showalter Date: 2005-10-19T04:17:08+09:00 Subject: Re: [QUIZ] Lost Cities (#51) 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