Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

[QUIZ] Texas Hold'Em (#24)

9 views
Skip to first unread message

Ruby Quiz

unread,
Mar 18, 2005, 9:41:37 AM3/18/05
to
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 ]


Francis Hwang

unread,
Mar 18, 2005, 11:40:15 AM3/18/05
to

On Mar 18, 2005, at 9:41 AM, Ruby Quiz wrote:
> 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/

Hal Fulton

unread,
Mar 18, 2005, 11:49:39 AM3/18/05
to

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

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


Hal

James Britt

unread,
Mar 18, 2005, 12:37:46 PM3/18/05
to
Hal Fulton wrote:
> ...

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

Michel Martens

unread,
Mar 18, 2005, 12:46:07 PM3/18/05
to
On Sat, 19 Mar 2005 02:37:46 +0900, James Britt
<jamesUN...@neurogami.com> wrote:
> Which is going to be in Vegas, right David?
>
> :)

I bet it is.

Michel.


James Edward Gray II

unread,
Mar 18, 2005, 3:04:23 PM3/18/05
to
On Mar 18, 2005, at 12:02 PM, Derek Wyatt wrote:

> 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

Francis Hwang

unread,
Mar 18, 2005, 7:37:26 PM3/18/05
to

On Mar 18, 2005, at 11:49 AM, Hal Fulton wrote:
> So Mr. Lafcadio is also a cardsharp? Who'd have guessed...

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/

James Britt

unread,
Mar 20, 2005, 1:21:16 AM3/20/05
to


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


Michel Martens

unread,
Mar 20, 2005, 11:06:28 AM3/20/05
to
On Sun, 20 Mar 2005 15:21:16 +0900, James Britt
<jamesUN...@neurogami.com> wrote:
> 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

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.


Glenn Parker

unread,
Mar 20, 2005, 1:36:14 PM3/20/05
to
#!/usr/bin/ruby -w
#
# Quiz 24: Texas Hold'em
# Solution by Glenn Parker

module Combine
# Generate all combinations of +pick+ elements from +items+ array.
def Combine.pick(pick, items, &block)
combine([], 0, pick, items, &block)
end

private

def Combine.combine(set, index, pick, items, &block)
if pick == 0 or index == items.length
yield set
else
set.push(items[index])
combine(set, index + 1, pick - 1, items, &block)
set.pop
combine(set, index + 1, pick, items, &block) if
pick < items.length - index
end
end
end

# One card, with a face [2-9TJQKA] and a suit [shdc].
class Card
attr_reader :face, :suit

Face_Ranks = {
:A => 12, :K => 11, :Q => 10, :J => 9,
:T => 8, :"9" => 7, :"8" => 6, :"7" => 5,
:"6" => 4, :"5" => 3, :"4" => 2, :"3" => 1,
:"2" => 0
}

Suit_Ranks = {
:s => 3, :h => 2, :d => 1, :c => 0
}

def initialize(face_suit)
@face = face_suit[0].chr.to_sym
raise "Invalid face \"#{@face}\"" unless Face_Ranks.has_key?(@face)
@suit = face_suit[1].chr.to_sym
raise "Invalid suit \"#{@suit}\"" unless Suit_Ranks.has_key?(@suit)
freeze
end

def rank # Overall ranking in the deck.
index * 4 + Suit_Ranks[@suit]
end

def index # Ranking, independent of suit.
Face_Ranks[@face]
end

def to_s
@face.to_s + @suit.to_s
end
end

# A typed collection of up to five cards.
class Hand
include Comparable # Hands can be compared.

attr_reader :hand_type, :cards

Hand_Names = [
"Folded",
"High Card",
"Pair",
"Two Pair",
"Three of a Kind",
"Straight",
"Flush",
"Full House",
"Four of a Kind",
"Straight Flush",
"Royal Flush"
]

# Define constants by converting "High Card" to Hand::High_Card = 0.
Hand_Names.each_with_index do |n, i|
const_set(n.tr(" ", "_"), i)
end

def initialize(hand_type, cards)
@hand_type = hand_type
@cards = cards.dup
freeze
end

def to_s
@cards.join(" ") + " " + Hand_Names[@hand_type]
end

def <=>(other)
if @hand_type != other.hand_type
# Hand ranking dominates.
return @hand_type <=> other.hand_type

elsif @hand_type == Flush
# Compare corresponding cards, highest to lowest.
@cards.reverse.zip(other.cards.reverse) do |a, b|
return a.index <=> b.index if a.index != b.index
end
return 0

elsif @hand_type == Two_Pair
# Compare the two highest pairs, then the remaining pairs
self_indices = [@cards[0].index, @cards[2].index].sort!
other_indices = [other.cards[0].index, other.cards[2].index].sort!
if self_indices[1] != other_indices[1]
return self_indices[1] <=> other_indices[1]
else
return self_indices[0] <=> other_indices[0]
end

else
# All others types of hand are compared using their first card.
return @cards[0].index <=> other.cards[0].index
end
end
end

# A collection of seven cards, from which Hands are extracted.
class Deal
attr_reader :all_cards, :best_hand, :kickers

def initialize(card_string)
# Parse and sort the cards. The sorting order chosen here is
# important when extracting and comparing hands later.
@all_cards = card_string.split(/ /).collect do |face_suit|
Card.new(face_suit)
end.sort_by { |card| card.rank }
@hands = []
if @all_cards.length == 7
# Extract all possible hands if we got 7 cards.
find_high_card
find_groups
find_two_pairs_and_full_house
find_straight_and_flush
else
# Otherwise, make a folded hand.
add_hand(Hand::Folded, @all_cards)
end
# Pick the best possible hand and determine the kickers.
@best_hand = @hands.max
@kickers = (@all_cards - @best_hand.cards).sort_by do |card|
-card.rank
end
end

private

def add_hand(hand_type, cards)
@hands << Hand.new(hand_type, cards)
end

def find_high_card
add_hand(Hand::High_Card, [ @all_cards[-1] ])
end

def find_groups
# Find the longest run of each face in @all_cards.
start = 0
while @all_cards[start]
for stop in ((start + 1)..@all_cards.length)
next if @all_cards[stop] and
(@all_cards[start].face == @all_cards[stop].face)
case (stop - start)
when 4:
add_hand(Hand::Four_of_a_Kind, @all_cards[start...stop])
when 3:
add_hand(Hand::Three_of_a_Kind, @all_cards[start...stop])
when 2:
add_hand(Hand::Pair, @all_cards[start...stop])
end
break
end
start = stop
end
end

def find_two_pairs_and_full_house
pairs = @hands.find_all do |h|
h.hand_type == Hand::Pair
end
threes = @hands.find_all do |h|
h.hand_type == Hand::Three_of_a_Kind
end
# Find up to three combinations of two pairs.
if (pairs.length > 1)
Combine.pick(2, pairs) do |pair_hands|
add_hand(Hand::Two_Pair,
pair_hands[0].cards + pair_hands[1].cards)
end
end
# Each combination of a pair and three-of-a-kind is a full house.
pairs.each do |pair|
threes.each do |three|
add_hand(Hand::Full_House, three.cards + pair.cards)
end
end
# Two three-of-a-kinds yield two possible full-houses.
if (threes.length > 1)
add_hand(Hand::Full_House,
threes[0].cards + threes[1].cards[0..1])
add_hand(Hand::Full_House,
threes[1].cards + threes[0].cards[0..1])
end
# We could combine four-of-a-kind and a pair for a full-house
# but four-of-a-kind already beats a full-house.
end

def find_straight_and_flush
# Examine all combinations of five cards
Combine.pick(5, @all_cards) do |cards|
is_flush = true
is_straight = true
1.upto(4) do |i|
is_straight = false if
(cards[i].index != cards[i - 1].index + 1)
is_flush = false if
(cards[i].suit != cards[0].suit)
end
# Add the best hand found in this iteration.
case
when (is_straight and is_flush and cards[0].face == :"T")
add_hand(Hand::Royal_Flush, cards)
when (is_straight and is_flush)
add_hand(Hand::Straight_Flush, cards)
when (is_flush)
add_hand(Hand::Flush, cards)
when (is_straight)
add_hand(Hand::Straight, cards)
end
end
end

end

# A card player that holds a Hand and some kickers.
class Player
attr_reader :hand, :kickers
attr_accessor :wins

def initialize(hand, kickers)
@hand = hand
@kickers = kickers
@wins = false
end

# Return <=> value comparing kickers from another Player.
def compare_kickers(other)
@kickers.zip(other.kickers) do |a_kicker, b_kicker|
return 1 if a_kicker.index > b_kicker.index
return -1 if a_kicker.index < b_kicker.index
end
return 0
end
end

# Read the input.

players = []
while line = gets
line.chomp!
# Take first 20 chars only, making it easy to use previously
# printed results as input for re-testing.
deal = Deal.new(line[0, 20])
players << Player.new(deal.best_hand, deal.kickers)
end

# Find the winner(s).

winners = []
players.each do |player|
if winners.empty?
winners << player
elsif player.hand > winners[0].hand
winners.clear
winners << player
elsif player.hand == winners[0].hand
# Try to resolve ties based on kickers.
comparison = player.compare_kickers(winners[0])
if comparison >= 0
winners.clear if comparison > 0
winners << player
end
end
end
winners.each { |player| player.wins = true }

# Report the results.

players.each do |player|
# Print cards sorted by face with kickers at the end.
print((player.hand.cards + player.kickers).join(" "))
# Print description of hand and (winner) flag
if player.hand.hand_type > 0
print " ", Hand::Hand_Names[player.hand.hand_type]
print " (winner)" if player.wins
end
print "\n"
end

Matthew D Moss

unread,
Mar 20, 2005, 8:30:15 PM3/20/05
to
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[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

Patrick Hurley

unread,
Mar 21, 2005, 10:50:24 AM3/21/05
to
I will concur this one was fun, although it took me more like 4 hours
than the one I planned on spending on it. I think this a complete
solution. There is no smarts in the folding logic, but the player
class is the place to make it smarter. It displays each round
(unranked, but with current "score/status" and then when the game is
over it rearranges the hands to display them based upon their best
hand and ranks them from best to worst (folded hands are just randomly
layed out at the bottom.

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


Carlos

unread,
Mar 21, 2005, 1:44:05 PM3/21/05
to
[Derek Wyatt <tone...@yahoo.ca>, 2005-03-20 15.55 CET]
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> This was a fun one :)
>
> I'm looking forward to seeing other solutions.

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


Patrick Hurley

unread,
Mar 22, 2005, 9:39:23 PM3/22/05
to
Find below a very slightly modified version of my quiz submission
where none of the lines exceed 72 charaters in width. I have become
"lazy" in that my editors on my 20" LCDs have no difficulty with lines
exceeding 100 chars. I have also sometime since I got those new
monitors started printing code in landscape.

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 }

Dave Burt

unread,
Mar 23, 2005, 4:13:34 AM3/23/05
to
http://www.dave.burt.id.au/poker.rb

(requires card.rb in the same directory)

An explanation (and comments) will come later tonight.

Cheers,
Dave


Dave Burt

unread,
Mar 23, 2005, 8:16:31 AM3/23/05
to
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

"Dave Burt" <da...@burt.id.au> faked:

Carlos

unread,
Mar 23, 2005, 9:02:00 AM3/23/05
to
[Patrick Hurley <phu...@gmail.com>, 2005-03-23 03.39 CET]

> Find below a very slightly modified version of my quiz submission
[...]

> 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")

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

Patrick Hurley

unread,
Mar 23, 2005, 9:20:19 AM3/23/05
to
Thanks - I need to keep the low aces so that they appear in the
correct position in the rearranged hand - if I had just deleted them,
straights with the aces low would have been displayed with the ace out
of order.

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

James Edward Gray II

unread,
Mar 23, 2005, 10:08:19 AM3/23/05
to
On Mar 23, 2005, at 8:20 AM, Patrick Hurley wrote:

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

unread,
Mar 23, 2005, 10:30:43 AM3/23/05
to
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


Carlos

unread,
Mar 23, 2005, 12:21:38 PM3/23/05
to
[Carlos <an...@quovadis.com.ar>, 2005-03-21 19.44 CET]
[...]

> Probably has bugs, the supplied card generator program never yielded a
> Royal Flush...

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

Carlos

unread,
Mar 23, 2005, 12:34:20 PM3/23/05
to
[Carlos <an...@quovadis.com.ar>, 2005-03-23 18.21 CET]

> [Carlos <an...@quovadis.com.ar>, 2005-03-21 19.44 CET]
> [...]
> > Probably has bugs, the supplied card generator program never yielded a
> > Royal Flush...
>
> ...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.

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)

James Edward Gray II

unread,
Mar 23, 2005, 2:07:31 PM3/23/05
to
On Mar 23, 2005, at 11:34 AM, Carlos wrote:

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

Carlos

unread,
Mar 23, 2005, 2:56:12 PM3/23/05
to
[James Edward Gray II <ja...@grayproductions.net>, 2005-03-23 20.07 CET]

> On Mar 23, 2005, at 11:34 AM, Carlos wrote:
>
> >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. ;)

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

Dave Burt

unread,
Mar 23, 2005, 3:34:08 PM3/23/05
to
"Patrick Hurley" <phu...@gmail.com> tested:

> 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


Patrick Hurley

unread,
Mar 23, 2005, 4:33:57 PM3/23/05
to
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

Ruby Quiz

unread,
Mar 24, 2005, 9:04:08 AM3/24/05
to
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

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


0 new messages