Skip to content

Instantly share code, notes, and snippets.

@Flambino Flambino/poker_hands.rb Secret
Created Dec 11, 2013

Embed
What would you like to do?
An implementation of poker hand evaluation for CodeReview's weekend challange (includes tests)
require 'test/unit'
# Note: Requires Ruby 1.9+
# Various constants
ACE_LOW = 1
ACE_HIGH = 14
# Use Struct to model a simple Card class
Card = Struct.new :suit, :value
# This class models and evaluates a hand of cards
class Hand
attr_reader :cards
RANKS = {
straight_flush: 8,
four_of_a_kind: 7,
full_house: 6,
flush: 5,
straight: 4,
three_of_a_kind: 3,
two_pair: 2,
pair: 1
}.freeze
def initialize(cards)
raise ArgumentError unless cards.count == 5
@cards = cards.freeze
end
# The hand's rank, as an array containing the hand's
# type and that type's base score
def rank
RANKS.detect { |method, rank| send :"#{method}?" } || [:high_card, 0]
end
# The hand's type (e.g. :flush or :pair)
def type
rank.first
end
# The hand's base score (based on rank)
def base_score
rank.last
end
# The hand's score is an array starting with the
# base score, followed by the kickers.
def score
[base_score] + kickers
end
# Tie-breaking kickers, ordered high to low.
# For straights and flushes only the first (highest)
# value is actually of interest. For repeated cards,
# the first value(s) are the repeated values. E.g.
# a 2, 2, 2, 10, 11 three-of-a-kind the kickers will
# will be [2, 11, 10]
def kickers
repeat_values + (aces_low? ? aces_low_values.reverse : single_values)
end
# If the hand's straight and flush, it's a straight flush
def straight_flush?
straight? && flush?
end
# Is a value repeated 4 times?
def four_of_a_kind?
repeat_counts.include? 4
end
# Three of a kind and a pair make a full house
def full_house?
three_of_a_kind? && pair?
end
# If the hand only contains one suit, it's flush
def flush?
suits.uniq.count == 1
end
# This is the only hand where high vs low aces comes into play.
# The hand is first checked, assuming aces high. If there's no
# straight, it's checked again, this time assuming aces low
def straight?
aces_high_straight? || aces_low_straight?
end
# Is a card value repeated 3 times?
def three_of_a_kind?
repeat_counts.include? 3
end
# Are there 2 instances of repeated card values?
def two_pair?
repeat_counts.count(2) == 2
end
# Any repeating card value?
def pair?
repeat_counts.include? 2
end
# Actually just an alias for aces_low_straight?
def aces_low?
aces_low_straight?
end
# Does the hand include one or more aces?
def aces?
values.include? ACE_HIGH
end
# The number of repeats in the hand. E.g. a full house would
# return [3, 2], and a two pair would return [2, 1, 2]
# (the ordering will be random)
def repeats
cards.group_by &:value
end
# The number of repeats in the hand. A two-pair hand will
# return something like [2, 1, 2], while a full house would
# return [2, 3]
# (the ordering will be random)
def repeat_counts
repeats.values.map &:count
end
# The values that are repeated more than once, sorted by
# number of occurrences. E.g. for a "4 4 4 9 9" full house
# it'll return [4, 9]
def repeat_values
repeated = repeats.map { |value, repeats| [value.to_i, repeats.count] }
repeated = repeated.reject { |value, count| count == 1 }
repeated = repeated.sort_by { |value, count| [count, value] }.reverse
repeated.map(&:first)
end
# Values that are not repeated, sorted high to low
def single_values
repeats.select { |value, repeats| repeats.count == 1 }.map(&:first).sort.reverse
end
# Ordered (low to high) array of card values (assumes aces high)
def values
cards.map(&:value).sort
end
# Unordered array of card suits
def suits
cards.map(&:suit)
end
# A "standard" straight, treating aces as high
def aces_high_straight?
straight_values_from(values.first) == values
end
# Special case straight, treating aces as low
def aces_low_straight?
aces? && straight_values_from(aces_low_values.first) == aces_low_values
end
# The card values as an array, treating aces as low
def aces_low_values
cards.map(&:value).map { |v| v == ACE_HIGH ? ACE_LOW : v }.sort
end
private
# Generate an array of 5 consecutive values
# starting with the `from` value
def straight_values_from(from)
(from...from + 5).to_a
end
end
# ================================================================================================
class TestHand < Test::Unit::TestCase
# Helpers for generating Card instances
# from short-hand notation
def card(string)
suit = case string
when /^h/i then :hearts
when /^d/i then :diamonds
when /^c/i then :clubs
when /^s/i then :spades
else raise ArgumentError
end
value = string[1..-1].to_i
raise ArgumentError unless (2..14).cover? value
Card.new suit, value
end
def cards(string)
string.split(/[^hdcs\d]/i).map { |str| card str }
end
# ======
def test_aces?
hand = Hand.new cards("h2 h3 h4 h5 h6")
assert !hand.aces?, "#aces? should be false"
hand = Hand.new cards("h2 h3 h4 h5 h14")
assert hand.aces?, "#aces? should be true"
end
def test_repeats
hand = Hand.new cards("h2 c2 h4 s4 h10")
assert_equal [2, 4, 10], hand.repeats.keys
end
def test_repeat_counts
hand = Hand.new cards("h2 c2 h4 s4 h10")
assert_equal [2, 2, 1], hand.repeat_counts
end
def test_repeat_values
hand = Hand.new cards("h2 c2 h4 s4 h10")
assert_equal [4, 2], hand.repeat_values
end
def test_single_values
hand = Hand.new cards("h2 c2 h4 s5 h10")
assert_equal [10, 5, 4], hand.single_values
end
def test_values
hand = Hand.new cards("h2 c2 h4 s5 h10")
assert_equal [2, 2, 4, 5, 10], hand.values
end
def test_suits
hand = Hand.new cards("h2 c2 h4 s5 h10")
assert_equal [:hearts, :clubs, :hearts, :spades, :hearts], hand.suits
end
def test_straight_flush
hand = Hand.new cards("h2 h3 h4 h5 h6")
assert hand.straight_flush?, "Hand should be a straight flush"
assert_equal :straight_flush, hand.type, "Type should by :straight_flush"
assert_equal [8, 6, 5, 4, 3, 2], hand.score, "Score should be the base score followed by the values, descending"
end
def test_straight_flush_aces_high
hand = Hand.new cards("h14 h10 h11 h12 h13")
assert hand.straight_flush?, "Hand should be a straight flush"
assert_equal :straight_flush, hand.type, "Type should by :straight_flush"
assert_equal [8, 14, 13, 12, 11, 10], hand.score, "Score should be the base score followed by the values, descending"
end
def test_straight_flush_aces_low
hand = Hand.new cards("h14 h2 h3 h4 h5")
assert hand.straight_flush?, "Hand should be a straight flush"
assert_equal :straight_flush, hand.type, "Type should by :straight_flush"
assert_equal [8, 5, 4, 3, 2, 1], hand.score, "Score should be the base score followed by the values (aces low), descending"
end
def test_four_of_a_kind
hand = Hand.new cards("h3 d3 c3 s3 c11")
assert hand.four_of_a_kind?, "Hand should contain four of a kind"
assert_equal :four_of_a_kind, hand.type, "Type should by :four_of_a_kind"
assert_equal [7, 3, 11], hand.score, "Score should be base score followed by the repeated value, then the remaining value"
end
def test_full_house
hand = Hand.new cards("h3 d3 c3 s11 c11")
assert hand.full_house?, "Hand should be a full house"
assert_equal :full_house, hand.type, "Type should by :full_house"
assert_equal [6, 3, 11], hand.score, "Score should be base score followed by the thrice repeated value, then the twice repeated value"
end
def test_flush
hand = Hand.new cards("c2 c3 c6 c7 c11")
assert hand.flush?, "Hand should be flush"
assert_equal :flush, hand.type, "Type should by :flush"
assert_equal [5, 11, 7, 6, 3, 2], hand.score, "Score should be base score followed by the values, descending"
end
def test_straight
hand = Hand.new cards("c3 c4 s5 h6 c7")
assert hand.straight?, "Hand should be straight"
assert_equal :straight, hand.type, "Type should by :straight"
assert_equal [4, 7, 6, 5, 4, 3], hand.score, "Score should be base score followed by the values, descending"
end
def test_straight_aces_high
hand = Hand.new cards("c10 c11 s12 h13 c14")
assert hand.straight?, "Hand should be an aces-high straight"
assert_equal [4, 14, 13, 12, 11, 10], hand.score, "Score should be base score followed by the values, descending"
end
def test_straight_aces_low
hand = Hand.new cards("c14 c2 s3 h4 c5")
assert_equal [4, 5, 4, 3, 2, 1], hand.score, "Score should be base score followed by the values, descending"
end
def test_three_of_a_kind
hand = Hand.new cards("h3 d3 c3 s10 c11")
assert hand.three_of_a_kind?, "Hand should contain three of a kind"
assert_equal :three_of_a_kind, hand.type, "Type should by :three_of_a_kind"
assert_equal [3, 3, 11, 10], hand.score, "Score should be base score followed by the repeated value, then the remaining values, descending"
end
def test_two_pair
hand = Hand.new cards("h3 d3 c10 s10 c11")
assert hand.two_pair?, "Hand should contain two pairs"
assert_equal :two_pair, hand.type, "Type should by :two_pair"
assert_equal [2, 10, 3, 11], hand.score, "Score should be base score followed by the repeated values, descending, then the remaining values, descending"
end
def test_pair
hand = Hand.new cards("h3 d3 c9 s10 c11")
assert hand.pair?, "Hand should contain a pair"
assert_equal :pair, hand.type, "Type should by :pair"
assert_equal [1, 3, 11, 10, 9], hand.score, "Score should be base score followed by the paired value, then the remaining values, descending"
end
def test_poor_hand
hand = Hand.new cards("h3 d4 c7 s8 c11")
assert_equal :high_card, hand.type, "Type should by :high_card"
assert_equal [0, 11, 8, 7, 4, 3], hand.score, "Score should be base score followed by the values, descending"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.