Glenn Parker
3/20/2005 6:36:00 PM
#!/usr/bin/ruby -w
#
# Quiz 24: Texas Hold'em
# Solution by Glenn Parker
module Combine
# Generate all combinations of +pick+ elements from +items+ array.
def Combine.pick(pick, items, &block)
combine([], 0, pick, items, &block)
end
private
def Combine.combine(set, index, pick, items, &block)
if pick == 0 or index == items.length
yield set
else
set.push(items[index])
combine(set, index + 1, pick - 1, items, &block)
set.pop
combine(set, index + 1, pick, items, &block) if
pick < items.length - index
end
end
end
# One card, with a face [2-9TJQKA] and a suit [shdc].
class Card
attr_reader :face, :suit
Face_Ranks = {
:A => 12, :K => 11, :Q => 10, :J => 9,
:T => 8, :"9" => 7, :"8" => 6, :"7" => 5,
:"6" => 4, :"5" => 3, :"4" => 2, :"3" => 1,
:"2" => 0
}
Suit_Ranks = {
:s => 3, :h => 2, :d => 1, :c => 0
}
def initialize(face_suit)
@face = face_suit[0].chr.to_sym
raise "Invalid face \"#{@face}\"" unless Face_Ranks.has_key?(@face)
@suit = face_suit[1].chr.to_sym
raise "Invalid suit \"#{@suit}\"" unless Suit_Ranks.has_key?(@suit)
freeze
end
def rank # Overall ranking in the deck.
index * 4 + Suit_Ranks[@suit]
end
def index # Ranking, independent of suit.
Face_Ranks[@face]
end
def to_s
@face.to_s + @suit.to_s
end
end
# A typed collection of up to five cards.
class Hand
include Comparable # Hands can be compared.
attr_reader :hand_type, :cards
Hand_Names = [
"Folded",
"High Card",
"Pair",
"Two Pair",
"Three of a Kind",
"Straight",
"Flush",
"Full House",
"Four of a Kind",
"Straight Flush",
"Royal Flush"
]
# Define constants by converting "High Card" to Hand::High_Card = 0.
Hand_Names.each_with_index do |n, i|
const_set(n.tr(" ", "_"), i)
end
def initialize(hand_type, cards)
@hand_type = hand_type
@cards = cards.dup
freeze
end
def to_s
@cards.join(" ") + " " + Hand_Names[@hand_type]
end
def <=>(other)
if @hand_type != other.hand_type
# Hand ranking dominates.
return @hand_type <=> other.hand_type
elsif @hand_type == Flush
# Compare corresponding cards, highest to lowest.
@cards.reverse.zip(other.cards.reverse) do |a, b|
return a.index <=> b.index if a.index != b.index
end
return 0
elsif @hand_type == Two_Pair
# Compare the two highest pairs, then the remaining pairs
self_indices = [@cards[0].index, @cards[2].index].sort!
other_indices = [other.cards[0].index, other.cards[2].index].sort!
if self_indices[1] != other_indices[1]
return self_indices[1] <=> other_indices[1]
else
return self_indices[0] <=> other_indices[0]
end
else
# All others types of hand are compared using their first card.
return @cards[0].index <=> other.cards[0].index
end
end
end
# A collection of seven cards, from which Hands are extracted.
class Deal
attr_reader :all_cards, :best_hand, :kickers
def initialize(card_string)
# Parse and sort the cards. The sorting order chosen here is
# important when extracting and comparing hands later.
@all_cards = card_string.split(/ /).collect do |face_suit|
Card.new(face_suit)
end.sort_by { |card| card.rank }
@hands = []
if @all_cards.length == 7
# Extract all possible hands if we got 7 cards.
find_high_card
find_groups
find_two_pairs_and_full_house
find_straight_and_flush
else
# Otherwise, make a folded hand.
add_hand(Hand::Folded, @all_cards)
end
# Pick the best possible hand and determine the kickers.
@best_hand = @hands.max
@kickers = (@all_cards - @best_hand.cards).sort_by do |card|
-card.rank
end
end
private
def add_hand(hand_type, cards)
@hands << Hand.new(hand_type, cards)
end
def find_high_card
add_hand(Hand::High_Card, [ @all_cards[-1] ])
end
def find_groups
# Find the longest run of each face in @all_cards.
start = 0
while @all_cards[start]
for stop in ((start + 1)..@all_cards.length)
next if @all_cards[stop] and
(@all_cards[start].face == @all_cards[stop].face)
case (stop - start)
when 4:
add_hand(Hand::Four_of_a_Kind, @all_cards[start...stop])
when 3:
add_hand(Hand::Three_of_a_Kind, @all_cards[start...stop])
when 2:
add_hand(Hand::Pair, @all_cards[start...stop])
end
break
end
start = stop
end
end
def find_two_pairs_and_full_house
pairs = @hands.find_all do |h|
h.hand_type == Hand::Pair
end
threes = @hands.find_all do |h|
h.hand_type == Hand::Three_of_a_Kind
end
# Find up to three combinations of two pairs.
if (pairs.length > 1)
Combine.pick(2, pairs) do |pair_hands|
add_hand(Hand::Two_Pair,
pair_hands[0].cards + pair_hands[1].cards)
end
end
# Each combination of a pair and three-of-a-kind is a full house.
pairs.each do |pair|
threes.each do |three|
add_hand(Hand::Full_House, three.cards + pair.cards)
end
end
# Two three-of-a-kinds yield two possible full-houses.
if (threes.length > 1)
add_hand(Hand::Full_House,
threes[0].cards + threes[1].cards[0..1])
add_hand(Hand::Full_House,
threes[1].cards + threes[0].cards[0..1])
end
# We could combine four-of-a-kind and a pair for a full-house
# but four-of-a-kind already beats a full-house.
end
def find_straight_and_flush
# Examine all combinations of five cards
Combine.pick(5, @all_cards) do |cards|
is_flush = true
is_straight = true
1.upto(4) do |i|
is_straight = false if
(cards[i].index != cards[i - 1].index + 1)
is_flush = false if
(cards[i].suit != cards[0].suit)
end
# Add the best hand found in this iteration.
case
when (is_straight and is_flush and cards[0].face == :"T")
add_hand(Hand::Royal_Flush, cards)
when (is_straight and is_flush)
add_hand(Hand::Straight_Flush, cards)
when (is_flush)
add_hand(Hand::Flush, cards)
when (is_straight)
add_hand(Hand::Straight, cards)
end
end
end
end
# A card player that holds a Hand and some kickers.
class Player
attr_reader :hand, :kickers
attr_accessor :wins
def initialize(hand, kickers)
@hand = hand
@kickers = kickers
@wins = false
end
# Return <=> value comparing kickers from another Player.
def compare_kickers(other)
@kickers.zip(other.kickers) do |a_kicker, b_kicker|
return 1 if a_kicker.index > b_kicker.index
return -1 if a_kicker.index < b_kicker.index
end
return 0
end
end
# Read the input.
players = []
while line = gets
line.chomp!
# Take first 20 chars only, making it easy to use previously
# printed results as input for re-testing.
deal = Deal.new(line[0, 20])
players << Player.new(deal.best_hand, deal.kickers)
end
# Find the winner(s).
winners = []
players.each do |player|
if winners.empty?
winners << player
elsif player.hand > winners[0].hand
winners.clear
winners << player
elsif player.hand == winners[0].hand
# Try to resolve ties based on kickers.
comparison = player.compare_kickers(winners[0])
if comparison >= 0
winners.clear if comparison > 0
winners << player
end
end
end
winners.each { |player| player.wins = true }
# Report the results.
players.each do |player|
# Print cards sorted by face with kickers at the end.
print((player.hand.cards + player.kickers).join(" "))
# Print description of hand and (winner) flag
if player.hand.hand_type > 0
print " ", Hand::Hand_Names[player.hand.hand_type]
print " (winner)" if player.wins
end
print "\n"
end