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