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. And if you need it, here are the Rules of Texas Hold'Em. 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:
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 ]
Quiz Summary
People wrote quite a bit of code to solve this quiz. I don't think it's all that tough, but there are quite a few combinations to check for, which seemed to increase the line count of the solutions.
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:
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
# ...
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:
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
# ...
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:
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:
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?
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 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)/)
[
[
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
# ...
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...