1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.
2. Support Ruby Quiz by submitting ideas as often as you can:
3. Enjoy!
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
by Matthew D Moss
You work for a cable network; specifically, you are the resident hacker for a
Texas Hold'Em Championship show.
The show's producer has come to you for a favor. It seems the play-by-play
announcers just can't think very fast. All beauty, no brains. The announcers
could certainly flap their jaws well enough, if they just knew what hands the
players were holding and which hand won the round. Since this is live TV, they
need those answers quick. Time to step up to the plate. Bob, the producer,
explains what you need to do.
BOB: Each player's cards for the round will be on a separate line of the input.
Each card is a pair of characters, the first character represents the face, the
second is the suit. Cards are separated by exactly one space. Here's a sample
hand.
Kc 9s Ks Kd 9d 3c 6d
9c Ah Ks Kd 9d 3c 6d
Ac Qc Ks Kd 9d 3c
9h 5s
4d 2d Ks Kd 9d 3c 6d
7s Ts Ks Kd 9d
YOU: Okay, I was going ask what character to use for 10, but I guess 'T' is it.
And 'c', 'd', 'h' and 's' for the suits, makes sense. Why aren't seven cards
listed for every player?
BOB: Well, if a player folds, only his hole cards and the community cards he's
seen so far are shown.
YOU: Right. And why did the fifth player play with a 4 and 2? They're suited,
but geez, talk about risk...
BOB: Stay on topic. Now, the end result of your code should generate output that
looks like this:
Kc 9s Ks Kd 9d 3c 6d Full House (winner)
9c Ah Ks Kd 9d 3c 6d Two Pair
Ac Qc Ks Kd 9d 3c
9h 5s
4d 2d Ks Kd 9d 3c 6d Flush
7s Ts Ks Kd 9d
YOU: Okay, so I repeat the cards, list the rank or nothing if the player folded,
and the word "winner" in parenthesis next to the winning hand. Do you want the
cards rearranged at all?
BOB: Hmmm... we can get by without it, but if you have the time, do it. Don't
bother for folded hands, but for ranked hands, move the cards used to the front
of the line, sorted by face. Kickers follow that, and the two unused cards go at
the end, just before the rank is listed.
YOU: Sounds good. One other thing, I need to brush up on the hand ranks. You
have any good references for Texas Hold'Em?
BOB: Yeah, check out these Poker Hand Rankings
(http://www.thepokerforum.com/pokerhands.htm). And if you need it, here are the
Rules of Texas Hold'Em (http://www.thepokerforum.com/texasholdem.htm). While
ranking, don't forget the kicker, the next highest card in their hand if
player's are tied. And of course, if -- even after the kicker -- player's are
still tied, put "(winner)" on each appropriate line of output.
YOU: Ok. I still don't understand one thing...
BOB: What's that?
YOU: Why he stayed in with only the 4 and 2 of diamonds? That's just...
BOB: Hey! Show's on in ten minutes! Get to work!
[ Editor's Note:
Matthew included a script for generating test games with his quiz. Here's that
code:
#!/usr/bin/env ruby
FACES = "AKQJT98765432"
SUITS = "cdhs"
srand
# build a deck
deck = []
FACES.each_byte do |f|
SUITS.each_byte do |s|
deck.push(f.chr + s.chr)
end
end
# shuffle deck
3.times do
shuf = []
deck.each do |c|
loc = rand(shuf.size + 1)
shuf.insert(loc, c)
end
deck = shuf.reverse
end
# deal common cards
common = Array.new(5) { deck.pop }
# deal player's hole cards
hole = Array.new(8) { Array.new(2) { deck.pop } }
# output hands
hands = []
all_fold = true
while all_fold do
hands = []
hole.each do |h|
num_common = [0, 3, 4, 5][rand(4)]
if num_common == 5
all_fold = false
end
if num_common > 0
hand = h + common[0 ... num_common]
else
hand = h
end
hands.push(hand.join(' '))
end
end
hands.each { |h| puts h }
-JEG2 ]
T is what people usually use for 10 when discussing the hands online.
> YOU: Why he stayed in with only the 4 and 2 of diamonds? That's just...
This is a bit of didacticism nobody wants, but: 42s is a pretty lousy
starting hand, but maybe:
1. You're in the button or one off, late position makes a lot of
marginal hands playable.
2. You were in the big blind and nobody bet over the blind, or you were
in the little blind, and you figured that the small amount you'd have
to pay to see the flop would be worth it.
3. You're on an exceptionally loose-passive table, so maybe you won't
have to pay much to draw to a flush or low straight, and when you do
nail your hand you'll be able to raise to make your bet payoff. These
tables do happen, though if you're being televised, your opponents are
probably all better players than this ...
4. Or maybe you're going in with the occasional pure, ludicrous bluff,
hoping to show terrible cards at the end to advertise the fact that
you're looser than you actually are.
Francis Hwang
http://fhwang.net/
That is the most amazing domain-specific reply I have ever seen.
So Mr. Lafcadio is also a cardsharp? Who'd have guessed...
Hal
Hmm. I'll keep this in mind at the next RubyConf.
Which is going to be in Vegas, right David?
:)
James
I bet it is.
Michel.
> How do we know if a player folded on the river? (For the ones who
> didn't read the rules or aren't hold'em junkies like myself, the river
> is the 5th community card -- a player could fold here and muck his
> hand)
>
> Or do we just not care? i.e. say he won anyways even though he mucked
> it.
Hmm, the quiz doesn't seem to account for this, so we should probably
just score it, in my non Texas Hold'em Junkie opinion. ;)
James Edward Gray II
I don't know if you could call me a cardsharp yet, otherwise I wouldn't
have to write code for a living ...
Francis Hwang
http://fhwang.net/
Oh, that might be a bad bet:
http://www.rubycentral.org/conference/
Oct. 14 - Oct. 16, 2005
(Facility TBA)
San Diego, CA
But still within driving distance for me. Nice.
James
--
http://www.ruby-doc.org
http://www.rubyxml.com
http://catapult.rubyforge.com
http://orbjson.rubyforge.com
http://ooo4r.rubyforge.com
http://www.jamesbritt.com
I'd love to be able to say the same.
Michel
Hint: living in Mar del Plata, Argentina. Earning pesos instead of
dollars or euros.
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
#!/usr/bin/env ruby
SUITS = %w(c d h s)
FACES = %w(A K Q J T 9 8 7 6 5 4 3 2)
RANKS = {
:royal_flush => 'Royal Flush',
:straight_flush => 'Straight Flush',
:four_of_a_kind => 'Four of a Kind',
:full_house => 'Full House',
:flush => 'Flush',
:straight => 'Straight',
:three_of_a_kind => 'Three of a Kind',
:two_pair => 'Two Pair',
:pair => 'Pair',
:high_card => 'High Card',
:fold => ''
}
class Hand
def initialize(line)
@cards = line.split
@faces = Hash.new { [] }
@suits = Hash.new { [] }
@count = Hash.new { [] }
@cards.each do |card|
f = FACES.index(card[0].chr)
s = SUITS.index(card[1].chr)
@faces[f] = @faces[f] << s
@suits[s] = @suits[s] << f
end
@faces.keys.each do |face|
n = @faces[face].size
@count[n] = @count[n] << face
end
@rank = rank_hand
end
def rank_hand
return :fold if @cards.size < 7
return :royal_flush if @suits.keys.any? do |suit|
(0..5).all? do |face|
@suits[suit].include? face
end
end
return :straight_flush if @suits.keys.any? do |suit|
high = @suits[suit].min
(high..high+5).all? do |face|
@suits[suit].include? face
end
end
return :four_of_a_kind if not @count[4].empty?
return :full_house if @count[3].size == 2 or (@count[3].size == 1
and not @count[2].empty?)
return :flush if @suits.keys.any? do |suit|
@suits[suit].size >= 5
end
return :straight if @faces.keys.any? do |high|
(high..high+5).all? do |face|
@faces.keys.include? face
end
end
return :three_of_a_kind if @count[3].size == 1
return :two_pair if @count[2].size >= 2
return :pair if @count[2].size == 1
:high_card
end
attr_reader :cards, :rank
end
def main
hands = $<.collect { |l| Hand.new(l.chomp) }
hands.each do |h|
puts "#{h.cards.join(' ')} #{RANKS[h.rank]}"
end
end
main
Thanks as always for the quiz.
Patrick
----------------------------------------------------------------------------------
#!ruby -w
class Card
SUITS = "cdhs"
FACES = "L23456789TJQKA"
SUIT_LOOKUP = {
'c' => 0,
'd' => 1,
'h' => 2,
's' => 3,
'C' => 0,
'D' => 1,
'H' => 2,
'S' => 3,
}
FACE_VALUES = {
'L' => 1, # this is a magic low ace
'2' => 2,
'3' => 3,
'4' => 4,
'5' => 5,
'6' => 6,
'7' => 7,
'8' => 8,
'9' => 9,
'T' => 10,
'J' => 11,
'Q' => 12,
'K' => 13,
'A' => 14,
}
def Card.face_value(face)
if (face)
FACE_VALUES[face] - 1
else
nil
end
end
def build_from_string(card)
build_from_face_suit(card[0,1], card[1,1])
end
def build_from_value(value)
@value = value
@suit = value / FACES.size()
@face = (value % FACES.size())
end
def build_from_face_suit(face, suit)
@face = Card::face_value(face)
@suit = SUIT_LOOKUP[suit]
@value = (@suit * FACES.size()) + (@face - 1)
end
def build_from_face_suit_values(face, suit)
build_from_value((face - 1) + (suit * FACES.size()))
end
# got a little carried away with this constructor ;-)
def initialize(*value)
if (value.size == 1)
if (value[0].respond_to?(:to_str))
build_from_string(value[0])
elsif (value[0].respond_to?(:to_int))
build_from_value(value[0])
end
elsif (value.size == 2)
if (value[0].respond_to?(:to_str) && value[1].respond_to?(:to_str))
build_from_face_suit(value[0], value[1])
elsif (value[0].respond_to?(:to_int) && value[1].respond_to?(:to_int))
build_from_face_suit_values(value[0], value[1])
end
end
end
attr_reader :suit, :face, :value
def to_s
FACES[@face].chr + SUITS[@suit].chr
end
end
class Deck
def shuffle
deck_size = @cards.size
(deck_size * 2).times do
pos1, pos2 = rand(deck_size), rand(deck_size)
@cards[pos1], @cards[pos2] = @cards[pos2], @cards[pos1]
end
end
def initialize
@cards = []
Card::SUITS.each_byte do |suit|
# careful not to double include the aces...
Card::FACES[1..-1].each_byte do |face|
@cards.push(Card.new(face.chr, suit.chr))
end
end
shuffle()
end
def deal
@cards.pop
end
def empty?
@cards.empty?
end
end
class Hand
def initialize(cards = [])
if (cards.respond_to?(:to_str))
@hand = cards.scan(/\S\S/).map { |str| Card.new(str) }
else
@hand = cards
end
end
attr_reader :hand
def face_values
@hand.map { |c| c.face }
end
def by_suit
Hand.new(@hand.sort_by { |c| [c.suit, c.face] }.reverse)
end
def by_face
Hand.new(@hand.sort_by { |c| [c.face, c.suit] }.reverse)
end
def =~ (re)
re.match(@hand.join(' '))
end
def royal_flush?
if (md = (by_suit =~ /A(.) K\1 Q\1 J\1 T\1/))
[[10], (md[0] + ' ' + md.pre_match + ' ' +
md.post_match).gsub(/\s+/, ' ')]
else
false
end
end
def delta_transform(use_suit = false)
aces = @hand.select { |c| c.face == Card::face_value('A') }
aces.map! { |c| Card.new(1,c.suit) }
base = if (use_suit)
(@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
else
(@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
end
result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
if (prev_card)
delta = prev_card - card.face
else
delta = 0
end
delta = 'x' if (delta > 9 || delta < 0) # does not really
matter for my needs
delta_hand += delta.to_s + card.to_s + ' '
[delta_hand, card.face]
end
# we just want the delta transform, not the last cards face too
result[0]
end
def fix_low_ace_display(arranged_hand)
# remove card deltas (this routine is only used for straights)
arranged_hand.gsub!(/\S(\S\S)\s+/, "\\1 ")
# Fix "low aces"
arranged_hand.gsub!(/L(\S)/, "A\\1")
# Remove duplicate aces (this will not work if you have multiple
decks or wild cards)
arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
# cleanup white space
arranged_hand.gsub!(/\s+/, ' ')
arranged_hand.gsub(/\s+$/, '') # careful to use gsub as gsub!
can return nil here
end
def straight_flush?
if (md = (/.(.)(.) 1.\2 1.\2 1.\2 1.\2/.match(delta_transform(true))))
high_card = Card::face_value(md[1])
arranged_hand = fix_low_ace_display(md[0] + ' ' + md.pre_match +
' ' + md.post_match)
[[9, high_card], arranged_hand]
else
false
end
end
def arrange_hand(md)
hand = if (md.respond_to?(:to_str))
md
else
md[0] + ' ' + md.pre_match + md.post_match
end
hand.gsub!(/\s+/, ' ')
hand.gsub(/\s+$/,'')
end
def four_of_a_kind?
if (md = (by_face =~ /(.). \1. \1. \1./))
# get kicker
(md.pre_match + md.post_match).match(/(\S)/)
[[8, Card::face_value(md[1]), Card::face_value($1)], arrange_hand(md)]
else
false
end
end
def full_house?
if (md = (by_face =~ /(.). \1. \1. (.*)(.). \3./))
arranged_hand = arrange_hand(md[0] + ' ' + md.pre_match + ' ' +
md[2] + ' ' + md.post_match)
[[7, Card::face_value(md[1]), Card::face_value(md[3])], arranged_hand]
elsif (md = (by_face =~ /((.). \2.) (.*)((.). \5. \5.)/))
arranged_hand = arrange_hand(md[4] + ' ' + md[1] + ' ' +
md.pre_match + ' ' + md[3] + ' ' + md.post_match)
[[7, Card::face_value(md[5]), Card::face_value(md[2])], arranged_hand]
else
false
end
end
def flush?
if (md = (by_suit =~ /(.)(.) (.)\2 (.)\2 (.)\2 (.)\2/))
[[6, Card::face_value(md[1]), *(md[3..6].map { |f|
Card::face_value(f) })], arrange_hand(md)]
else
false
end
end
def straight?
if (md = (/.(.). 1.. 1.. 1.. 1../.match(delta_transform)))
high_card = Card::face_value(md[1])
arranged_hand = fix_low_ace_display(md[0] + ' ' + md.pre_match +
' ' + md.post_match)
[[5, high_card], arranged_hand]
else
false
end
end
def three_of_a_kind?
if (md = (by_face =~ /(.). \1. \1./))
# get kicker
arranged_hand = arrange_hand(md)
arranged_hand.match(/(?:\S\S ){3}(\S)\S (\S)/)
[[4, Card::face_value(md[1]), Card::face_value($1),
Card::face_value($2)], arranged_hand]
else
false
end
end
def two_pair?
if (md = (by_face =~ /(.). \1.(.*) (.). \3./))
# get kicker
arranged_hand = arrange_hand(md[0] + ' ' + md.pre_match + ' ' +
md[2] + ' ' + md.post_match)
arranged_hand.match(/(?:\S\S ){4}(\S)/)
[[3, Card::face_value(md[1]), Card::face_value(md[3]),
Card::face_value($1)], arranged_hand]
else
false
end
end
def pair?
if (md = (by_face =~ /(.). \1./))
# get kicker
arranged_hand = arrange_hand(md)
arranged_hand.match(/(?:\S\S ){2}(\S)\S\s+(\S)\S\s+(\S)/)
# (' ' + md.pre_match + md.post_match).match(/^\s+(\S)\S\s+(\S)\S\s+(\S)/)
[[2, Card::face_value(md[1]), Card::face_value($1),
Card::face_value($2), Card::face_value($3)], arranged_hand]
else
false
end
end
def highest_card?
result = by_face
[[1, *result.face_values[0..4]], result.hand.join(' ')]
end
OPS = [
['Royal Flush', :royal_flush? ],
['Straight Flush', :straight_flush? ],
['Four of a kind', :four_of_a_kind? ],
['Full house', :full_house? ],
['Flush', :flush? ],
['Straight', :straight? ],
['Three of a kind', :three_of_a_kind?],
['Two pair', :two_pair? ],
['Pair', :pair? ],
['Highest Card', :highest_card? ],
]
def hand_rating
OPS.map { |op| (method(op[1]).call()) ? op[0] : false }. find { |v| v }
end
def score
OPS.map { |op| method(op[1]).call() }.find([0]) { |score| score }
end
def take_card(card)
@hand.push(card)
end
def arranged_hand
score[1] + " (#{hand_rating})"
end
def just_cards
@hand.join(" ")
end
def to_s
just_cards + " (" + hand_rating + ")"
end
end
class Player
def initialize(name, deck)
@name = name
@hand = Hand.new
2.times { @hand.take_card(deck.deal()) }
@folded = false
end
def folded?
@folded
end
def take_card(card)
@hand.take_card(card)
end
def fold?(players)
unless (folded?)
if (players)
folded_count = players.inject(0) { |count, p| (p.folded?) ?
count + 1 : count }
@folded = rand(players.size - folded_count) > (folded_count)
else
@folded = (rand(10) <= 1)
end
end
folded?
end
def score
(folded?) ? [[0]] : @hand.score
end
def arranged_hand
@name + ' ' +
if (folded?)
@hand.just_cards + ' (folded)'
else
@hand.arranged_hand
end
end
def to_s
@name + ' ' +
if (folded?)
@hand.just_cards + ' (folded)'
else
@hand.to_s
end
end
def <=>(other)
score <=> other.score
end
end
class TexasHoldEm
def initialize(player_count)
@deck = Deck.new
@common_cards = Array.new(5) { @deck.deal }
@players = (1..player_count).inject([]) { |players, num| players
<< Player.new("Player #{num}", @deck) }
end
def game_over?
@common_cards.empty?
end
def play_round
unless game_over?
card = @common_cards.pop
@players.each do |p|
unless p.fold?(@players)
p.take_card(card)
end
end
end
game_over?
end
def rank_players!
@players = @players.sort.reverse
end
def arranged_players
@players.inject('') { |result, player| result +=
player.arranged_hand + "\n" }
end
def to_s
@players.join("\n")
end
end
if __FILE__ == $0
srand
game = TexasHoldEm.new(5)
round = 1
until game.game_over?
puts "\nRound #{round}"
puts game
game.play_round
round += 1
end
puts "\nRound #{round}"
puts game
game.rank_players!
puts "\nFinal Ranking"
puts game.arranged_players
end
Well, here is mine. I agree it was a fun one, except in the very long part
where I try to match different plays (what is the correct word?). Poker has
too many!
The strategy was nothing OO, or elegant. I just ordered the hand by rank (or
by suit when checking for flush) and tried to match some regexes. Probably
has bugs, the supplied card generator program never yielded a Royal Flush...
Here it is:
RANKS = "AKQJT98765432"
INTERNAL = "ABCDEFGHIJKLM"
RANKS_REVERSED = RANKS.reverse
ACE = "A"
LOW_ACE = "N"
# "plays"? maybe "figures"? (?)
PLAYS = {
"Royal Flush" => 10,
"Straight Flush" => 9,
"Four of a Kind" => 8,
"Full House" => 7,
"Flush" => 6,
"Straight" => 5,
"Three of a Kind" => 4,
"Double Pair" => 3,
"Pair" => 2,
"High Card" => 1,
"" => 0
}
class String
# split, do something with the array except finding, join, replace
# I never find the right method name...
def do! (method, low_ace=false, &block)
s = self.tr RANKS, INTERNAL
s.tr!(ACE, LOW_ACE) if low_ace
arr = s.split.send(method, &block)
s = arr.join(" ")
s.tr!(LOW_ACE, ACE) if low_ace
replace s.tr(INTERNAL, RANKS)
self
end
end
module Enumerable
# yields n items each time (but advances by one)
def each_n (n)
a = []
each do |cur|
a << cur
next if a.size < n
yield *a
a.shift
end
end
end
# moves the used cards to the left, calculates hand score,
# creates hash to insert in hands array
# hand is the hand, name is name of the play (game?)
# m is the matched play (game? hand?)
# groups are the indices of the groups in m that form the hand
# I repeat, I'm very bad choosing method names
def finish (hand, name, m, *groups)
# extract the matched play (?) from hand,
# sort its parts from biggest to smallest (for the full house)
duphand = hand.dup
groups = groups.map {|g|
b = m.begin(g); e = m.end(g)
hand[b...e] = "*" * (e-b)
duphand.slice(b...e) }.
sort_by {|g| -g.size }
hand.delete!("*")
# if there are any remaining cards (kickers), sort them
if hand.size > 2
hand.do!(:sort)
end
# reinsert hand at the beginning
hand = groups.join(" ") + " " + hand
hand.squeeze!
# calculate score
# the score is a 5-digit hex number, each digit with
# the rank of the card at that position
# ups... can't use String#do! here :(
score = hand.split[0,5].inject(1) { |sc, card|
(sc << 4) + RANKS_REVERSED.index(card[0].chr) }
# build the hash and return it
{ :hand => hand, :name => name, :score => score }
end
hands = []
while line = gets
line.chomp!
if line.split.size != 7
hands << {:hand => line, :name => "", :score => 0}
next
end
line.do!(:sort)
catch :found do
# try to find...
# ... straight (and royal) flush
RANKS.split(//).each_n(5) do |a,b,c,d,e|
r = /(#{a}(.) #{b}\2 #{c}\2 #{d}\2 #{e}\2)/
if m = r.match(line)
hands << finish(line,
(m[0][0]==?A ? # if it starts with ace
"Royal Flush" : # it's royal
"Straight Flush"),
m, 1)
throw :found
end
end
# try to find straight flush with low ace
line.do!(:sort, true)
if m = /(5(.) 4\2 3\2 2\2 A\2)/.match(line)
hands << finish(line, "Straight Flush", m, 1)
throw :found
end
# ... four of a kind
line.do!(:sort)
if m = /((\w). \2. \2. \2.)/.match(line)
hands << finish(line, "Four of a Kind", m, 1)
throw :found
end
# ... full house
if m = /((\w)\w \2\w \2\w).*((\w)\w \4\w)/.match(line) or
m = /((\w)\w \2\w).*((\w)\w \4\w \4\w)/.match(line)
hands << finish(line, "Full House", m, 1, 3)
throw :found
end
# ...flush
# sort by color
line.do!(:sort_by){|card| [card[1],card[0]]}
if m = /(\w(\w) \w\2 \w\2 \w\2 \w\2)/.match(line)
hands << finish(line, "Flush", m, 1)
throw :found
end
# ...straight
line.do!(:sort)
RANKS.split(//).each_n(5) do |a,b,c,d,e|
r = /(#{a}. #{b}. #{c}. #{d}. #{e}.)/
if m = r.match(line)
hands << finish(line, "Straight", m, 1)
throw :found
end
end
# ...straight, low ace
line.do!(:sort, true)
if m = /(5. 4. 3. 2. A.)/.match(line)
hands << finish(line, "Straight", m, 1)
throw :found
end
# ... three of a kind
line.do!(:sort)
if m = /((\w)\w \2\w \2\w)/.match(line)
hands << finish(line, "Three of a Kind", m, 1)
throw :found
end
# ... double pair
if m = /((\w)\w \2\w).*((\w)\w \4\w)/.match(line)
hands << finish(line, "Double Pair", m, 1, 3)
throw :found
end
# ...pair
if m = /((\w)\w \2\w)/.match(line)
hands << finish(line, "Pair", m, 1)
throw :found
end
# ... high card.. FINISH AT LAST!!!
if m = /^(\w\w)/.match(line)
hands << finish(line, "High Card", m, 1)
throw :found
end
raise "This program is buggy. Terminating."
end
end
# get the winner hand
winner = hands.sort_by {|h| [-PLAYS[h[:name]], -h[:score]] }.first
# print the lines
hands.each do |h|
print h[:hand], " ", h[:name]
if winner[:name] != "" &&
h[:name] == winner[:name] &&
h[:score] == winner[:score]
print " (winner)"
end
puts
end
Alas I have forgotton other people and so to make amends here is the
reformatted quiz submission.
Grinning :-)
Patrick
----------------------------------------------------------------------------------------------------------------
#!ruby -w
attr_reader :suit, :face, :value
def deal
@cards.pop
end
def arrange_hand(md)
hand = if (md.respond_to?(:to_str))
md
else
md[0] + ' ' + md.pre_match + md.post_match
end
hand.gsub!(/\s+/, ' ')
hand.gsub(/\s+$/,'')
end
def royal_flush?
if (md = (by_suit =~ /A(.) K\1 Q\1 J\1 T\1/))
[[10], arrange_hand(md)]
else
false
end
end
def delta_transform(use_suit = false)
aces = @hand.select { |c| c.face == Card::face_value('A') }
aces.map! { |c| Card.new(1,c.suit) }
base = if (use_suit)
(@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
else
(@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
end
result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
if (prev_card)
delta = prev_card - card.face
else
delta = 0
end
# does not really matter for my needs
delta = 'x' if (delta > 9 || delta < 0)
delta_hand += delta.to_s + card.to_s + ' '
[delta_hand, card.face]
end
# we just want the delta transform, not the last cards face too
result[0]
end
def fix_low_ace_display(arranged_hand)
# remove card deltas (this routine is only used for straights)
arranged_hand.gsub!(/\S(\S\S)\s+/, "\\1 ")
# Fix "low aces"
arranged_hand.gsub!(/L(\S)/, "A\\1")
# Remove duplicate aces (this will not work if you have
# multiple decks or wild cards)
arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
# cleanup white space
arranged_hand.gsub!(/\s+/, ' ')
# careful to use gsub as gsub! can return nil here
arranged_hand.gsub(/\s+$/, '')
end
def straight_flush?
if (md = (/.(.)(.)(?: 1.\2){4}/.match(delta_transform(true))))
high_card = Card::face_value(md[1])
arranged_hand = fix_low_ace_display(md[0] + ' ' +
md.pre_match + ' ' + md.post_match)
[[9, high_card], arranged_hand]
else
false
end
end
def four_of_a_kind?
[
[
}.find { |v| v }
(requires card.rb in the same directory)
An explanation (and comments) will come later tonight.
Cheers,
Dave
This is a pretty long project. Here are some highlights:
# each hand type has a name and a function taking a hand and returning it,
# sorted, or nil if the hand doesn't match this type.
HandTypes = [
['Royal Flush', proc {|hand|
if hand.find_all{|c|c.value >= 10}.map(:suit).frequencies(5).size >= 1
Poker.find_straight(hand)
end }],
['Straight Flush', proc {|hand|
suit, count = *hand.map(:suit).frequencies(5)[0]
if count && count >= 1
if straight = Poker.find_straight(hand.find_all {|card| card.suit ==
suit })
result = hand.dup
result.delete_if {|card| straight.include?(card) }
straight + result.sort_by_most_frequent(:value)
end
end }],
['Four of a Kind', proc {|hand|
if hand.map(:value).frequencies(2).size >= 4
hand.sort_by_most_frequent :value
end }],
['Flush', proc {|hand|
if hand.map(:suit).frequencies(5).size >= 1
hand.sort_by_most_frequent :suit
end }],
['Straight', proc {|hand|
Poker.find_straight(hand) }],
['Three of a Kind', proc {|hand|
if hand.map(:value).frequencies(3).size >= 1
hand.sort_by_most_frequent :value
end }],
['Two Pair', proc {|hand|
if hand.map(:value).frequencies(2).size >= 2
hand.sort_by_most_frequent :value
end }],
['Pair', proc {|hand|
if hand.map(:value).frequencies(2).size >= 1
hand.sort_by_most_frequent :value
end }],
['High Card', proc {|hand|
hand.sort.reverse }]
]
#
# Returns [n, "Hand type", ordered_cards]
# The n at the front is bigger for better hands, so that
# you can sort by hand_value.
#
def self.hand_value(cards, min_cards = 7) # self #=> Poker:Module
if cards.size >= min_cards
HandTypes.each_with_index do |hand_type, index|
hand_match = hand_type[1].call(cards)
if hand_match
# return EvaluatedHand.new(hand_match, hand_type[0], [HandTypes.size -
index, hand_match])
return [
HandTypes.size - index,
hand_type[0],
hand_match
]
end
end
end
[0, '', cards.sort_by_most_frequent(:value)]
end
# then it all comes together like this:
hands = []
# get hands from input and evaluate them
input.each do |line|
hands << line.to_cards.poker_value
end
# determine the winner
winner = hands.inject([]) do |memo, hand|
[memo, hand].max
end
# output each hand and its value
hands.each do |hand|
puts "#{hand[2]} #{hand[1]} #{'(winner)' if hand == winner}"
end
That plus a Poker::find_straight and Array#frequencies and that's pretty
much all there is to my answer.
Cheers,
Dave
"Dave Burt" <da...@burt.id.au> faked:
Why not just delete the low aces (gsub!(/L./, ""))? What am I missing?
BTW, I liked how you detected straights using the delta transform. Clever
idea (for me at least :).
I liked the delta transform as well, of course it was the cause of the
bug (delta 0 when pairs were encountered) - if fixed it by shuffling
the delta zeros (excluding the first card) to the back of the hand.
Patrick
> I liked the delta transform as well, of course it was the cause of the
> bug (delta 0 when pairs were encountered) - if fixed it by shuffling
> the delta zeros (excluding the first card) to the back of the hand.
And since I never saw that version of the program hit the list, here it
is:
#!ruby -w
attr_reader :suit, :face, :value
def deal
@cards.pop
end
result[0].chop
end
def fix_low_ace_display(arranged_hand)
# remove card deltas (this routine is only used for straights)
arranged_hand.gsub!(/\S(\S\S)\s*/, "\\1 ")
# Fix "low aces"
arranged_hand.gsub!(/L(\S)/, "A\\1")
# Remove duplicate aces (this will not work if you have
# multiple decks or wild cards)
arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
# cleanup white space
arranged_hand.gsub!(/\s+/, ' ')
# careful to use gsub as gsub! can return nil here
arranged_hand.gsub(/\s+$/, '')
end
def straight_flush?
def straight?
result = false
if hand.size > 5
transform = delta_transform
# note we can have more than one delta 0 that we
# need to shuffle to the back of the hand
until transform.match(/^\S{3}( [1-9x]\S\S)+( 0\S\S)*$/) do
transform.gsub!(/(\s0\S\S)(.*)/, "\\2\\1")
end
if (md = (/.(.). 1.. 1.. 1.. 1../.match(transform)))
high_card = Card::face_value(md[1])
arranged_hand = fix_low_ace_display(md[0] + ' ' +
md.pre_match + ' ' + md.post_match)
result = [[5, high_card], arranged_hand]
end
end
end
def take_card(card)
@hand.push(card)
end
def folded?
@folded
end
def take_card(card)
@hand.take_card(card)
end
end
def game_over?
@common_cards.empty?
end
game_over?
end
__END__
James Edward Gray II
Patrick
Input:
As Ks Qs Js Ts 9s 8s
Ad Ks Qs Js Ts 9s Ac
Ts 9d 9s 3s 9h 9c 2s
3s 9d 9s 4s 9h 9c 2s
3s 9d 9s 4s 9h 9c 3d
3s 3d 2s 2c 2h 9c 4s
3s 3d Ts 2s 2h 9c 3h
8d 6d Ts 7d 5d Jd Kd
8d Qs Th 7h 9s Js Kd
Ad 2s 3h 4h 5s Js Kd
Js Ts 8c 7d 9s Td 4d
Js Ts 8c 7d 9s Td Tc
3s 3d Ts As 2h 9c 3h
3s 3d Ts As 2h 9c 2s
3s 3d Ts As 4h 9c 2s
8d 6s Th 7h 5s Js Kd
Output:
As Ks Qs Js Ts 9s 8s Royal Flush (winner)
Ks Qs Js Ts 9s Ad Ac Straight Flush
9h 9d 9c 9s 2s 3s Ts Full House *** should be 4 of a kind
9h 9d 9c 9s 2s 3s 4s Full House *** should be 4 of a kind
9h 9d 9c 9s 3s 3d 4s Full House *** should be 4 of a kind
2c 2h 2s 3s 3d 4s 9c Full House
3s 3d 3h 2s 2h 9c Ts Full House
8d 6d Kd Jd 5d 7d Ts Flush
Kd Qs Js Th 9s 8d 7h Straight
Ad Kd Js 5s 4h 3h 2s High Card *** Should be a straight Ace low
Js Td 9s 8c 7d 4d Straight
Tc Ts Td 7d 8c 9s Js Full House *** Should be a straight (this one
threw my first cut)
3s 3d 3h 2h 9c Ts As Full House *** Should be 3 of a kind
2s 2h 3d 3s 9c Ts As Two Pair
3s 3d 2s 4h 9c Ts As Pair
Kd Js Th 8d 7h 6s 5s High Card
..or a straight with a pair inside it!
Thanks to the last message from Patrick Hurley, I've found the same bug in
my program. Here is the diff, and after that the full new version.
Thanks Patrick.
The diff:
--- told.rb 2005-03-23 18:15:54.062826872 +0100
+++ t.rb 2005-03-23 18:10:32.546704752 +0100
@@ -134,17 +134,24 @@
# ...straight
line.do!(:sort)
+ # take out pairs inside a possible straight!
+ # -- thanks Patrick Hurley
+ pairs = ""
+ line_wo_pairs =
+ line.gsub(/((\w). )((\2. ?)+)/) { pairs << $3; $1 }
RANKS.split(//).each_n(5) do |a,b,c,d,e|
r = /(#{a}. #{b}. #{c}. #{d}. #{e}.)/
- if m = r.match(line)
- hands << finish(line, "Straight", m, 1)
+ if m = r.match(line_wo_pairs)
+ hands << finish(line_wo_pairs+" "+pairs,
+ "Straight", m, 1)
throw :found
end
end
# ...straight, low ace
- line.do!(:sort, true)
- if m = /(5. 4. 3. 2. A.)/.match(line)
- hands << finish(line, "Straight", m, 1)
+ line_wo_pairs.do!(:sort, true)
+ if m = /(5. 4. 3. 2. A.)/.match(line_wo_pairs)
+ hands << finish(line_wo_pairs+" "+pairs,
+ "Straight", m, 1)
throw :found
end
The full program:
# groups are the groups in m that form the hand
hands = []
"Royal Flush" :
# take out pairs inside a possible straight!
# -- thanks Patrick Hurley
pairs = ""
line_wo_pairs =
line.gsub(/((\w). )((\2. ?)+)/) { pairs << $3; $1 }
RANKS.split(//).each_n(5) do |a,b,c,d,e|
r = /(#{a}. #{b}. #{c}. #{d}. #{e}.)/
if m = r.match(line_wo_pairs)
hands << finish(line_wo_pairs+" "+pairs,
"Straight", m, 1)
throw :found
end
end
# ...straight, low ace
line_wo_pairs.do!(:sort, true)
if m = /(5. 4. 3. 2. A.)/.match(line_wo_pairs)
hands << finish(line_wo_pairs+" "+pairs,
Bahh... I forgot about the straight flush. The change should've been more
above. Please disregard the version in my last message and use this one.
Sorry about the spam (at least it is the sortest solution :). If I find more
bugs I should send the new versions to JGE2 directly.
Here it is:
hands = []
# take out pairs inside a possible straight!
# -- thanks Patrick Hurley
pairs = ""
line_wo_pairs =
line.gsub(/((\w). )((\2. ?)+)/) { pairs << $3; $1 }
catch :found do
# try to find...
# ... straight (and royal) flush
RANKS.split(//).each_n(5) do |a,b,c,d,e|
r = /(#{a}(.) #{b}\2 #{c}\2 #{d}\2 #{e}\2)/
if m = r.match(line_wo_pairs+" "+pairs)
hands << finish(line_wo_pairs+" "+pairs,
(m[0][0]==?A ?
"Royal Flush" :
"Straight Flush"),
m, 1)
throw :found
end
end
# try to find straight flush with low ace
line_wo_pairs.do!(:sort, true)
if m = /(5(.) 4\2 3\2 2\2 A\2)/.match(line_wo_pairs)
hands << finish(line_wo_pairs+" "+pairs,
"Straight Flush", m, 1)
throw :found
end
# ... four of a kind
line.do!(:sort)
if m = /((\w). \2. \2. \2.)/.match(line)
hands << finish(line, "Four of a Kind", m, 1)
throw :found
end
# ... full house
if m = /((\w)\w \2\w \2\w).*((\w)\w \4\w)/.match(line) or
m = /((\w)\w \2\w).*((\w)\w \4\w \4\w)/.match(line)
hands << finish(line, "Full House", m, 1, 3)
throw :found
end
# ...flush
# sort by color
line.do!(:sort_by){|card| [card[1],card[0]]}
if m = /(\w(\w) \w\2 \w\2 \w\2 \w\2)/.match(line)
hands << finish(line, "Flush", m, 1)
throw :found
end
# ...straight
line.do!(:sort)
> Sorry about the spam (at least it is the sortest solution :). If I
> find more
> bugs I should send the new versions to JGE2 directly.
Most seem pretty tolerant of our ramblings. I prefer solutions stay on
this list, for all to see. We'll revise that when we start getting
hate mail about the volume. ;)
James Edward Gray II
P.S. Nice solution.
Ok, but better I'll better post only diffs, or we'll start getting it very
soon :).
I hope this is the last remnant of that bug...
--- told.rb 2005-03-23 20:33:24.332595224 +0100
+++ t.rb 2005-03-23 20:33:39.129345776 +0100
@@ -140,7 +140,7 @@
end
# ...straight
- line.do!(:sort)
+ line_wo_pairs.do!(:sort)
RANKS.split(//).each_n(5) do |a,b,c,d,e|
r = /(#{a}. #{b}. #{c}. #{d}. #{e}.)/
if m = r.match(line_wo_pairs)
> P.S. Nice solution.
Thanks :))
Thanks Pat, I will look at this later today when I get the time. I will add
some more tests than that, too, to try and trap another suspected bug: I
fear Two pairs are not evaluated correctly by highest pair then second pair.
These example output lines show this:
2s 2h 3d 3s 9c Ts As Two Pair # from your test
9c 9d Ks Kd 3c 6d Ah Two Pair # from the quiz definition
That's what you get for programming after bed-time.
I had already written card.rb for an implementation of blackjack
(blackjack.rb).
I haven't had a chance to look over any solutions in detail yet, but I saw
your "delta transform" and its use and am intrigued - what is that?
Cheers,
Dave
Patrick
There was something interesting in all the solutions though, so I do recommend
browsing through them if you haven't already. I know I'm always saying that. I
guess it's always true.
I'm going to show Patrick Hurley's solution below. Patrick resubmitted just to
defend against my rant about how programs should stay within an 80 character
line limit. My argument wasn't meant as an attack on any submissions, but I
still appreciate Patrick's efforts. Here's the start of the code:
#!ruby -w
attr_reader :suit, :face, :value
# ...
That's the Card class Patrick uses for tracking individual cards. It looks like
a lot of code, but it's mostly a single constructor that accepts many different
forms of initialization. initialize() breaks down the parameters and hands them
off to the various build_from_... methods. Those build methods should probably
be private, leaning on initialize() as their interface. Once you get past
construction, you'll see that Card just contains a suit, face, and value.
Glance at build_from_face_suit() to see how those break down.
You can see it above and a little more below, but this code has a little
creeping featurism. Patrick was clearly building for the future with the card
handling classes. That's probably a safe bet as card quizzes are fairly common.
Dave Burt reused code from his Blackjack solution this time around. All I'm
saying is, don't be surprised if you see a handful of things in here that never
get used. Agile purists bare with us...
Let's move on to Deck objects:
# ...
def deal
@cards.pop
end
# ...
initialize() just creates and shuffles a deck. deal() pops a card and empty?()
tells you if there are any left. If you read shuffle(), you'll see that it's
just a bunch of random swaps. Not sure why Patrick went this way. I believe
the standard Ruby shuffling idiom is:
@cards.sort_by { rand }
On to the Hand class, but let's take this one in slices:
# ...
class Hand
def initialize(cards = [])
if (cards.respond_to?(:to_str))
@hand = cards.scan(/\S\S/).map { |str| Card.new(str) }
else
@hand = cards
end
end
attr_reader :hand
# ...
initialize() just builds new Hand objects from the lines of input in the quiz by
scan()ing for the two character format. You can also build a Hand from an Array
of Card objects. Then there's the accessor to get them back.
# ...
def face_values
@hand.map { |c| c.face }
end
def by_suit
Hand.new(@hand.sort_by { |c| [c.suit, c.face] }.reverse)
end
def by_face
Hand.new(@hand.sort_by { |c| [c.face, c.suit] }.reverse)
end
# ...
You can use the above methods to request hands by face_values(), by_suit(), or
by_face(). Note that both of the by_... sorts also sort by the other value, as a
secondary condition.
# ...
def =~ (re)
re.match(@hand.join(' '))
end
def arrange_hand(md)
hand = if (md.respond_to?(:to_str))
md
else
md[0] + ' ' + md.pre_match + md.post_match
end
hand.gsub!(/\s+/, ' ')
hand.gsub(/\s+$/,'')
end
# ...
The first method here is an operator overload to allow using regular expressions
on Hand objects. The second method returns a hand string in an order specified
by a MatchData object (the else clause). Whatever cards were matched are put
first, follow by cards preceding the match, and finally trailing cards. This
floats a matched "hand" to the front of the string while keeping the ordering
for any non-matched cards. arrange_hand() can also be called with a string
order (the if clause), but it doesn't do much in these cases except clean up
spacing issues.
From here, we start to get into hand matching code:
# ..
def royal_flush?
if (md = (by_suit =~ /A(.) K\1 Q\1 J\1 T\1/))
[[10], arrange_hand(md)]
else
false
end
end
# ...
This method looks for the coveted royal flush. First it calls by_suit() to
order the cards. Remember that will order suits first, then faces. That makes
it trivial to spot the pattern with a Regexp. When found, royal_flush?()
returns a hand ranking number and the properly arranged hand in an Array, which
is of course a true value in Ruby. false is used when no match is found.
The code then pauses to define a couple more helper methods for spotting the
other hands:
# ...
def delta_transform(use_suit = false)
aces = @hand.select { |c| c.face == Card::face_value('A') }
aces.map! { |c| Card.new(1,c.suit) }
base = if (use_suit)
(@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
else
(@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
end
result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
if (prev_card)
delta = prev_card - card.face
else
delta = 0
end
# does not really matter for my needs
delta = 'x' if (delta > 9 || delta < 0)
delta_hand += delta.to_s + card.to_s + ' '
[delta_hand, card.face]
end
# we just want the delta transform, not the last cards face too
result[0].chop
end
# ...
Dave Burt asked on Ruby Talk what delta_transform() does. Here's the author's
own response:
The delta transform creates a version of the cards where the delta
between card values is in the string, so a regexp can then match a
straight and/or straight flush - I used regexp to match all my cases
with appropriate sort and/or transforms.
Because that's probably easier to understand when you see it, here's a typical
return value from delta_tranform():
"0Jh 38h xJd 38d 44d 13d x8c"
The extra character preceding each card shows the drop from the previous card
rank. The jack is the first card, so it shows a 0 drop. The eight is then down
3, as shown. Tracking increases isn't needed in the solution, so the code just
punts with an x character, as seen with the next jack. All this is just
building up a handy string for pattern matching.
Note that the first couple of lines of delta_transform() add a "low ace" to the
back of the hand for each ace found in the hand. This is for spotting low
straights, but the magic must eventually be undone by:
# ...
def fix_low_ace_display(arranged_hand)
# remove card deltas (this routine is only used for straights)
arranged_hand.gsub!(/\S(\S\S)\s*/, "\\1 ")
# Fix "low aces"
arranged_hand.gsub!(/L(\S)/, "A\\1")
# Remove duplicate aces (this will not work if you have
# multiple decks or wild cards)
arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
# cleanup white space
arranged_hand.gsub!(/\s+/, ' ')
# careful to use gsub as gsub! can return nil here
arranged_hand.gsub(/\s+$/, '')
end
# ...
This just restores the ace back to its usual display.
Now we can see both of those methods put to good use:
# ...
def straight_flush?
if (md = (/.(.)(.)(?: 1.\2){4}/.match(delta_transform(true))))
high_card = Card::face_value(md[1])
arranged_hand = fix_low_ace_display(md[0] + ' ' +
md.pre_match + ' ' + md.post_match)
[[9, high_card], arranged_hand]
else
false
end
end
# ...
This is similar in function to royal_flush?(), but you can see that it uses
delta_transform() to make it easy to match a straight. fix_low_ace_display() is
called on the result, before the method returns.
The rest of the hand methods are very similar. Sort the cards, match a pattern,
return rank and hand or false. Here they are, without further explanation:
# ...
# ...
Now what we really need to know is which one of those hands was found. The code
for that isn't overly complex:
# ...
OPS = [
['Royal Flush', :royal_flush? ],
['Straight Flush', :straight_flush? ],
['Four of a kind', :four_of_a_kind? ],
['Full house', :full_house? ],
['Flush', :flush? ],
['Straight', :straight? ],
['Three of a kind', :three_of_a_kind?],
['Two pair', :two_pair? ],
['Pair', :pair? ],
['Highest Card', :highest_card? ],
]
def hand_rating
OPS.map { |op|
(method(op[1]).call()) ? op[0] : false
}.find { |v| v }
end
def score
OPS.map { |op|
method(op[1]).call()
}.find([0]) { |score| score }
end
# ...
The OPS Array maps hand names to the method that will spot them. With that, you
call call either hand_rating() or score() which will walk the whole list of
tests, then return the first one that was true. hand_rating() returns the name
while score() returns the rank and hand Array from the hand method call.
Finally, Hand has a few more very basic helper methods:
# ...
def take_card(card)
@hand.push(card)
end
def arranged_hand
score[1] + " (#{hand_rating})"
end
def just_cards
@hand.join(" ")
end
def to_s
just_cards + " (" + hand_rating + ")"
end
end
# ...
The only thing to notice there is the arranged_hand() is just a shell over
score() and hand_rating() and to_s() is a shell over just_cards() and
hand_rating().
The rest of Patrick's code goes on to build a complete game of Texas Hold'Em
that plays itself out round by round and displays results as it goes. This is
very interesting stuff, but it doesn't solve the quiz, the way I read it.
Luckily, a solution is easy to finish off from here. Here's my solution to the
quiz, using Partick's classes:
# ...
### code by JEG2 ###
if __FILE__ == $0
best = nil
results = []
ARGF.each_line do |line|
if line.length < 20 # they folded
results << line.chomp
else
hand = Hand.new(line) # rank hand
name = hand.hand_rating
score, arranged = hand.score
if best.nil? or (score[0] <=> best[0]) == 1 # track best
best = [score[0], results.size]
end
results << "#{arranged} #{name}"
end
end
# show results
results.each_with_index do |e, index|
puts(if index == best[1] then "#{e} (winner)" else e end)
end
end
That should be pretty straight forward by this point. I setup variables to
track the best hand and the complete results, parse input, handle folds, score
each hand, remembering to track the best so far, and finally out the results.
That funny compare, (score[0] <=> best[0]) == 1, is because the grade returned
by score is actually an Array of values and Array implements <=> but not >; go
figure. That gets me the following output for the quiz example:
Ks Kd Kc 9s 9d 6d 3c Full house (winner)
Ks Kd 9d 9c Ah 6d 3c Two pair
Ac Qc Ks Kd 9d 3c
9h 5s
Kd 9d 6d 4d 2d Ks 3c Flush
7s Ts Ks Kd 9d
While I'm showing output, check out this neat variation by Derek Wyatt:
9d 9s Kd Ks Kc 3c 6d Full House (Kings over Nines) (winner)
9d 9c Kd Ks Ah 3c 6d Two Pair (Kings and Nines)
Ac Qc Ks Kd 9d 3c
9h 5s
Ks Kd 2d 4d 3c 6d 9d Pair (Kings)
7s Ts Ks Kd 9d
I love the way it gives you extra details about the hand, but as you can see we
don't agree on hand number four. Don't sweat that though, seems everyone had a
good round of bug hunting for this one.
My thanks to all the card sharks out there. I also want to thank Patrick for
writing code I could figure out how to hijack. This summary was definitely a
team effort.
Tomorrow, Tymothy Byrd will hit you with a brain bender you and Ruby can work
together to solve...