-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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