⚠️ Warning: This is a draft ⚠️

This means it might contain formatting issues, incorrect code, conceptual problems, or other severe issues.

If you want to help to improve and eventually enable this page, please fork RosettaGit's repository and open a merge request on GitHub.

{{collection|Go Fish}}

class Card
  RANKS = %w(2 3 4 5 6 7 8 9 10 J Q K A)
  SUITS = %w(C D H S)

  def initialize(rank, suit)
    @rank = rank
    @suit = suit
  end
  attr_reader :rank, :suit

  def <=>(other)
    # this ordering sorts first by rank, then by suit
    (RANKS.find_index(self.rank) <=> RANKS.find_index(other.rank)).nonzero? ||
    (SUITS.find_index(self.suit) <=> SUITS.find_index(other.suit))
  end

  def to_s
    @rank + @suit
  end
end

#######################################################################
class Deck
  def initialize
    @deck = []
    Card::SUITS.each do |suit|
      Card::RANKS.each do |rank|
        @deck << Card.new(rank, suit)
      end
    end
    @deck.shuffle!
  end
  attr_reader :deck

  # returns an array of cards, even for dealing just 1 card
  def deal(n=1)
    @deck.pop(n)
  end

  def empty?
    @deck.empty?
  end

  def cards_remaining
    @deck.length
  end
end

#######################################################################
class Player
  def initialize(game)
    @hand = {}
    @books = []
    @game = game
    @opponents_hand = {
      :known_to_have => [],
      :known_not_to_have => [],
    }
  end
  attr_reader :name

  def take_cards(cards)
    my_cards = @hand.values.flatten.concat(cards)
    @hand = my_cards.group_by {|card| card.rank}

    # look for, and remove, any books
    @hand.each do |rank, cards|
      if cards.length == 4
        puts "#@name made a book of #{rank}"
        @books << rank
        @hand.delete(rank)
      end
    end
    if @hand.empty? and not @game.deck.empty?
      @game.deal(self, 1)
    end
  end

  def num_books
    @books.length
  end

  # return true if the next turn is still mine
  # return false if the next turn is my opponent's
  def query(opponent)
    wanted = wanted_card
    puts "#@name: Do you have a #{wanted}?"
    received = opponent.answer(wanted)
    @opponents_hand[:known_to_have].delete(wanted)
    if received.empty?
      @game.deal(self, 1)
      # by my next turn, opponent will have been dealt a card
      # so I cannot know what he does not have.
      @opponents_hand[:known_not_to_have] = []
      false
    else
      take_cards(received)
      @opponents_hand[:known_not_to_have].push(wanted).uniq!
      true
    end
  end

  def answer(rank)
    cards = []
    @opponents_hand[:known_to_have].push(rank).uniq!
    if not @hand[rank]
      puts "#@name: Go Fish!"
    else
      cards = @hand[rank]
      @hand.delete(rank)
      puts "#@name: Here you go -- #{cards.join(', ')}"
      @game.deal(self, 1) if @hand.empty?
    end
    cards
  end

  def print_hand
    puts "hand for #@name:"
    puts "  hand: "+ @hand.values.flatten.sort.join(', ')
    puts "  books: "+ @books.join(', ')
    puts "opponent is known to have: " + @opponents_hand[:known_to_have].sort.join(', ')
  end
end

#######################################################################
class ComputerPlayer < Player
  def initialize(game)
    super
    @name = 'Computer'
  end

  def wanted_card
    known = @hand.keys & @opponents_hand[:known_to_have]
    if not known.empty?
      sort_cards_by_most(known).first
    else
      possibilities = @hand.keys - @opponents_hand[:known_not_to_have]
      if not possibilities.empty?
        possibilities.shuffle.first
      else
        #sort_cards_by_most(@hand.keys).first
        @hand.keys.shuffle.first
      end
    end
  end

  # sort ranks by ones with most cards in my hand.  better chance to make a book
  def sort_cards_by_most(array_of_ranks)
    array_of_ranks.sort_by {|rank| -@hand[rank].length}
  end
end

#######################################################################
class HumanPlayer < Player
  def initialize(game)
    super
    @name = 'Human'
  end

  def take_cards(cards)
    puts "#@name received: #{cards.join(', ')}"
    super
  end

  def wanted_card
    print_hand
    wanted = nil
    loop do
      print "\nWhat rank to ask for? "
      wanted = $stdin.gets
      wanted.strip!.upcase!
      if not Card::RANKS.include?(wanted)
        puts "not a valid rank: #{wanted} -- try again."
      elsif not @hand.has_key?(wanted)
        puts "you don't have a #{wanted} -- try again"
      else
        break
      end
    end
    wanted
  end
end

#######################################################################
class GoFishGame
  def initialize
    @deck = Deck.new
    @players = [HumanPlayer.new(self), ComputerPlayer.new(self)]
    rotate_players if rand(2) == 1
    @players.each {|p| deal(p, 9)}
  end
  attr_reader :deck

  def start
    loop do
      p1, p2 = @players
      # p1.query(p2) method returns true if p1 keeps his turn
      # and returns false otherwise
      p1.query(p2) or rotate_players
      break if p1.num_books + p2.num_books == 13
    end
    puts "
### ========================
" # add a separator between turns
    puts "Game over"
    @players.each {|p| puts "#{p.name} has #{p.num_books} books"}
    nil
  end

  def rotate_players
    @players.push(@players.shift)
    puts "------------------------------" # add a separator between turns
  end

  def deal(player, n=1)
    n = [n, @deck.cards_remaining].min
    puts "Dealer: #{n} card(s) to #{player.name}"
    player.take_cards(@deck.deal(n))
  end
end

#######################################################################
# main

srand
GoFishGame.new.start