[QUIZ] Texas Hold'Em (#24)

R

Ruby Quiz

The three rules of Ruby Quiz:

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:

http://www.rubyquiz.com/

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 ]
 
F

Francis Hwang

YOU: Okay, I was going ask what character to use for 10, but I guess
'T' is it.

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/
 
H

Hal Fulton

Francis said:
T is what people usually use for 10 when discussing the hands online.



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.

That is the most amazing domain-specific reply I have ever seen.

So Mr. Lafcadio is also a cardsharp? Who'd have guessed...


Hal
 
J

James Britt

Hal said:
...

That is the most amazing domain-specific reply I have ever seen.

So Mr. Lafcadio is also a cardsharp? Who'd have guessed...

Hmm. I'll keep this in mind at the next RubyConf.





Which is going to be in Vegas, right David?





:)



James
 
J

James Edward Gray II

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
 
G

Glenn Parker

#!/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.index != cards[i - 1].index + 1)
is_flush = false if
(cards.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
 
M

Matthew D Moss

I'll add my (partial, again) solution... I didn't get a whole lot of
time to work on it, despite having written the original proposal. But
it was fun, so I may work a bit more on it. In any case, just to add
my ideas and approach, my current code determines the rank of each
hand, but that's it -- didn't get to refactoring and determining the
actual winner or sorting cards, checking high, kickers, etc.

#!/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 = @suits << 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
 
D

Dave Burt

I apologise for posting the URL incorrectly. It's really:
http://www.dave.burt.id.au/ruby/poker.rb

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
 
P

Patrick Hurley

Dave I am still looking over your code, but since I got your test data
last week here is some for you). This was a tough one, lots of
combinations - now I know why I don't gamble. Your card abstraction
seems very complete, did it come from some place else? I did some of
the same, but realized I was getting too far afield for the quiz alone
and stopped myself (I can be accused of over engineering sometimes :)

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
 
D

Dave Burt

Patrick Hurley said:
Dave I am still looking over your code, but since I got your test data
last week here is some for you). This was a tough one, lots of
combinations - now I know why I don't gamble. Your card abstraction
seems very complete, did it come from some place else? I did some of
the same, but realized I was getting too far afield for the quiz alone
and stopped myself (I can be accused of over engineering sometimes :)

Patrick

Input:
<snip reams of testing :))>

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
 
P

Patrick Hurley

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.

Patrick
 

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

Similar Threads


Members online

Forum statistics

Threads
474,260
Messages
2,571,039
Members
48,768
Latest member
first4landlord

Latest Threads

Top