├── test
├── support
│ └── fixtures
│ │ └── .gitkeep
├── test_helper.rb
├── test_full_house.rb
├── test_card.rb
├── integration
│ └── test_a_million_hands.rb
└── test_poker_hand.rb
├── lib
├── ruby-poker.rb
└── ruby-poker
│ ├── card.rb
│ └── poker_hand.rb
├── .gitignore
├── kochiku.yml
├── Gemfile
├── .travis.yml
├── examples
├── quick_example.rb
└── deck.rb
├── Rakefile
├── ruby-poker.gemspec
├── LICENSE
├── CHANGELOG
└── README.rdoc
/test/support/fixtures/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/ruby-poker.rb:
--------------------------------------------------------------------------------
1 | require 'ruby-poker/card'
2 | require 'ruby-poker/poker_hand'
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage/
2 | /pkg/
3 | /rdoc/
4 | /Gemfile.lock
5 | /test/support/fixtures/poker-hand-testing.data
6 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 | require 'test/unit'
4 | require 'shoulda-context'
5 |
6 | require 'ruby-poker'
7 |
--------------------------------------------------------------------------------
/kochiku.yml:
--------------------------------------------------------------------------------
1 | test_command: 'gem install bundler --conservative && bundle install && bundle exec rake'
2 |
3 | ruby:
4 | - 2.3.0
5 |
6 | targets:
7 | - type: unit
8 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | # Include rake in Gemfile so that `bundle exec rake` doesn't raise an error
6 | gem 'rake', :group => :test
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 1.9.3
4 | - 2.0.0
5 | - 2.1.0
6 | - 2.2.0
7 | - jruby
8 | - rbx
9 | # uncomment this line if your project needs to run something other than `rake`:
10 | # script: bundle exec rspec spec
11 |
--------------------------------------------------------------------------------
/examples/quick_example.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'ruby-poker'
3 |
4 | hand1 = PokerHand.new("8H 9C TC JD QH")
5 | hand2 = PokerHand.new(["3D", "3C", "3S", "KD", "AH"])
6 | puts hand1
7 | puts hand1.just_cards
8 | puts hand1.rank
9 | puts hand2
10 | puts hand2.rank
11 | puts hand1 > hand2
12 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake/testtask'
2 | Rake::TestTask.new do |t|
3 | t.libs = ['lib']
4 | t.verbose = true
5 | t.warning = true
6 | t.test_files = FileList['test/test_card.rb', 'test/test_poker_hand.rb', 'test/test_full_house.rb']
7 | end
8 |
9 | Rake::TestTask.new do |t|
10 | t.libs = ['lib']
11 | t.name = 'test:integration'
12 | t.verbose = true
13 | t.warning = true
14 | t.test_files = FileList['test/integration/test_a_million_hands.rb']
15 | end
16 |
17 | task :default => :test
18 |
--------------------------------------------------------------------------------
/examples/deck.rb:
--------------------------------------------------------------------------------
1 | # This is a sample Deck implementation.
2 | class Deck
3 | def initialize
4 | @cards = []
5 | Card::SUITS.each_byte do |suit|
6 | # careful not to double include the aces...
7 | Card::FACES[1..-1].each_byte do |face|
8 | @cards.push(Card.new(face.chr, suit.chr))
9 | end
10 | end
11 | shuffle
12 | end
13 |
14 | def shuffle
15 | @cards = @cards.sort_by { rand }
16 | return self
17 | end
18 |
19 | # removes a single card from the top of the deck and returns it
20 | # synonymous to poping off a stack
21 | def deal
22 | @cards.pop
23 | end
24 |
25 | # delete an array or a single card from the deck
26 | # converts a string to a new card, if a string is given
27 | def burn(burn_cards)
28 | return false if burn_cards.is_a?(Integer)
29 | if burn_cards.is_a?(Card) || burn_cards.is_a?(String)
30 | burn_cards = [burn_cards]
31 | end
32 |
33 | burn_cards.map! do |c|
34 | c = Card.new(c) unless c.class == Card
35 | @cards.delete(c)
36 | end
37 | true
38 | end
39 |
40 | # return count of the remaining cards
41 | def size
42 | @cards.size
43 | end
44 |
45 | def empty?
46 | @cards.empty?
47 | end
48 | end
--------------------------------------------------------------------------------
/test/test_full_house.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2 |
3 | class TestFullHouse < Test::Unit::TestCase
4 |
5 | context "A Full House should return the correct cards from sort_using_rank" do
6 |
7 | setup do
8 | @fh_high_pair = PokerHand.new("8h 8d 8c Qh Td Kd Ks")
9 | @fh_low_pair = PokerHand.new("8h 8d 8c Qh Td 4h 4d")
10 | @fh_high_trips = PokerHand.new("Kh Kd Kc Qh Td 8h 8d")
11 | @fh_low_trips = PokerHand.new("4h 4d 4c Qh Td Kd Ks")
12 | end
13 |
14 | should "full house #sort_using_rank from seven cards (high pair) should return correct cards" do
15 | assert_equal("8h 8d 8c Ks Kd Qh Td", @fh_high_pair.sort_using_rank)
16 | end
17 |
18 | should "full house #sort_using_rank from seven cards (low pair) should return correct cards" do
19 | assert_equal("8h 8d 8c 4h 4d Qh Td", @fh_low_pair.sort_using_rank)
20 | end
21 |
22 | should "full house #sort_using_rank from seven cards (high trips) should return correct cards" do
23 | assert_equal("Kh Kd Kc 8h 8d Qh Td", @fh_high_trips.sort_using_rank)
24 | end
25 |
26 | should "full house #sort_using_rank from seven cards (low trips) should return correct cards" do
27 | assert_equal("4h 4d 4c Ks Kd Qh Td", @fh_low_trips.sort_using_rank)
28 | end
29 |
30 | end
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/ruby-poker.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = "ruby-poker"
3 | s.version = "1.0.1"
4 | s.platform = Gem::Platform::RUBY
5 | s.summary = "Poker library in Ruby"
6 | s.description = "Ruby library for comparing poker hands and determining the winner."
7 | s.author = "Rob Olson"
8 | s.email = "rob@thinkingdigitally.com"
9 | s.homepage = "https://github.com/robolson/ruby-poker"
10 | s.license = 'BSD'
11 | s.files = ["CHANGELOG",
12 | "examples/deck.rb",
13 | "examples/quick_example.rb",
14 | "lib/ruby-poker.rb",
15 | "lib/ruby-poker/card.rb",
16 | "lib/ruby-poker/poker_hand.rb",
17 | "LICENSE",
18 | "Rakefile",
19 | "README.rdoc",
20 | "ruby-poker.gemspec"]
21 | s.test_files = ["test/test_helper.rb", "test/test_card.rb", "test/test_poker_hand.rb", "test/test_full_house.rb"]
22 | s.require_paths << 'lib'
23 |
24 | s.extra_rdoc_files = ["README.rdoc", "CHANGELOG", "LICENSE"]
25 | s.rdoc_options << '--title' << 'Ruby Poker Documentation' <<
26 | '--main' << 'README.rdoc' <<
27 | '--inline-source' << '-q'
28 |
29 | s.add_development_dependency('test-unit', '~> 3.1') if /^2.[2-9]/ =~ RUBY_VERSION && RUBY_ENGINE == 'ruby'
30 | s.add_development_dependency('shoulda-context', '~> 1.1')
31 | end
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008, Robert Olson
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the distribution.
13 | * Neither the name of the author nor the names of its
14 | contributors may be used to endorse or promote products derived
15 | from this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | 2014-09-01 (1.0.1)
2 | * Rearrange full house cards to return correct order when low pairs. (Stuart Hanscombe)
3 |
4 | 2013-12-28 (1.0.0)
5 | * Add Gemfile
6 | * Add "Empty Hand" rank for empty hands. Fixes #1.
7 | * Simplify internal calculation of `value` in Card class. Fixes #2.
8 |
9 | 2009-07-12 (0.3.2)
10 | * Reorganized ruby-poker's lib folder to match the standard layout for gems. This makes ruby-poker compatible with Rip.
11 | * Bug [#26276] improper two_pair? behavior. Applied patch by Uro.
12 | * Changed protected methods in PokerHand to private
13 | * Added natural_value method to Card
14 |
15 | 2009-01-24 (0.3.1)
16 | * Bug [#23623] undefined method <=> for nil:NilClass
17 |
18 | 2008-12-30 (0.3.1)
19 | * Bug (#20407) Raise an exception when creating a new hand with duplicates
20 | * Added PokerHand#uniq method
21 | * Removed deprecated `Gem::manage_gems` from Rakefile
22 |
23 | 2008-05-17 (0.3.0)
24 | * Changed Card#== to compare based on card suit and face value. Before it only compared the face value of two cards. Warning: This change may potentially break your program if you were comparing Card objects directly.
25 | * Replaced `PokerHand#arranged_hand` with `PokerHand#sort_using_rank` which is more descriptive. This loosely corresponds to bug #20194.
26 | * Bug [#20196] 'rank' goes into an infinite loop.
27 | * Bug [#20195] Allows the same card to be entered into the hand.
28 | * Bug [#20344] sort_using_rank does not return expected results
29 |
30 | 2008-04-20 (0.2.4)
31 | * Modernized the Rakefile
32 | * Updated to be compatible with Ruby 1.9
33 |
34 | 2008-04-06 (0.2.2)
35 | * Fixed bug where two hands that had the same values but different suits returned not equal
36 |
37 | 2008-02-08 (0.2.1)
38 | * Cards can be added to a hand after it is created by using (<<) on a PokerHand
39 | * Cards can be deleted from a hand with PokerHand.delete()
40 |
41 | 2008-01-21 (0.2.0)
42 | * Merged Patrick Hurley's poker solver
43 | * Added support for hands with >5 cards
44 | * Straights with a low Ace count now
45 | * to_s on a PokerHand now includes the rank after the card list
46 | * Finally wrote the Unit Tests suite
47 |
48 | 2008-01-12 (0.1.2)
49 | * Fixed critical bug that was causing the whole program to not work
50 | * Added some test cases as a result
51 | * More test cases coming soon
52 |
53 | 2008-01-12 (0.1.1)
54 | * Ranks are now a class.
55 | * Extracted card, rank, and arrays methods to individual files
56 | * Added gem packaging
57 |
58 | 2008-01-10 (0.1.0)
59 | * Initial version
60 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = Poker library in Ruby {
}[https://travis-ci.org/robolson/ruby-poker]
2 | ===
3 |
4 | Author:: {Rob Olson}[https://github.com/robolson]
5 | Email:: [first name] [at] thinkingdigitally.com
6 | GitHub:: https://github.com/robolson/ruby-poker
7 |
8 | == Description
9 |
10 | Ruby-Poker handles the logic for getting the rank of a poker hand. It can also be used to compare two or more hands to determine which hand has the highest poker value.
11 |
12 | Card representations can be passed to the PokerHand constructor as a string or an array. Face cards (cards ten, jack, queen, king, and ace) are created using their letter representation (T, J, Q, K, A).
13 |
14 | == Install
15 |
16 | gem install ruby-poker
17 |
18 | == Example
19 |
20 | require 'rubygems'
21 | require 'ruby-poker'
22 |
23 | hand1 = PokerHand.new("8H 9C TC JD QH")
24 | hand2 = PokerHand.new(["3D", "3C", "3S", "KD", "AH"])
25 | puts hand1 => 8h 9c Tc Jd Qh (Straight)
26 | puts hand1.just_cards => 8h 9c Tc Jd Qh
27 | puts hand1.rank => Straight
28 | puts hand2 => 3d 3c 3s Kd Ah (Three of a kind)
29 | puts hand2.rank => Three of a kind
30 | puts hand1 > hand2 => true
31 |
32 | == Duplicates
33 |
34 | By default ruby-poker will not raise an exception if you add the same card to a hand twice. You can tell ruby-poker to not allow duplicates by doing the following
35 |
36 | PokerHand.allow_duplicates = false
37 |
38 | Place that line near the beginning of your program. The change is program wide so once allow_duplicates is set to false, _all_ poker hands will raise an exception if a duplicate card is added to the hand.
39 |
40 | == Compatibility
41 |
42 | Ruby-Poker is compatible with Ruby 1.8, Ruby 1.9, and Ruby 2.X.
43 |
44 | == RDoc
45 |
46 | View the {generated documentation for ruby-poker}[http://rdoc.info/gems/ruby-poker/frames] on rdoc.info.
47 |
48 | == Running test suite
49 |
50 | bundle exec rake
51 |
52 | There is also a more compreshenive test, testing all permuations of poker hands, but it requires a large data file that must be downloaded first.
53 |
54 | wget -P ./test/support/fixtures http://archive.ics.uci.edu/ml/machine-learning-databases/poker/poker-hand-testing.data
55 | bundle exec rake test:integration
56 |
57 | == History
58 |
59 | In the 0.2.0 release Patrick Hurley's Texas Holdem code from http://www.rubyquiz.com/quiz24.html was merged into ruby-poker.
60 |
61 | == License
62 |
63 | This is free software; you can redistribute it and/or modify it under the terms of the BSD license. See LICENSE for more details.
64 |
--------------------------------------------------------------------------------
/test/test_card.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2 |
3 | class TestCard < Test::Unit::TestCase
4 | def setup
5 | # testing various input formats for cards
6 | @c1 = Card.new("9c")
7 | @c2 = Card.new("TD")
8 | @c3 = Card.new("jh")
9 | @c4 = Card.new("qS")
10 | @c5 = Card.new("AC")
11 | end
12 |
13 | def test_class_face_value
14 | assert_equal(0, Card.face_value('L'))
15 | assert_equal(13, Card.face_value('A'))
16 | end
17 |
18 | def test_build_from_card
19 | assert_equal("9c", Card.new(@c1).to_s)
20 | end
21 |
22 | def test_build_from_value
23 | assert_equal(@c1, Card.new(8))
24 | assert_equal(@c2, Card.new(22))
25 | assert_equal(@c3, Card.new(36))
26 | assert_equal(@c4, Card.new(50))
27 | assert_equal(@c5, Card.new(13))
28 | end
29 |
30 | def test_build_from_face_suit
31 | assert_equal(8, Card.new('9', 'c').value)
32 | assert_equal(22, Card.new('T', 'd').value)
33 | assert_equal(36, Card.new('J', 'h').value)
34 | assert_equal(50, Card.new('Q', 's').value)
35 | assert_equal(13, Card.new('A', 'c').value)
36 | end
37 |
38 | def test_build_from_value_and_from_face_suit_match
39 | ticker = 0
40 | Card::SUITS.each_char do |suit|
41 | "23456789TJQKA".each_char do |face|
42 | ticker += 1
43 | from_value = Card.new(ticker)
44 | from_face_suit = Card.new(face, suit)
45 | assert_equal(from_face_suit, from_value,
46 | "Face and suit #{face + suit} did not match card from value #{ticker}")
47 | end
48 | end
49 | end
50 |
51 | def test_build_from_value_and_from_face_suit_values_match
52 | ticker = 0
53 | 0.upto(3) do |suit|
54 | 1.upto(13) do |face|
55 | ticker += 1
56 | from_value = Card.new(ticker)
57 | from_face_suit_values = Card.new(face, suit)
58 | assert_equal(from_face_suit_values, from_value,
59 | "Face=#{face} and suit=#{suit} did not match card from value #{ticker}")
60 | end
61 | end
62 | end
63 |
64 | def test_face
65 | assert_equal(8, @c1.face)
66 | assert_equal(9, @c2.face)
67 | assert_equal(10, @c3.face)
68 | assert_equal(11, @c4.face)
69 | end
70 |
71 | def test_suit
72 | assert_equal(0, @c1.suit)
73 | assert_equal(1, @c2.suit)
74 | assert_equal(2, @c3.suit)
75 | assert_equal(3, @c4.suit)
76 | end
77 |
78 | def test_value
79 | assert_equal(8, @c1.value)
80 | assert_equal(22, @c2.value)
81 | assert_equal(36, @c3.value)
82 | assert_equal(50, @c4.value)
83 | assert_equal(13, @c5.value)
84 | end
85 |
86 | def test_natural_value
87 | assert_equal(1, Card.new("AC").natural_value)
88 | assert_equal(15, Card.new("2D").natural_value)
89 | assert_equal(52, Card.new("KS").natural_value)
90 | end
91 |
92 | def test_comparison
93 | assert(@c1 < @c2)
94 | assert(@c3 > @c2)
95 | end
96 |
97 | def test_equals
98 | c = Card.new("9h")
99 | assert_not_equal(@c1, c)
100 | assert_equal(@c1, @c1)
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/test/integration/test_a_million_hands.rb:
--------------------------------------------------------------------------------
1 | # Instructions:
2 | #
3 | # Download 'poker-hand-testing.data' dataset from http://archive.ics.uci.edu/ml/datasets/Poker+Hand.
4 | # wget -P ./test/support/fixtures http://archive.ics.uci.edu/ml/machine-learning-databases/poker/poker-hand-testing.data
5 | #
6 | # bundle exec ruby test/integration/test_a_million_hands.rb
7 |
8 | # Attribute information for poker-hand-testing.data
9 | #
10 | # Attribute Information:
11 | #
12 | # 1) S1 "Suit of card #1"
13 | # Ordinal (1-4) representing {Hearts, Spades, Diamonds, Clubs}
14 | #
15 | # 2) C1 "Rank of card #1"
16 | # Numerical (1-13) representing (Ace, 2, 3, ... , Queen, King)
17 | #
18 | # 3) S2 "Suit of card #2"
19 | # Ordinal (1-4) representing {Hearts, Spades, Diamonds, Clubs}
20 | #
21 | # 4) C2 "Rank of card #2"
22 | # Numerical (1-13) representing (Ace, 2, 3, ... , Queen, King)
23 | #
24 | # 5) S3 "Suit of card #3"
25 | # Ordinal (1-4) representing {Hearts, Spades, Diamonds, Clubs}
26 | #
27 | # 6) C3 "Rank of card #3"
28 | # Numerical (1-13) representing (Ace, 2, 3, ... , Queen, King)
29 | #
30 | # 7) S4 "Suit of card #4"
31 | # Ordinal (1-4) representing {Hearts, Spades, Diamonds, Clubs}
32 | #
33 | # 8) C4 "Rank of card #4"
34 | # Numerical (1-13) representing (Ace, 2, 3, ... , Queen, King)
35 | #
36 | # 9) S5 "Suit of card #5"
37 | # Ordinal (1-4) representing {Hearts, Spades, Diamonds, Clubs}
38 | #
39 | # 10) C5 "Rank of card 5"
40 | # Numerical (1-13) representing (Ace, 2, 3, ... , Queen, King)
41 | #
42 | # 11) CLASS "Poker Hand"
43 | # Ordinal (0-9)
44 | #
45 | # 0: Nothing in hand; not a recognized poker hand
46 | # 1: One pair; one pair of equal ranks within five cards
47 | # 2: Two pairs; two pairs of equal ranks within five cards
48 | # 3: Three of a kind; three equal ranks within five cards
49 | # 4: Straight; five cards, sequentially ranked with no gaps
50 | # 5: Flush; five cards with the same suit
51 | # 6: Full house; pair + different rank three of a kind
52 | # 7: Four of a kind; four equal ranks within five cards
53 | # 8: Straight flush; straight + flush
54 | # 9: Royal flush; {Ace, King, Queen, Jack, Ten} + flush
55 |
56 | require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
57 |
58 | SUITS = ['H', 'S', 'D', 'C']
59 |
60 | data_file = File.expand_path(File.dirname(__FILE__) + "/../support/fixtures/poker-hand-testing.data")
61 |
62 | if File.exist?(data_file)
63 | puts "Starting integration test....this could take a while"
64 | else
65 | warn "*"*80
66 | warn "Could not find #{data_file}"
67 | warn "Please see README for instructions running integration tests"
68 | warn "*"*80
69 | exit(1)
70 | end
71 |
72 | File.new(data_file).each do |line|
73 | columns = line.split(',')
74 |
75 | expected_rank = columns.delete_at(10).to_i
76 |
77 | cards = []
78 | columns.each_slice(2) do |suit, face_value|
79 |
80 | # The translations below come from the ordering of suits within the Card class
81 | rp_suit = case suit.to_i
82 | when 1 # Hearts
83 | 2
84 | when 2 # Spades
85 | 3
86 | when 3 # Diamonds
87 | 1
88 | when 4 # Clubs
89 | 0
90 | end
91 |
92 | rp_face_value = face_value.to_i - 1
93 | # in ruby-poker Aces have the highest value
94 | rp_face_value = 13 if rp_face_value == 0
95 |
96 | cards << Card.new(rp_face_value, rp_suit)
97 | end
98 |
99 | hand = PokerHand.new(cards)
100 | score = hand.score[0][0] # don't know what was I thinking with this double nested array
101 |
102 | if score - 1 != expected_rank
103 | puts "\nInconsistency found in: #{line}"
104 | end
105 | print "."
106 | end
107 |
--------------------------------------------------------------------------------
/lib/ruby-poker/card.rb:
--------------------------------------------------------------------------------
1 | class Card
2 | SUITS = "cdhs"
3 | FACES = "L23456789TJQKA"
4 | SUIT_LOOKUP = {
5 | 'c' => 0,
6 | 'd' => 1,
7 | 'h' => 2,
8 | 's' => 3
9 | }
10 | FACE_VALUES = {
11 | 'L' => 0, # this is a low ace
12 | '2' => 1,
13 | '3' => 2,
14 | '4' => 3,
15 | '5' => 4,
16 | '6' => 5,
17 | '7' => 6,
18 | '8' => 7,
19 | '9' => 8,
20 | 'T' => 9,
21 | 'J' => 10,
22 | 'Q' => 11,
23 | 'K' => 12,
24 | 'A' => 13
25 | }
26 |
27 | def Card.face_value(face)
28 | FACE_VALUES[face.upcase]
29 | end
30 |
31 | private
32 |
33 | def build_from_value(given_value)
34 | @suit = given_value / 13
35 | @face = given_value % 13
36 | end
37 |
38 | def build_from_face_suit(face, suit)
39 | suit.downcase!
40 | @face = Card::face_value(face)
41 | @suit = SUIT_LOOKUP[suit]
42 | raise ArgumentError, "Invalid card: \"#{face}#{suit}\"" unless @face and @suit
43 | end
44 |
45 | def build_from_face_suit_values(face_int, suit_int)
46 | @face = face_int
47 | @suit = suit_int
48 | end
49 |
50 | def build_from_string(card)
51 | build_from_face_suit(card[0,1], card[1,1])
52 | end
53 |
54 | # Constructs this card object from another card object
55 | def build_from_card(card)
56 | @suit = card.suit
57 | @face = card.face
58 | end
59 |
60 | public
61 |
62 | def initialize(*args)
63 | if (args.size == 1)
64 | arg = args.first
65 | if (arg.respond_to?(:to_card))
66 | build_from_card(arg)
67 | elsif (arg.respond_to?(:to_str))
68 | build_from_string(arg)
69 | elsif (arg.respond_to?(:to_int))
70 | build_from_value(arg)
71 | end
72 | elsif (args.size == 2)
73 | arg1, arg2 = args
74 | if (arg1.respond_to?(:to_str) &&
75 | arg2.respond_to?(:to_str))
76 | build_from_face_suit(arg1, arg2)
77 | elsif (arg1.respond_to?(:to_int) &&
78 | arg2.respond_to?(:to_int))
79 | build_from_face_suit_values(arg1, arg2)
80 | end
81 | end
82 | end
83 |
84 | attr_reader :suit, :face
85 | include Comparable
86 |
87 | def value
88 | (@suit * 13) + @face
89 | end
90 |
91 | # Returns a string containing the representation of Card
92 | #
93 | # Card.new("7c").to_s # => "7c"
94 | def to_s
95 | FACES[@face].chr + SUITS[@suit].chr
96 | end
97 |
98 | # If to_card is called on a `Card` it should return itself
99 | def to_card
100 | self
101 | end
102 |
103 | # Compare the face value of this card with another card. Returns:
104 | # -1 if self is less than card2
105 | # 0 if self is the same face value of card2
106 | # 1 if self is greater than card2
107 | def <=> card2
108 | @face <=> card2.face
109 | end
110 |
111 | # Returns true if the cards are the same card. Meaning they
112 | # have the same suit and the same face value.
113 | def == card2
114 | value == card2.value
115 | end
116 | alias :eql? :==
117 |
118 | # Compute a hash-code for this Card. Two Cards with the same
119 | # content will have the same hash code (and will compare using eql?).
120 | def hash
121 | value.hash
122 | end
123 |
124 | # A card's natural value is the closer to it's intuitive value in a deck
125 | # in the range of 1 to 52. Aces are low with a value of 1. Uses the bridge
126 | # order of suits: clubs, diamonds, hearts, and spades. The formula used is:
127 | # If the suit is clubs, the natural value is the face value (remember
128 | # Aces are low). If the suit is diamonds, it is the clubs value plus 13.
129 | # If the suit is hearts, it is plus 26. If it is spades, it is plus 39.
130 | #
131 | # Card.new("Ac").natural_value # => 1
132 | # Card.new("Kc").natural_value # => 12
133 | # Card.new("Ad").natural_value # => 13
134 | def natural_value
135 | natural_face = @face == 13 ? 1 : @face+1 # flip Ace from 13 to 1 and
136 | # increment everything else by 1
137 | natural_face + @suit * 13
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/test/test_poker_hand.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2 |
3 | class TestPokerHand < Test::Unit::TestCase
4 | context "A PokerHand instance" do
5 |
6 | setup do
7 | @quads = PokerHand.new('Kc Kh Kd Ks Qs')
8 | @full_boat = PokerHand.new(["2H", "2D", "4C", "4D", "4S"])
9 | @flush = PokerHand.new("3D 6D 7D TD QD 5H 2S")
10 | @straight = PokerHand.new("8H 9D TS JH QC AS")
11 | @trips = PokerHand.new("2D 9C AS AH AC")
12 | @two_pair = PokerHand.new("As Ac Kc Kd 2s")
13 | @pair = PokerHand.new("As Ac Kc Qd 2s")
14 | @ace_high = PokerHand.new("As Jh 9c 7d 5s")
15 | end
16 |
17 | should "handle empty hands" do
18 | assert_equal(PokerHand.new.rank, "Empty Hand")
19 | end
20 |
21 | should "handle single card hands" do
22 | assert_equal(PokerHand.new('As').rank, @ace_high.rank)
23 | end
24 |
25 | should "handle two card hands" do
26 | assert_equal(PokerHand.new('As Ac').rank, @pair.rank)
27 | end
28 |
29 | should "handle three card hands" do
30 | assert_equal(PokerHand.new('As Ac Ah').rank, @trips.rank)
31 | end
32 |
33 | should "handle four card hands" do
34 | assert_equal(PokerHand.new('As Ac Kd Kh').rank, @two_pair.rank)
35 | assert_equal(PokerHand.new('As Ac Ad Ah').rank, @quads.rank)
36 | end
37 |
38 | should "handle lower case face card names" do
39 | assert_equal(0, PokerHand.new('kc kd') <=> PokerHand.new('Kc Kd'))
40 | assert_equal(0, PokerHand.new('kc kd') <=> PokerHand.new('Kc KD'))
41 | end
42 |
43 | should "handle hands without space" do
44 | assert_equal(0, PokerHand.new('KcKd') <=> PokerHand.new('Kc Kd'))
45 | assert_equal(0, PokerHand.new('KcKd9d') <=> PokerHand.new('Kc Kd 9d'))
46 | end
47 |
48 | should "raise a clear error with invalid cards" do
49 | e = assert_raises(ArgumentError) { PokerHand.new('Fc') }
50 | assert_match(/"Fc"/, e.message)
51 | e = assert_raises(ArgumentError) { PokerHand.new('Tp') }
52 | assert_match(/"Tp"/, e.message)
53 | end
54 |
55 | should "sort using rank" do
56 | assert_equal("As Ah Ac 9c 2d", @trips.sort_using_rank)
57 | assert_equal("4s 4d 4c 2h 2d", @full_boat.sort_using_rank)
58 | assert_equal("Qd Td 7d 6d 3d 2s 5h", @flush.sort_using_rank)
59 | assert_equal("Qc Jh Ts 9d 8h As", @straight.sort_using_rank)
60 |
61 | assert_equal("As Ah 3d 3c Kd", PokerHand.new("AS AH KD 3D 3C").sort_using_rank)
62 | assert_equal("As Ah 3d 3c 2d", PokerHand.new("2D AS AH 3D 3C").sort_using_rank)
63 | end
64 |
65 | should "return card sorted by face value" do
66 | assert_equal([13, 13, 13, 8, 1], @trips.by_face.hand.collect {|c| c.face})
67 | end
68 |
69 | should "return cards sorted by suit" do
70 | assert_equal([3, 2, 1, 0, 0], @trips.by_suit.hand.collect {|c| c.suit})
71 | end
72 |
73 | should "return just the face values of the cards" do
74 | assert_equal([1, 8, 13, 13, 13], @trips.face_values)
75 | end
76 |
77 | should "recognize a straight flush" do
78 | assert !@flush.straight_flush?
79 | assert !@straight.straight_flush?
80 | assert PokerHand.new("8H 9H TH JH QH AS").straight_flush?
81 | end
82 |
83 | should "recognize a royal flush" do
84 | assert !@flush.royal_flush?
85 | assert PokerHand.new("AD KD QD JD TD").royal_flush?
86 | end
87 |
88 | should "recognize a flush" do
89 | assert @flush.flush?
90 | assert !@trips.flush?
91 | end
92 |
93 | should "recognize a four of a kind" do
94 | assert !@trips.four_of_a_kind?
95 | assert PokerHand.new("AD 9C AS AH AC")
96 | end
97 |
98 | should "recognize a full house" do
99 | assert !@trips.full_house?
100 | assert @full_boat.full_house?
101 | end
102 |
103 | should "recognize a straight" do
104 | assert @straight.straight?
105 | # ace low straight
106 | assert PokerHand.new("AH 2S 3D 4H 5D").straight?
107 | # ace high straight
108 | assert PokerHand.new("AH KS QD JH TD").straight?
109 | end
110 |
111 | should "recognize a three of a kind" do
112 | assert @trips.three_of_a_kind?
113 | end
114 |
115 | should "recognize a two pair" do
116 | assert PokerHand.new("2S 2D TH TD 4S").two_pair?
117 | assert !PokerHand.new("6D 7C 5D 5H 3S").two_pair?
118 | end
119 |
120 | should "recognize a pair" do
121 | assert !PokerHand.new("5C JC 2H 7S 3D").pair?
122 | assert PokerHand.new("6D 7C 5D 5H 3S").pair?
123 | end
124 |
125 | should "recognize a hand with the rank highest_card" do
126 | # hard to test, make sure it does not return null
127 | assert PokerHand.new("2D 4S 6C 8C TH").highest_card?
128 | end
129 |
130 | should "have an instance variable hand that is an array of Cards" do
131 | assert_instance_of Card, @trips.hand[0]
132 | end
133 |
134 | should "return the hand's rating as a string" do
135 | assert_equal "Three of a kind", @trips.hand_rating
136 | assert_equal "Full house", @full_boat.hand_rating
137 | end
138 |
139 | should "respond to rank" do
140 | # rank is an alias for hand_rating
141 | assert_respond_to @trips, :rank
142 | end
143 |
144 | should "return the hand as a string" do
145 | assert_equal("2d 9c As Ah Ac", @trips.just_cards)
146 | end
147 |
148 | should "return the hand's score" do
149 | assert_equal([4, 13, 8, 1], @trips.score[0])
150 | end
151 |
152 | should "be able to match regular expressions" do
153 | assert_match(/9c/, @trips.to_s)
154 | assert_no_match(/AD/, @trips.to_s)
155 | end
156 |
157 | should "return the correct number of cards in the hand" do
158 | assert_equal(0, PokerHand.new.size)
159 | assert_equal(1, PokerHand.new("2c").size)
160 | assert_equal(2, PokerHand.new("2c 3d").size)
161 | end
162 |
163 | should "be comparable to other PokerHands" do
164 | hand1 = PokerHand.new("5C JC 2H 5S 3D")
165 | hand2 = PokerHand.new("6D 7C 5D 5H 3S")
166 | assert_equal(1, hand1 <=> hand2)
167 | assert_equal(-1, hand2 <=> hand1)
168 | end
169 |
170 | should "be considered equal to other poker hands that contain the same cards" do
171 | assert_equal(0, @trips <=> @trips)
172 |
173 | hand1 = PokerHand.new("Ac Qc Ks Kd 9d 3c")
174 | hand2 = PokerHand.new("Ah Qs 9h Kh Kc 3s")
175 | assert_equal(0, hand1 <=> hand2)
176 | end
177 |
178 | should "be able to insert new cards into the hand" do
179 | ph = PokerHand.new()
180 | ph << "Qd"
181 | ph << Card.new("2D")
182 | ph << ["3d", "4d"]
183 | assert_equal("Qd 2d 3d 4d", ph.just_cards)
184 | end
185 |
186 | should "be able to delete a card" do
187 | ph = PokerHand.new("Ac")
188 | ph.delete("Ac")
189 | assert_equal(Array.new, ph.hand)
190 | end
191 |
192 | should "detect the two highest pairs when there are more than two" do
193 | ph = PokerHand.new("7d 7s 4d 4c 2h 2d")
194 | assert_equal([3, 6, 3, 1], ph.two_pair?[0])
195 | # Explanation of [3, 6, 3, 1]
196 | # 3: the number for a two pair
197 | # 6: highest pair is two 7's
198 | # 3: second highest pair is two 4's
199 | # 1: kicker is a 2
200 | end
201 |
202 | context "when duplicates are allowed" do
203 | setup do
204 | PokerHand.allow_duplicates = true
205 | end
206 |
207 | should "create a PokerHand of unique cards" do
208 | uniq_ph = PokerHand.new("3s 4s 3s").uniq
209 | assert_instance_of(PokerHand, uniq_ph) # want to be sure uniq hands back a PokerHand
210 | assert_contains(uniq_ph.hand, Card.new('3s'))
211 | assert_contains(uniq_ph.hand, Card.new('4s'))
212 | end
213 |
214 | should "allow five of a kind" do
215 | # there is no five of a kind. This just tests to make sure
216 | # that ruby-poker doesn't crash if given 5 of the same card
217 | ph = PokerHand.new("KS KS KS KS KS")
218 | assert_equal("Four of a kind", ph.rank)
219 | end
220 |
221 | should "allow duplicates on initialize" do
222 | assert_nothing_raised RuntimeError do
223 | PokerHand.new("3s 3s")
224 | end
225 | end
226 |
227 | should "allow duplicate card to be added after initialize" do
228 | ph = PokerHand.new("2d")
229 | ph << "2d"
230 | assert_equal("2d 2d", ph.just_cards)
231 | end
232 | end
233 |
234 | context "when duplicates are not allowed" do
235 | setup do
236 | PokerHand.allow_duplicates = false
237 | end
238 |
239 | should "not allow duplicates on initialize" do
240 | PokerHand.allow_duplicates = false
241 |
242 | assert_raise RuntimeError do
243 | PokerHand.new("3s 3s")
244 | end
245 |
246 | PokerHand.allow_duplicates = true
247 | end
248 |
249 | should "not allow duplicates after initialize" do
250 | PokerHand.allow_duplicates = false
251 |
252 | ph = PokerHand.new("2d")
253 | assert_raise RuntimeError do
254 | ph << "2d"
255 | end
256 |
257 | PokerHand.allow_duplicates = true
258 | end
259 | end
260 |
261 | should "have an each method" do
262 | cards = []
263 | @straight.each do |card|
264 | cards << card
265 | end
266 | assert_equal @straight.to_a, cards
267 | end
268 |
269 | should "be Enumerable" do
270 | assert PokerHand.include?(Enumerable)
271 | end
272 | end
273 |
274 | context "addition" do
275 | setup do
276 | @base = PokerHand.new('Ac Kc')
277 | end
278 |
279 | should "work with a string" do
280 | assert_equal PokerHand.new('Ac Kc Qc'), @base + 'Qc'
281 | end
282 |
283 | should "work with a card" do
284 | assert_equal PokerHand.new('Ac Kc Qc'), @base + Card.new('Qc')
285 | end
286 |
287 | should "work with a hand" do
288 | assert_equal PokerHand.new('Ac Kc Qc'), @base + PokerHand.new('Qc')
289 | end
290 |
291 | should "not modify the receiver hand" do
292 | result = @base + 'Qc'
293 | assert_not_equal result, @base
294 | end
295 |
296 | should "not affect receiver cards" do
297 | result = @base + 'Qc'
298 | result.to_a.first.instance_eval { @face = Card.face_value('2') }
299 | assert_equal PokerHand.new('Ac Kc'), @base
300 | end
301 | end
302 |
303 | context "PokerHand#pair?" do
304 |
305 | should "return false with one card" do
306 | assert !PokerHand.new("2h").pair?
307 | end
308 |
309 | context "with a pair" do
310 |
311 | should "return 2, followed by the pair value" do
312 | assert_equal [2, 5-1], PokerHand.new("5h 5s").pair?[0]
313 | end
314 |
315 | context "with a two card hand" do
316 | setup do
317 | @ph = PokerHand.new("5h 5s")
318 | @scoring = @ph.pair?[0]
319 | end
320 |
321 | should "return scoring with 2 entries" do
322 | assert_equal 2, @scoring.size
323 | end
324 | end
325 |
326 | context "with a three card hand" do
327 | setup do
328 | @ph = PokerHand.new("5h 5s 8s")
329 | @scoring = @ph.pair?[0]
330 | end
331 |
332 | should "return scoring with 3 entries" do
333 | assert_equal 3, @scoring.size
334 | end
335 |
336 | should "return the value of the kicker" do
337 | assert_equal 8-1, @scoring[2]
338 | end
339 | end
340 |
341 | context "with a four card hand" do
342 | setup do
343 | @ph = PokerHand.new("5h 5s 8s 7s")
344 | @scoring = @ph.pair?[0]
345 | end
346 |
347 | should "return scoring with 4 entries" do
348 | assert_equal 4, @scoring.size
349 | end
350 |
351 | should "return the values of the kickers" do
352 | assert_equal 8-1, @scoring[2]
353 | assert_equal 7-1, @scoring[3]
354 | end
355 | end
356 |
357 | context "with a five (or more) card hand" do
358 | setup do
359 | @ph = PokerHand.new("5h 5s 8s 7s 6s 2h")
360 | @scoring = @ph.pair?[0]
361 | end
362 |
363 | should "return scoring with 5 entries" do
364 | assert_equal 5, @scoring.size
365 | end
366 |
367 | should "return the values of the kickers" do
368 | assert_equal 8-1, @scoring[2]
369 | assert_equal 7-1, @scoring[3]
370 | assert_equal 6-1, @scoring[4]
371 | end
372 | end
373 | end
374 |
375 | context "without a pair" do
376 | should "return false" do
377 | assert !PokerHand.new("2h 3h").pair?
378 | end
379 | end
380 | end
381 |
382 |
383 | def assert_hand_match(expression, cards)
384 | hand = PokerHand.new(cards)
385 | assert hand.match?(expression), "#{cards} didn't match #{expression}"
386 | end
387 |
388 | def assert_hand_not_match(expression, cards)
389 | hand = PokerHand.new(cards)
390 | assert !hand.match?(expression), "#{cards} did match #{expression}"
391 | end
392 |
393 | context "matching expression" do
394 | should "match two faces" do
395 | assert_hand_match 'AA', 'Ah Ad'
396 | assert_hand_match 'Q8', 'Qc 8d'
397 | end
398 |
399 | should "not match two faces" do
400 | assert_hand_not_match 'T9', 'Tc 8s'
401 | assert_hand_not_match 'QQ', 'Tc 8s'
402 | end
403 |
404 | should "match unordered faces" do
405 | assert_hand_match 'K7', '7c Ks'
406 | end
407 |
408 | should "match suited when suited" do
409 | assert_hand_match 'Q8s', 'Qc 8c'
410 | assert_hand_match '56s', '5h 6h'
411 | end
412 |
413 | should "not match suited when offsuit" do
414 | assert_hand_not_match 'Q8s', 'Qc 8d'
415 | assert_hand_not_match '56s', '5h 6c'
416 | end
417 |
418 | should "match offsuit when offsuited" do
419 | assert_hand_match 'Q8o', 'Qc 8h'
420 | assert_hand_match '56o', '5h 6s'
421 | end
422 |
423 | should "not match offsuit when suited" do
424 | assert_hand_not_match 'Q8o', 'Qc 8c'
425 | assert_hand_not_match '56o', '5h 6h'
426 | end
427 |
428 | should "match pair min" do
429 | assert_hand_match 'JJ+', 'Jc Js'
430 | assert_hand_match '66+', 'Qc Qh'
431 | assert_hand_match 'JJ+', 'Ad Ac'
432 | end
433 |
434 | should "not match pair min" do
435 | assert_hand_not_match 'JJ+', 'Tc Ts'
436 | assert_hand_not_match '66+', 'Qc Kh'
437 | assert_hand_not_match 'AA+', '2d 2c'
438 | end
439 |
440 | should "match face min" do
441 | assert_hand_match 'AJ+', 'Ac Js'
442 | assert_hand_match 'AQ+', 'Ac Kc'
443 | assert_hand_match 'AJ+', 'Ac As'
444 | assert_hand_match 'QT+', 'Qc Ts'
445 | assert_hand_match 'QT+', 'Qc Qs'
446 | assert_hand_not_match 'QT+', 'Qc Ks' # sure? should be matched with KQ+?
447 | assert_hand_not_match 'AJ+', 'Ac Ts'
448 | assert_hand_not_match 'AJ+', 'Tc Ts'
449 | end
450 |
451 | should "match suited face min" do
452 | assert_hand_match 'AJs+', 'Ac Jc'
453 | assert_hand_match 'AQs+', 'Ac Kc'
454 | assert_hand_not_match 'AJs+', 'Ac As'
455 | assert_hand_match 'QTs+', 'Qc Tc'
456 | assert_hand_not_match 'QTs+', 'Qc Ts'
457 | assert_hand_not_match 'AJs+', 'Ac Qs'
458 | end
459 |
460 | should "match offsuit face min" do
461 | assert_hand_match 'AJo+', 'Ac Jd'
462 | assert_hand_match 'AQo+', 'Ac Kh'
463 | assert_hand_match 'AJo+', 'Ac As'
464 | assert_hand_match 'QTo+', 'Qc Td'
465 | assert_hand_not_match 'QTo+', 'Qc Tc'
466 | assert_hand_not_match 'AJo+', 'Ac Qc'
467 | end
468 |
469 | should "match face with 1 gap" do
470 | assert_hand_match '89+', '8c 9d'
471 | assert_hand_match '89+', '9c Td'
472 | assert_hand_match '89+', 'Tc Jd'
473 | assert_hand_match '89+', 'Ac Kd'
474 | assert_hand_not_match '89+', '8c Td'
475 | assert_hand_not_match '89+', 'Tc Td'
476 | assert_hand_not_match '89+', '7c 8d'
477 | end
478 |
479 | should "match face with 2 gaps" do
480 | assert_hand_match '8T+', '8c Td'
481 | assert_hand_match '8T+', 'Tc 8d'
482 | assert_hand_match '24+', '9c Jd'
483 | assert_hand_match '79+', 'Ac Qd'
484 | assert_hand_not_match '8T+', '8c 9d'
485 | assert_hand_not_match '8T+', 'Tc Td'
486 | assert_hand_not_match '8T+', 'Jc Ad'
487 | assert_hand_not_match '8T+', '7c 9d'
488 | end
489 |
490 | should "match face with many gaps" do
491 | assert_hand_match '8J+', '9c Qd'
492 | assert_hand_match '8Q+', '9c Kd'
493 | assert_hand_match '8K+', 'Ac 9d'
494 | assert_hand_not_match '8J+', '7c Td'
495 | end
496 |
497 | should "match face gap with suit" do
498 | assert_hand_match '89s+', '9c Tc'
499 | assert_hand_not_match '89s+', '9c Td'
500 | assert_hand_match '89o+', '9c Th'
501 | assert_hand_not_match '89o+', '9d Td'
502 | end
503 |
504 | [
505 | %w(),
506 | %w(Ac),
507 | %w(Ac Kc Qc),
508 | %w(Ac Kc Qc Jc Tc),
509 | ].each do |cards|
510 | should "raise an error if the number of cards is #{cards.size}" do
511 | hand = PokerHand.new(cards)
512 | assert_raises RuntimeError do
513 | hand.match?('AA')
514 | end
515 | end
516 | end
517 |
518 | should "raise an error with invalid expression" do
519 | hand = PokerHand.new("Ac Kc")
520 | assert_raises ArgumentError do
521 | hand.match? "foo"
522 | end
523 |
524 | assert_raises ArgumentError do
525 | hand.match? ""
526 | end
527 | end
528 | end
529 |
530 | end
531 |
--------------------------------------------------------------------------------
/lib/ruby-poker/poker_hand.rb:
--------------------------------------------------------------------------------
1 | class PokerHand
2 | include Comparable
3 | include Enumerable
4 | attr_reader :hand
5 |
6 | @@allow_duplicates = true # true by default
7 | def self.allow_duplicates; @@allow_duplicates; end
8 | def self.allow_duplicates=(v); @@allow_duplicates = v; end
9 |
10 | # Returns a new PokerHand object. Accepts the cards represented
11 | # in a string or an array
12 | #
13 | # PokerHand.new("3d 5c 8h Ks") # => # # "Ks 8h 3d 5c"
38 | def by_suit
39 | PokerHand.new(@hand.sort_by { |c| [c.suit, c.face] }.reverse)
40 | end
41 |
42 | # Returns a new PokerHand object with the cards sorted by face value
43 | # with the highest value first.
44 | #
45 | # PokerHand.new("3d 5c 8h Ks").by_face.just_cards # => "Ks 8h 5c 3d"
46 | def by_face
47 | PokerHand.new(@hand.sort_by { |c| [c.face, c.suit] }.reverse)
48 | end
49 |
50 | # Returns string representation of the hand without the rank
51 | #
52 | # PokerHand.new(["3c", "Kh"]).just_cards # => "3c Kh"
53 | def just_cards
54 | @hand.join(" ")
55 | end
56 | alias :cards :just_cards
57 |
58 | # Returns an array of the card values in the hand.
59 | # The values returned are 1 less than the value on the card.
60 | # For example: 2's will be shown as 1.
61 | #
62 | # PokerHand.new(["3c", "Kh"]).face_values # => [2, 12]
63 | def face_values
64 | @hand.map { |c| c.face }
65 | end
66 |
67 | # The =~ method does a regular expression match on the cards in this hand.
68 | # This can be useful for many purposes. A common use is the check if a card
69 | # exists in a hand.
70 | #
71 | # PokerHand.new("3d 4d 5d") =~ /8h/ # => nil
72 | # PokerHand.new("3d 4d 5d") =~ /4d/ # => #
73 | def =~ (re)
74 | re.match(just_cards)
75 | end
76 |
77 | def royal_flush?
78 | if (md = (by_suit =~ /A(.) K\1 Q\1 J\1 T\1/))
79 | [[10], arrange_hand(md)]
80 | else
81 | false
82 | end
83 | end
84 |
85 | def straight_flush?
86 | if (md = (/.(.)(.)(?: 1.\2){4}/.match(delta_transform(true))))
87 | high_card = Card::face_value(md[1])
88 | arranged_hand = fix_low_ace_display(md[0] + ' ' +
89 | md.pre_match + ' ' + md.post_match)
90 | [[9, high_card], arranged_hand]
91 | else
92 | false
93 | end
94 | end
95 |
96 | def four_of_a_kind?
97 | if (md = (by_face =~ /(.). \1. \1. \1./))
98 | # get kicker
99 | result = [8, Card::face_value(md[1])]
100 | result << Card::face_value($1) if (md.pre_match + md.post_match).match(/(\S)/)
101 | return [result, arrange_hand(md)]
102 | end
103 | false
104 | end
105 |
106 | def full_house?
107 | if (md = (by_face =~ /(.). \1. \1. (.*)(.). \3./))
108 | arranged_hand = rearrange_full_house(by_face.cards)
109 | [
110 | [7, Card::face_value(md[1]), Card::face_value(md[3])],
111 | arranged_hand
112 | ]
113 | elsif (md = (by_face =~ /((.). \2.) (.*)((.). \5. \5.)/))
114 | arranged_hand = rearrange_full_house(by_face.cards)
115 | [
116 | [7, Card::face_value(md[5]), Card::face_value(md[2])],
117 | arranged_hand
118 | ]
119 | else
120 | false
121 | end
122 | end
123 |
124 | def flush?
125 | if (md = (by_suit =~ /(.)(.) (.)\2 (.)\2 (.)\2 (.)\2/))
126 | [
127 | [
128 | 6,
129 | Card::face_value(md[1]),
130 | *(md[3..6].map { |f| Card::face_value(f) })
131 | ],
132 | arrange_hand(md)
133 | ]
134 | else
135 | false
136 | end
137 | end
138 |
139 | def straight?
140 | if hand.size >= 5
141 | transform = delta_transform
142 | # note we can have more than one delta 0 that we
143 | # need to shuffle to the back of the hand
144 | i = 0
145 | until transform.match(/^\S{3}( [1-9x]\S\S)+( 0\S\S)*$/) or i >= hand.size do
146 | # only do this once per card in the hand to avoid entering an
147 | # infinite loop if all of the cards in the hand are the same
148 | transform.gsub!(/(\s0\S\S)(.*)/, "\\2\\1") # moves the front card to the back of the string
149 | i += 1
150 | end
151 | if (md = (/.(.). 1.. 1.. 1.. 1../.match(transform)))
152 | high_card = Card::face_value(md[1])
153 | arranged_hand = fix_low_ace_display(md[0] + ' ' + md.pre_match + ' ' + md.post_match)
154 | return [[5, high_card], arranged_hand]
155 | end
156 | end
157 | false
158 | end
159 |
160 | def three_of_a_kind?
161 | if (md = (by_face =~ /(.). \1. \1./))
162 | # get kicker
163 | arranged_hand = arrange_hand(md)
164 | matches = arranged_hand.match(/(?:\S\S ){2}(\S\S)/)
165 | if matches
166 | result = [4, Card::face_value(md[1])]
167 | matches = arranged_hand.match(/(?:\S\S ){3}(\S)/)
168 | result << Card::face_value($1) if matches
169 | matches = arranged_hand.match(/(?:\S\S ){3}(\S)\S (\S)/)
170 | result << Card::face_value($2) if matches
171 | return [result, arranged_hand]
172 | end
173 | end
174 | false
175 | end
176 |
177 | def two_pair?
178 | # \1 is the face value of the first pair
179 | # \2 is the card in between the first pair and the second pair
180 | # \3 is the face value of the second pair
181 | if (md = (by_face =~ /(.). \1.(.*?) (.). \3./))
182 | # to get the kicker this does the following
183 | # md[0] is the regex matched above which includes the first pair and
184 | # the second pair but also some cards in the middle so we sub them out
185 | # then we add on the cards that came before the first pair, the cards
186 | # that were in-between, and the cards that came after.
187 | arranged_hand = arrange_hand(md[0].sub(md[2], '') + ' ' +
188 | md.pre_match + ' ' + md[2] + ' ' + md.post_match)
189 | matches = arranged_hand.match(/(?:\S\S ){3}(\S\S)/)
190 | if matches
191 | result = []
192 | result << 3
193 | result << Card::face_value(md[1]) # face value of the first pair
194 | result << Card::face_value(md[3]) # face value of the second pair
195 | matches = arranged_hand.match(/(?:\S\S ){4}(\S)/)
196 | result << Card::face_value($1) if matches # face value of the kicker
197 | return [result, arranged_hand]
198 | end
199 | end
200 | false
201 | end
202 |
203 | def pair?
204 | if (md = (by_face =~ /(.). \1./))
205 | arranged_hand_str = arrange_hand(md)
206 | arranged_hand = PokerHand.new(arranged_hand_str)
207 |
208 | if arranged_hand.hand[0].face == arranged_hand.hand[1].face &&
209 | arranged_hand.hand[0].suit != arranged_hand.hand[1].suit
210 | result = [2, arranged_hand.hand[0].face]
211 | result << arranged_hand.hand[2].face if arranged_hand.size > 2
212 | result << arranged_hand.hand[3].face if arranged_hand.size > 3
213 | result << arranged_hand.hand[4].face if arranged_hand.size > 4
214 |
215 | return [result, arranged_hand_str]
216 | end
217 | else
218 | false
219 | end
220 | end
221 |
222 | def highest_card?
223 | if size > 0
224 | result = by_face
225 | [[1, *result.face_values[0..result.face_values.length]], result.hand.join(' ')]
226 | else
227 | false
228 | end
229 | end
230 |
231 | def empty_hand?
232 | if size == 0
233 | [[0]]
234 | end
235 | end
236 |
237 | OPS = [
238 | ['Royal Flush', :royal_flush? ],
239 | ['Straight Flush', :straight_flush? ],
240 | ['Four of a kind', :four_of_a_kind? ],
241 | ['Full house', :full_house? ],
242 | ['Flush', :flush? ],
243 | ['Straight', :straight? ],
244 | ['Three of a kind', :three_of_a_kind?],
245 | ['Two pair', :two_pair? ],
246 | ['Pair', :pair? ],
247 | ['Highest Card', :highest_card? ],
248 | ['Empty Hand', :empty_hand? ],
249 | ]
250 |
251 | # Returns the verbose hand rating
252 | #
253 | # PokerHand.new("4s 5h 6c 7d 8s").hand_rating # => "Straight"
254 | def hand_rating
255 | OPS.map { |op|
256 | (method(op[1]).call()) ? op[0] : false
257 | }.find { |v| v }
258 | end
259 |
260 | alias :rank :hand_rating
261 |
262 | def score
263 | # OPS.map returns an array containing the result of calling each OPS method against
264 | # the poker hand. The truthy cell closest to the front of the array represents
265 | # the highest ranking.
266 | OPS.map { |op|
267 | method(op[1]).call()
268 | }.find { |score| score }
269 | end
270 |
271 | # Returns a string of the hand arranged based on its rank. Usually this will be the
272 | # same as by_face but there are some cases where it makes a difference.
273 | #
274 | # ph = PokerHand.new("As 3s 5s 2s 4s")
275 | # ph.sort_using_rank # => "5s 4s 3s 2s As"
276 | # ph.by_face.just_cards # => "As 5s 4s 3s 2s"
277 | def sort_using_rank
278 | score[1]
279 | end
280 |
281 | # Returns string with a listing of the cards in the hand followed by the hand's rank.
282 | #
283 | # h = PokerHand.new("8c 8s")
284 | # h.to_s # => "8c 8s (Pair)"
285 | def to_s
286 | just_cards + " (" + hand_rating + ")"
287 | end
288 |
289 | # Returns an array of `Card` objects that make up the `PokerHand`.
290 | def to_a
291 | @hand
292 | end
293 | alias :to_ary :to_a
294 |
295 | def <=> other_hand
296 | self.score[0].compact <=> other_hand.score[0].compact
297 | end
298 |
299 | # Add a card to the hand
300 | #
301 | # hand = PokerHand.new("5d")
302 | # hand << "6s" # => Add a six of spades to the hand by passing a string
303 | # hand << ["7h", "8d"] # => Add multiple cards to the hand using an array
304 | def << new_cards
305 | if new_cards.is_a?(Card) || new_cards.is_a?(String)
306 | new_cards = [new_cards]
307 | end
308 |
309 | new_cards.each do |nc|
310 | unless allow_duplicates
311 | raise "A card with the value #{nc} already exists in this hand. Set PokerHand.allow_duplicates to true if you want to be able to add a card more than once." if self =~ /#{nc}/
312 | end
313 |
314 | @hand << Card.new(nc)
315 | end
316 | end
317 |
318 | # Remove a card from the hand.
319 | #
320 | # hand = PokerHand.new("5d Jd")
321 | # hand.delete("Jd") # => #
322 | # hand.just_cards # => "5d"
323 | def delete card
324 | @hand.delete(Card.new(card))
325 | end
326 |
327 | # Same concept as Array#uniq
328 | def uniq
329 | PokerHand.new(@hand.uniq)
330 | end
331 |
332 | # Resolving methods are just passed directly down to the @hand array
333 | RESOLVING_METHODS = [:each, :size, :-]
334 | RESOLVING_METHODS.each do |method|
335 | class_eval %{
336 | def #{method}(*args, &block)
337 | @hand.#{method}(*args, &block)
338 | end
339 | }
340 | end
341 |
342 | def allow_duplicates
343 | @@allow_duplicates
344 | end
345 |
346 | # Checks whether the hand matches usual expressions like AA, AK, AJ+, 66+, AQs, AQo...
347 | #
348 | # Valid expressions:
349 | # * "AJ": Matches exact faces (in this case an Ace and a Jack), suited or not
350 | # * "AJs": Same but suited only
351 | # * "AJo": Same but offsuit only
352 | # * "AJ+": Matches an Ace with any card >= Jack, suited or not
353 | # * "AJs+": Same but suited only
354 | # * "AJo+": Same but offsuit only
355 | # * "JJ+": Matches any pair >= "JJ".
356 | # * "8T+": Matches connectors (in this case with 1 gap : 8T, 9J, TQ, JK, QA)
357 | # * "8Ts+": Same but suited only
358 | # * "8To+": Same but offsuit only
359 | #
360 | # The order of the cards in the expression is important (8T+ is not the same as T8+), but the order of the cards in the hand is not ("AK" will match "Ad Kc" and "Kc Ad").
361 | #
362 | # The expression can be an array of expressions. In this case the method returns true if any expression matches.
363 | #
364 | # This method only works on hands with 2 cards.
365 | #
366 | # PokerHand.new('Ah Ad').match? 'AA' # => true
367 | # PokerHand.new('Ah Kd').match? 'AQ+' # => true
368 | # PokerHand.new('Jc Qc').match? '89s+' # => true
369 | # PokerHand.new('Ah Jd').match? %w( 22+ A6s+ AJ+ ) # => true
370 | # PokerHand.new('Ah Td').match? %w( 22+ A6s+ AJ+ ) # => false
371 | #
372 | def match? expression
373 | raise "Hands with #{@hand.size} cards is not supported" unless @hand.size == 2
374 |
375 | if expression.is_a? Array
376 | return expression.any? { |e| match?(e) }
377 | end
378 |
379 | faces = @hand.map { |card| card.face }.sort.reverse
380 | suited = @hand.map { |card| card.suit }.uniq.size == 1
381 | if expression =~ /^(.)(.)(s|o|)(\+|)$/
382 | face1 = Card.face_value($1)
383 | face2 = Card.face_value($2)
384 | raise ArgumentError, "Invalid expression: #{expression.inspect}" unless face1 and face2
385 | suit_match = $3
386 | plus = ($4 != "")
387 |
388 | if plus
389 | if face1 == face2
390 | face_match = (faces.first == faces.last and faces.first >= face1)
391 | elsif face1 > face2
392 | face_match = (faces.first == face1 and faces.last >= face2)
393 | else
394 | face_match = ((faces.first - faces.last) == (face2 - face1) and faces.last >= face1)
395 | end
396 | else
397 | expression_faces = [face1, face2].sort.reverse
398 | face_match = (expression_faces == faces)
399 | end
400 | case suit_match
401 | when ''
402 | face_match
403 | when 's'
404 | face_match and suited
405 | when 'o'
406 | face_match and !suited
407 | end
408 | else
409 | raise ArgumentError, "Invalid expression: #{expression.inspect}"
410 | end
411 | end
412 |
413 | def +(other)
414 | cards = @hand.map { |card| Card.new(card) }
415 | case other
416 | when String
417 | cards << Card.new(other)
418 | when Card
419 | cards << other
420 | when PokerHand
421 | cards += other.hand
422 | else
423 | raise ArgumentError, "Invalid argument: #{other.inspect}"
424 | end
425 | PokerHand.new(cards)
426 | end
427 |
428 | private
429 |
430 | def check_for_duplicates
431 | if @hand.size != @hand.uniq.size && !allow_duplicates
432 | raise "Attempting to create a hand that contains duplicate cards. Set PokerHand.allow_duplicates to true if you do not want to ignore this error."
433 | end
434 | end
435 |
436 | # if md is a string, arrange_hand will remove extra white space
437 | # if md is a MatchData, arrange_hand returns the matched segment
438 | # followed by the pre_match and the post_match
439 | def arrange_hand(md)
440 | hand = if md.respond_to?(:to_str)
441 | md
442 | else
443 | md[0] + ' ' + md.pre_match + md.post_match
444 | end
445 | hand.strip.squeeze(" ") # remove extra whitespace
446 | end
447 |
448 | # delta transform returns a string representation of the cards where the
449 | # delta between card values is in the string. This is necessary so a regexp
450 | # can then match a straight and/or straight flush
451 | #
452 | # Examples
453 | #
454 | # PokerHand.new("As Qc Jh Ts 9d 8h")
455 | # # => '0As 2Qc 1Jh 1Ts 19d 18h'
456 | #
457 | # PokerHand.new("Ah Qd Td 5d 4d")
458 | # # => '0Ah 2Qd 2Td 55d 14d'
459 | #
460 | def delta_transform(use_suit = false)
461 | # In order to check for both ace high and ace low straights we create low
462 | # ace duplicates of all of the high aces.
463 | aces = @hand.select { |c| c.face == Card::face_value('A') }
464 | aces.map! { |c| Card.new(0, c.suit) } # hack to give the appearance of a low ace
465 |
466 | base = if (use_suit)
467 | (@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
468 | else
469 | (@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
470 | end
471 |
472 | # Insert delta in front of each card
473 | result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
474 | if (prev_card)
475 | delta = prev_card - card.face
476 | else
477 | delta = 0
478 | end
479 | # does not really matter for my needs
480 | delta = 'x' if (delta > 9 || delta < 0)
481 | delta_hand += delta.to_s + card.to_s + ' '
482 | [delta_hand, card.face]
483 | end
484 |
485 | # we just want the delta transform, not the last cards face too
486 | result[0].chop
487 | end
488 |
489 | def fix_low_ace_display(arranged_hand)
490 | # remove card deltas (this routine is only used for straights)
491 | arranged_hand.gsub!(/\S(\S\S)\s*/, "\\1 ")
492 |
493 | # Fix "low aces"
494 | arranged_hand.gsub!(/L(\S)/, "A\\1")
495 |
496 | # Remove duplicate aces (this will not work if you have
497 | # multiple decks or wild cards)
498 | arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
499 |
500 | # cleanup white space
501 | arranged_hand.gsub!(/\s+/, ' ')
502 | # careful to use gsub as gsub! can return nil here
503 | arranged_hand.gsub(/\s+$/, '')
504 | end
505 |
506 | def rearrange_full_house(cards)
507 | card_array = cards.split.uniq
508 | card_hash = Hash[card_array.collect{|c| [c[0], card_array.count{|n| n[0] == c[0]}]}]
509 | arranged_hand = card_array.select{|c| c if c[0] == card_hash.key(3)}
510 | arranged_hand += card_array.select{|c| c if c[0] == card_hash.key(2)}
511 | (arranged_hand + (card_array - arranged_hand)).join(" ")
512 | end
513 |
514 | end
515 |
--------------------------------------------------------------------------------