From: Patrick Hurley Date: 2005-03-22T00:50:24+09:00 Subject: Re: [QUIZ SOLUTION] Texas Hold'Em (#24) 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