Skip to content

Instantly share code, notes, and snippets.

@beatrichartz
Created December 17, 2013 19:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save beatrichartz/06c3142d8a0dae8dae84 to your computer and use it in GitHub Desktop.
Save beatrichartz/06c3142d8a0dae8dae84 to your computer and use it in GitHub Desktop.
require 'test/unit'
class Card
include Comparable
attr_reader :suit, :value
# Value to use as ace low
ACE_LOW = 1
# Value to use as ace high
ACE_HIGH = 14
# initialize the card with a suit and a value
def initialize suit, value
super()
@suit = suit
@value = value
end
# Return the low card
def low_card
ace? ? Card.new(suit, ACE_LOW) : self
end
# Return if the card is an ace high
def ace?
value == ACE_HIGH
end
def ace_low?
value == ACE_LOW
end
# Return if the card has suit spades
def spades?
suit == :spades
end
# Return if the card has suit diamonds
def diamonds?
suit == :diamonds
end
# Return if the card is suit hearts
def hearts?
suit == :hearts
end
# Return if the card has suit clubs
def clubs?
suit == :clubs
end
# Compare cards based on values and suits
# Ordered by suits and values - the suits_index will be introduced below
def <=> other
if other.is_a? Card
(suit_index(suit) <=> suit_index(other.suit)).nonzero? || value <=> other.value
else
value <=> other
end
end
# Allow for construction of card ranges across suits
# the suits_index will be introduced below
def succ
if ace?
i = suit_index suit
Card.new(Deck::SUITS[i + 1] || Deck::SUITS.first, ACE_LOW)
else
Card.new(suit, value + 1)
end
end
def successor? other
!ace? && succ == other
end
def straight_successor? other
!ace? && succ === other
end
# Compare cards for equality in value
def == other
if other.is_a? Card
value == other.value
else
value == other
end
end
alias :eql? :==
# overwrite hash with value since cards with same values are considered equal
alias :hash :value
# Compare cards for strict equality (value and suit)
def === other
if other.is_a? Card
value == other.value && suit == other.suit
else
false
end
end
private
# If no deck, this has to be done with an array of suits
# gets the suit index
def suit_index suit
Deck::SUITS.index suit
end
end
class Hand < Array
# .. RANKS
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.new "There must be 5 cards" unless cards.count == 5
super(cards)
sort_by! &:value # This will give you a nicely sorted hand by default
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.map(&:value))
end
# Tie-breaking kickers, ordered high to low.
def kickers
same_of_kind + (aces_low? ? aces_low.reverse : single_cards.reverse)
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?
same_of_kind? 4
end
# Three of a kind and a pair make a full house
def full_house?
same_of_kind?(3) && same_of_kind?(2)
end
# If the hand only contains one suit, it's flush
def flush?
suits.uniq.one?
end
# single cards in the hand
def single_cards
select{ |c| count(c) == 1 }.sort_by(&:value)
end
# This is the only hand where high vs low aces comes into play.
def straight?
aces_high_straight? || aces_low_straight?
end
# Is a card value repeated 3 times?
def three_of_a_kind?
collapsed_size == 2 && same_of_kind?(3)
end
# Are there 2 instances of repeated card values?
def two_pair?
collapsed_size == 2 && same_of_kind?(2)
end
# Any repeating card value?
def pair?
same_of_kind?(2)
end
# Does the hand include one or more aces?
def aces?
any? &:ace?
end
# Ordered (low to high) array of card values (assumes aces high)
def values
map(&:value).sort
end
# Ordered Array of card suits
def suits
sort.map &:suit
end
# A "standard" straight, treating aces as high
def aces_high_straight?
all?{|card| card === last || card.successor?(self[index(card) + 1]) }
end
alias :all_successors? :aces_high_straight?
# Special case straight, treating aces as low
def aces_low_straight?
aces? && aces_low.all_successors?
end
alias :aces_low? :aces_low_straight?
# The card values as an array, treating aces as low
def aces_low
Hand.new *map(&:low_card)
end
private
# Are there n cards of the same kind?
def same_of_kind?(n)
!!detect{|card| count(card) == n }
end
def same_of_kind
2.upto(4).map{|n| select{|card| count(card) == n }.reverse }.sort_by(&:size).reverse.flatten.uniq
end
# How many cards vanish if we collapse the cards to single values
def collapsed_size
size - uniq.size
end
end
class Deck < Array
# the hands this deck creates
attr_reader :hands
# You can install any order here, Bridge, Preferans, Five Hundred
SUITS = %i(clubs diamonds hearts spades).freeze
# Initialize a deck of cards
def initialize
super (Card.new(SUITS.first, 1)..Card.new(SUITS.last, 14)).to_a
shuffle!
end
# Deal n hands
def deal! hands=5
@hands = hands.times.map {|i| Hand.new *pop(5) }
end
# ... and so on
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_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 [:clubs, :hearts, :hearts, :hearts, :spades], 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