[SUMMARY] Texas Hold'Em (#24)

R

Ruby Quiz

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:

#!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

# ...

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:

@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:

# ...

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...
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,768
Messages
2,569,575
Members
45,053
Latest member
billing-software

Latest Threads

Top