├── 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 {Build Status}[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 | --------------------------------------------------------------------------------