[QUIZ SOLUTION] Texas Hold'Em (#24)

[Derek Wyatt <tone_hole@yahoo.ca>, 2005-03-20 15.55 CET]

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

This was a fun one :slight_smile:

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

[Carlos <angus@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:

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 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 :frowning:
  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 ?
          "Royal Flush" :
          "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)
    # 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,
          "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

[Carlos <angus@quovadis.com.ar>, 2005-03-23 18.21 CET]

[Carlos <angus@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:

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 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 :frowning:
  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)
  
  # 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)
    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,
          "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

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

James Edward Gray II

P.S. Nice solution.

路路路

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.

[James Edward Gray II <james@grayproductions.net>, 2005-03-23 20.07 CET]

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

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

路路路

On Mar 23, 2005, at 11:34 AM, Carlos wrote: