Go Fish/Ruby

From Rosetta Code
Go Fish/Ruby is part of Go Fish. You may find other members of Go Fish at Category: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