Skip to content

Instantly share code, notes, and snippets.

@Flambino
Created December 11, 2013 20:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Flambino/2c67651035f59c9820fd to your computer and use it in GitHub Desktop.
Save Flambino/2c67651035f59c9820fd to your computer and use it in GitHub Desktop.
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