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