bhousel-ruby-poker 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +54 -0
- data/LICENSE.txt +20 -0
- data/README.md +50 -0
- data/Rakefile +11 -0
- data/examples/deck.rb +48 -0
- data/examples/quick_example.rb +11 -0
- data/lib/ruby-poker.rb +2 -0
- data/lib/ruby-poker/card.rb +142 -0
- data/lib/ruby-poker/poker_hand.rb +487 -0
- data/ruby-poker.gemspec +29 -0
- data/test/test_card.rb +72 -0
- data/test/test_helper.rb +7 -0
- data/test/test_poker_hand.rb +523 -0
- metadata +78 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
2012-10-30 (0.4.0)
|
2
|
+
* Forked from git://github.com/robolson/ruby-poker.git
|
3
|
+
|
4
|
+
2009-07-12 (0.3.2)
|
5
|
+
* Reorganized ruby-poker's lib folder to match the standard layout for gems. This makes ruby-poker compatible with Rip.
|
6
|
+
* Bug [#26276] improper two_pair? behavior. Applied patch by Uro.
|
7
|
+
* Changed protected methods in PokerHand to private
|
8
|
+
* Added natural_value method to Card
|
9
|
+
|
10
|
+
2009-01-24 (0.3.1)
|
11
|
+
* Bug [#23623] undefined method <=> for nil:NilClass
|
12
|
+
|
13
|
+
2008-12-30 (0.3.1)
|
14
|
+
* Bug (#20407) Raise an exception when creating a new hand with duplicates
|
15
|
+
* Added PokerHand#uniq method
|
16
|
+
* Removed deprecated `Gem::manage_gems` from Rakefile
|
17
|
+
|
18
|
+
2008-05-17 (0.3.0)
|
19
|
+
* 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.
|
20
|
+
* Replaced `PokerHand#arranged_hand` with `PokerHand#sort_using_rank` which is more descriptive. This loosely corresponds to bug #20194.
|
21
|
+
* Bug [#20196] 'rank' goes into an infinite loop.
|
22
|
+
* Bug [#20195] Allows the same card to be entered into the hand.
|
23
|
+
* Bug [#20344] sort_using_rank does not return expected results
|
24
|
+
|
25
|
+
2008-04-20 (0.2.4)
|
26
|
+
* Modernized the Rakefile
|
27
|
+
* Updated to be compatible with Ruby 1.9
|
28
|
+
|
29
|
+
2008-04-06 (0.2.2)
|
30
|
+
* Fixed bug where two hands that had the same values but different suits returned not equal
|
31
|
+
|
32
|
+
2008-02-08 (0.2.1)
|
33
|
+
* Cards can be added to a hand after it is created by using (<<) on a PokerHand
|
34
|
+
* Cards can be deleted from a hand with PokerHand.delete()
|
35
|
+
|
36
|
+
2008-01-21 (0.2.0)
|
37
|
+
* Merged Patrick Hurley's poker solver
|
38
|
+
* Added support for hands with >5 cards
|
39
|
+
* Straights with a low Ace count now
|
40
|
+
* to_s on a PokerHand now includes the rank after the card list
|
41
|
+
* Finally wrote the Unit Tests suite
|
42
|
+
|
43
|
+
2008-01-12 (0.1.2)
|
44
|
+
* Fixed critical bug that was stopping the whole program to not work
|
45
|
+
* Added some test cases as a result
|
46
|
+
* More test cases coming soon
|
47
|
+
|
48
|
+
2008-01-12 (0.1.1)
|
49
|
+
* Ranks are now a class.
|
50
|
+
* Extracted card, rank, and arrays methods to individual files
|
51
|
+
* Added gem packaging
|
52
|
+
|
53
|
+
2008-01-10 (0.1.0)
|
54
|
+
* Initial version
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (C) 2012 Bryan Housel
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Poker library in Ruby
|
2
|
+
|
3
|
+
## Forked From
|
4
|
+
|
5
|
+
git://github.com/robolson/ruby-poker.git
|
6
|
+
|
7
|
+
## Description
|
8
|
+
|
9
|
+
Ruby-Poker handles the logic for getting the rank of a poker hand. It can also
|
10
|
+
be used to compare two or more hands to determine which hand has the highest
|
11
|
+
poker value.
|
12
|
+
|
13
|
+
Card representations can be passed to the PokerHand constructor as a string or
|
14
|
+
an array. Face cards (cards ten, jack, queen, king, and ace) are created using
|
15
|
+
their letter representation (T, J, Q, K, A).
|
16
|
+
|
17
|
+
## Install
|
18
|
+
|
19
|
+
gem install ruby-poker
|
20
|
+
|
21
|
+
## Example
|
22
|
+
|
23
|
+
require 'rubygems'
|
24
|
+
require 'ruby-poker'
|
25
|
+
|
26
|
+
hand1 = PokerHand.new("8H 9C TC JD QH")
|
27
|
+
hand2 = PokerHand.new(["3D", "3C", "3S", "KD", "AH"])
|
28
|
+
puts hand1 => 8h 9c Tc Jd Qh (Straight)
|
29
|
+
puts hand1.just_cards => 8h 9c Tc Jd Qh
|
30
|
+
puts hand1.rank => Straight
|
31
|
+
puts hand2 => 3d 3c 3s Kd Ah (Three of a kind)
|
32
|
+
puts hand2.rank => Three of a kind
|
33
|
+
puts hand1 > hand2 => true
|
34
|
+
|
35
|
+
## Duplicates
|
36
|
+
|
37
|
+
By default ruby-poker will not raise an exception if you add the same card to
|
38
|
+
a hand twice. You can tell ruby-poker to not allow duplicates by doing the
|
39
|
+
following
|
40
|
+
|
41
|
+
PokerHand.allow_duplicates = false
|
42
|
+
|
43
|
+
Place that line near the beginning of your program. The change is program wide
|
44
|
+
so once allow_duplicates is set to false, _all_ poker hands will raise an
|
45
|
+
exception if a duplicate card is added to the hand.
|
46
|
+
|
47
|
+
## Compatibility
|
48
|
+
|
49
|
+
Ruby-Poker is compatible with Ruby 1.8 and Ruby 1.9
|
50
|
+
|
data/Rakefile
ADDED
data/examples/deck.rb
ADDED
@@ -0,0 +1,48 @@
|
|
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
|
data/lib/ruby-poker.rb
ADDED
@@ -0,0 +1,142 @@
|
|
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' => 1, # this is a magic low ace
|
12
|
+
'2' => 2,
|
13
|
+
'3' => 3,
|
14
|
+
'4' => 4,
|
15
|
+
'5' => 5,
|
16
|
+
'6' => 6,
|
17
|
+
'7' => 7,
|
18
|
+
'8' => 8,
|
19
|
+
'9' => 9,
|
20
|
+
'T' => 10,
|
21
|
+
'J' => 11,
|
22
|
+
'Q' => 12,
|
23
|
+
'K' => 13,
|
24
|
+
'A' => 14
|
25
|
+
}
|
26
|
+
|
27
|
+
def Card.face_value(face)
|
28
|
+
face.upcase!
|
29
|
+
if face == 'L' || !FACE_VALUES.has_key?(face)
|
30
|
+
nil
|
31
|
+
else
|
32
|
+
FACE_VALUES[face] - 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def build_from_value(value)
|
39
|
+
@value = value
|
40
|
+
@suit = value / FACES.size()
|
41
|
+
@face = (value % FACES.size())
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_from_face_suit(face, suit)
|
45
|
+
suit.downcase!
|
46
|
+
@face = Card::face_value(face)
|
47
|
+
@suit = SUIT_LOOKUP[suit]
|
48
|
+
raise ArgumentError, "Invalid card: \"#{face}#{suit}\"" unless @face and @suit
|
49
|
+
@value = (@suit * FACES.size()) + (@face - 1)
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_from_face_suit_values(face, suit)
|
53
|
+
build_from_value((face - 1) + (suit * FACES.size()))
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_from_string(card)
|
57
|
+
build_from_face_suit(card[0,1], card[1,1])
|
58
|
+
end
|
59
|
+
|
60
|
+
# Constructs this card object from another card object
|
61
|
+
def build_from_card(card)
|
62
|
+
@value = card.value
|
63
|
+
@suit = card.suit
|
64
|
+
@face = card.face
|
65
|
+
end
|
66
|
+
|
67
|
+
public
|
68
|
+
|
69
|
+
def initialize(*args)
|
70
|
+
if (args.size == 1)
|
71
|
+
value = args.first
|
72
|
+
if (value.respond_to?(:to_card))
|
73
|
+
build_from_card(value)
|
74
|
+
elsif (value.respond_to?(:to_str))
|
75
|
+
build_from_string(value)
|
76
|
+
elsif (value.respond_to?(:to_int))
|
77
|
+
build_from_value(value)
|
78
|
+
end
|
79
|
+
elsif (args.size == 2)
|
80
|
+
arg1, arg2 = args
|
81
|
+
if (arg1.respond_to?(:to_str) &&
|
82
|
+
arg2.respond_to?(:to_str))
|
83
|
+
build_from_face_suit(arg1, arg2)
|
84
|
+
elsif (arg1.respond_to?(:to_int) &&
|
85
|
+
arg2.respond_to?(:to_int))
|
86
|
+
build_from_face_suit_values(arg1, arg2)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :suit, :face, :value
|
92
|
+
include Comparable
|
93
|
+
|
94
|
+
# Returns a string containing the representation of Card
|
95
|
+
#
|
96
|
+
# Card.new("7c").to_s # => "7c"
|
97
|
+
def to_s
|
98
|
+
FACES[@face].chr + SUITS[@suit].chr
|
99
|
+
end
|
100
|
+
|
101
|
+
# If to_card is called on a `Card` it should return itself
|
102
|
+
def to_card
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Compare the face value of this card with another card. Returns:
|
107
|
+
# -1 if self is less than card2
|
108
|
+
# 0 if self is the same face value of card2
|
109
|
+
# 1 if self is greater than card2
|
110
|
+
def <=> card2
|
111
|
+
@face <=> card2.face
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns true if the cards are the same card. Meaning they
|
115
|
+
# have the same suit and the same face value.
|
116
|
+
def == card2
|
117
|
+
@value == card2.value
|
118
|
+
end
|
119
|
+
alias :eql? :==
|
120
|
+
|
121
|
+
# Compute a hash-code for this Card. Two Cards with the same
|
122
|
+
# content will have the same hash code (and will compare using eql?).
|
123
|
+
def hash
|
124
|
+
@value.hash
|
125
|
+
end
|
126
|
+
|
127
|
+
# A card's natural value is the closer to it's intuitive value in a deck
|
128
|
+
# in the range of 1 to 52. Aces are low with a value of 1. Uses the bridge
|
129
|
+
# order of suits: clubs, diamonds, hearts, and spades. The formula used is:
|
130
|
+
# If the suit is clubs, the natural value is the face value (remember
|
131
|
+
# Aces are low). If the suit is diamonds, it is the clubs value plus 13.
|
132
|
+
# If the suit is hearts, it is plus 26. If it is spades, it is plus 39.
|
133
|
+
#
|
134
|
+
# Card.new("Ac").natural_value # => 1
|
135
|
+
# Card.new("Kc").natural_value # => 12
|
136
|
+
# Card.new("Ad").natural_value # => 13
|
137
|
+
def natural_value
|
138
|
+
natural_face = @face == 13 ? 1 : @face+1 # flip Ace from 13 to 1 and
|
139
|
+
# increment everything else by 1
|
140
|
+
natural_face + @suit * 13
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,487 @@
|
|
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") # => #<PokerHand:0x5c673c ...
|
14
|
+
# PokerHand.new(["3d", "5c", "8h", "Ks"]) # => #<PokerHand:0x5c2d6c ...
|
15
|
+
def initialize(cards = [])
|
16
|
+
@hand = case cards
|
17
|
+
when Array
|
18
|
+
cards.map do |card|
|
19
|
+
if card.is_a? Card
|
20
|
+
card
|
21
|
+
else
|
22
|
+
Card.new(card.to_s)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
when String
|
26
|
+
cards.scan(/\S{2}/).map { |str| Card.new(str) }
|
27
|
+
else
|
28
|
+
cards
|
29
|
+
end
|
30
|
+
|
31
|
+
check_for_duplicates unless allow_duplicates
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a new PokerHand object with the cards sorted by suit
|
35
|
+
# The suit order is spades, hearts, diamonds, clubs
|
36
|
+
#
|
37
|
+
# PokerHand.new("3d 5c 8h Ks").by_suit.just_cards # => "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 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/ # => #<MatchData:0x615e18>
|
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 = arrange_hand(md[0] + ' ' +
|
109
|
+
md.pre_match + ' ' + md[2] + ' ' + md.post_match)
|
110
|
+
[
|
111
|
+
[7, Card::face_value(md[1]), Card::face_value(md[3])],
|
112
|
+
arranged_hand
|
113
|
+
]
|
114
|
+
elsif (md = (by_face =~ /((.). \2.) (.*)((.). \5. \5.)/))
|
115
|
+
arranged_hand = arrange_hand(md[4] + ' ' + md[1] + ' ' +
|
116
|
+
md.pre_match + ' ' + md[3] + ' ' + md.post_match)
|
117
|
+
[
|
118
|
+
[7, Card::face_value(md[5]), Card::face_value(md[2])],
|
119
|
+
arranged_hand
|
120
|
+
]
|
121
|
+
else
|
122
|
+
false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def flush?
|
127
|
+
if (md = (by_suit =~ /(.)(.) (.)\2 (.)\2 (.)\2 (.)\2/))
|
128
|
+
[
|
129
|
+
[
|
130
|
+
6,
|
131
|
+
Card::face_value(md[1]),
|
132
|
+
*(md[3..6].map { |f| Card::face_value(f) })
|
133
|
+
],
|
134
|
+
arrange_hand(md)
|
135
|
+
]
|
136
|
+
else
|
137
|
+
false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def straight?
|
142
|
+
result = false
|
143
|
+
if hand.size >= 5
|
144
|
+
transform = delta_transform
|
145
|
+
# note we can have more than one delta 0 that we
|
146
|
+
# need to shuffle to the back of the hand
|
147
|
+
i = 0
|
148
|
+
until transform.match(/^\S{3}( [1-9x]\S\S)+( 0\S\S)*$/) or i >= hand.size do
|
149
|
+
# only do this once per card in the hand to avoid entering an
|
150
|
+
# infinite loop if all of the cards in the hand are the same
|
151
|
+
transform.gsub!(/(\s0\S\S)(.*)/, "\\2\\1") # moves the front card to the back of the string
|
152
|
+
i += 1
|
153
|
+
end
|
154
|
+
if (md = (/.(.). 1.. 1.. 1.. 1../.match(transform)))
|
155
|
+
high_card = Card::face_value(md[1])
|
156
|
+
arranged_hand = fix_low_ace_display(md[0] + ' ' + md.pre_match + ' ' + md.post_match)
|
157
|
+
result = [[5, high_card], arranged_hand]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def three_of_a_kind?
|
163
|
+
if (md = (by_face =~ /(.). \1. \1./))
|
164
|
+
# get kicker
|
165
|
+
arranged_hand = arrange_hand(md)
|
166
|
+
matches = arranged_hand.match(/(?:\S\S ){2}(\S\S)/)
|
167
|
+
if matches
|
168
|
+
result = [4, Card::face_value(md[1])]
|
169
|
+
matches = arranged_hand.match(/(?:\S\S ){3}(\S)/)
|
170
|
+
result << Card::face_value($1) if matches
|
171
|
+
matches = arranged_hand.match(/(?:\S\S ){3}(\S)\S (\S)/)
|
172
|
+
result << Card::face_value($2) if matches
|
173
|
+
return [result, arranged_hand]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
false
|
177
|
+
end
|
178
|
+
|
179
|
+
def two_pair?
|
180
|
+
# \1 is the face value of the first pair
|
181
|
+
# \2 is the card in between the first pair and the second pair
|
182
|
+
# \3 is the face value of the second pair
|
183
|
+
if (md = (by_face =~ /(.). \1.(.*?) (.). \3./))
|
184
|
+
# to get the kicker this does the following
|
185
|
+
# md[0] is the regex matched above which includes the first pair and
|
186
|
+
# the second pair but also some cards in the middle so we sub them out
|
187
|
+
# then we add on the cards that came before the first pair, the cards
|
188
|
+
# that were in-between, and the cards that came after.
|
189
|
+
arranged_hand = arrange_hand(md[0].sub(md[2], '') + ' ' +
|
190
|
+
md.pre_match + ' ' + md[2] + ' ' + md.post_match)
|
191
|
+
matches = arranged_hand.match(/(?:\S\S ){3}(\S\S)/)
|
192
|
+
if matches
|
193
|
+
result = []
|
194
|
+
result << 3
|
195
|
+
result << Card::face_value(md[1]) # face value of the first pair
|
196
|
+
result << Card::face_value(md[3]) # face value of the second pair
|
197
|
+
matches = arranged_hand.match(/(?:\S\S ){4}(\S)/)
|
198
|
+
result << Card::face_value($1) if matches # face value of the kicker
|
199
|
+
return [result, arranged_hand]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
false
|
203
|
+
end
|
204
|
+
|
205
|
+
def pair?
|
206
|
+
if (md = (by_face =~ /(.). \1./))
|
207
|
+
arranged_hand_str = arrange_hand(md)
|
208
|
+
arranged_hand = PokerHand.new(arranged_hand_str)
|
209
|
+
|
210
|
+
if arranged_hand.hand[0].face == arranged_hand.hand[1].face &&
|
211
|
+
arranged_hand.hand[0].suit != arranged_hand.hand[1].suit
|
212
|
+
result = [2, arranged_hand.hand[0].face]
|
213
|
+
result << arranged_hand.hand[2].face if arranged_hand.size > 2
|
214
|
+
result << arranged_hand.hand[3].face if arranged_hand.size > 3
|
215
|
+
result << arranged_hand.hand[4].face if arranged_hand.size > 4
|
216
|
+
|
217
|
+
return [result, arranged_hand_str]
|
218
|
+
end
|
219
|
+
else
|
220
|
+
false
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def highest_card?
|
225
|
+
result = by_face
|
226
|
+
[[1, *result.face_values[0..result.face_values.length]], result.hand.join(' ')]
|
227
|
+
end
|
228
|
+
|
229
|
+
OPS = [
|
230
|
+
['Royal Flush', :royal_flush? ],
|
231
|
+
['Straight Flush', :straight_flush? ],
|
232
|
+
['Four of a kind', :four_of_a_kind? ],
|
233
|
+
['Full house', :full_house? ],
|
234
|
+
['Flush', :flush? ],
|
235
|
+
['Straight', :straight? ],
|
236
|
+
['Three of a kind', :three_of_a_kind?],
|
237
|
+
['Two pair', :two_pair? ],
|
238
|
+
['Pair', :pair? ],
|
239
|
+
['Highest Card', :highest_card? ],
|
240
|
+
]
|
241
|
+
|
242
|
+
# Returns the verbose hand rating
|
243
|
+
#
|
244
|
+
# PokerHand.new("4s 5h 6c 7d 8s").hand_rating # => "Straight"
|
245
|
+
def hand_rating
|
246
|
+
OPS.map { |op|
|
247
|
+
(method(op[1]).call()) ? op[0] : false
|
248
|
+
}.find { |v| v }
|
249
|
+
end
|
250
|
+
|
251
|
+
alias :rank :hand_rating
|
252
|
+
|
253
|
+
def score
|
254
|
+
# OPS.map returns an array containing the result of calling each OPS method again
|
255
|
+
# the poker hand. The non-nil cell closest to the front of the array represents
|
256
|
+
# the highest ranking.
|
257
|
+
# find([0]) returns [0] instead of nil if the hand does not match any of the rankings
|
258
|
+
# which is not likely to occur since every hand should at least have a highest card
|
259
|
+
OPS.map { |op|
|
260
|
+
method(op[1]).call()
|
261
|
+
}.find([0]) { |score| score }
|
262
|
+
end
|
263
|
+
|
264
|
+
# Returns a string of the hand arranged based on its rank. Usually this will be the
|
265
|
+
# same as by_face but there are some cases where it makes a difference.
|
266
|
+
#
|
267
|
+
# ph = PokerHand.new("As 3s 5s 2s 4s")
|
268
|
+
# ph.sort_using_rank # => "5s 4s 3s 2s As"
|
269
|
+
# ph.by_face.just_cards # => "As 5s 4s 3s 2s"
|
270
|
+
def sort_using_rank
|
271
|
+
score[1]
|
272
|
+
end
|
273
|
+
|
274
|
+
# Returns string with a listing of the cards in the hand followed by the hand's rank.
|
275
|
+
#
|
276
|
+
# h = PokerHand.new("8c 8s")
|
277
|
+
# h.to_s # => "8c 8s (Pair)"
|
278
|
+
def to_s
|
279
|
+
just_cards + " (" + hand_rating + ")"
|
280
|
+
end
|
281
|
+
|
282
|
+
# Returns an array of `Card` objects that make up the `PokerHand`.
|
283
|
+
def to_a
|
284
|
+
@hand
|
285
|
+
end
|
286
|
+
alias :to_ary :to_a
|
287
|
+
|
288
|
+
def <=> other_hand
|
289
|
+
self.score[0].compact <=> other_hand.score[0].compact
|
290
|
+
end
|
291
|
+
|
292
|
+
# Add a card to the hand
|
293
|
+
#
|
294
|
+
# hand = PokerHand.new("5d")
|
295
|
+
# hand << "6s" # => Add a six of spades to the hand by passing a string
|
296
|
+
# hand << ["7h", "8d"] # => Add multiple cards to the hand using an array
|
297
|
+
def << new_cards
|
298
|
+
if new_cards.is_a?(Card) || new_cards.is_a?(String)
|
299
|
+
new_cards = [new_cards]
|
300
|
+
end
|
301
|
+
|
302
|
+
new_cards.each do |nc|
|
303
|
+
unless allow_duplicates
|
304
|
+
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}/
|
305
|
+
end
|
306
|
+
|
307
|
+
@hand << Card.new(nc)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Remove a card from the hand.
|
312
|
+
#
|
313
|
+
# hand = PokerHand.new("5d Jd")
|
314
|
+
# hand.delete("Jd") # => #<Card:0x5d0674 @value=23, @face=10, @suit=1>
|
315
|
+
# hand.just_cards # => "5d"
|
316
|
+
def delete card
|
317
|
+
@hand.delete(Card.new(card))
|
318
|
+
end
|
319
|
+
|
320
|
+
# Same concept as Array#uniq
|
321
|
+
def uniq
|
322
|
+
PokerHand.new(@hand.uniq)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Resolving methods are just passed directly down to the @hand array
|
326
|
+
RESOLVING_METHODS = [:each, :size, :-]
|
327
|
+
RESOLVING_METHODS.each do |method|
|
328
|
+
class_eval %{
|
329
|
+
def #{method}(*args, &block)
|
330
|
+
@hand.#{method}(*args, &block)
|
331
|
+
end
|
332
|
+
}
|
333
|
+
end
|
334
|
+
|
335
|
+
def allow_duplicates
|
336
|
+
@@allow_duplicates
|
337
|
+
end
|
338
|
+
|
339
|
+
# Checks whether the hand matches usual expressions like AA, AK, AJ+, 66+, AQs, AQo...
|
340
|
+
#
|
341
|
+
# Valid expressions:
|
342
|
+
# * "AJ": Matches exact faces (in this case an Ace and a Jack), suited or not
|
343
|
+
# * "AJs": Same but suited only
|
344
|
+
# * "AJo": Same but offsuit only
|
345
|
+
# * "AJ+": Matches an Ace with any card >= Jack, suited or not
|
346
|
+
# * "AJs+": Same but suited only
|
347
|
+
# * "AJo+": Same but offsuit only
|
348
|
+
# * "JJ+": Matches any pair >= "JJ".
|
349
|
+
# * "8T+": Matches connectors (in this case with 1 gap : 8T, 9J, TQ, JK, QA)
|
350
|
+
# * "8Ts+": Same but suited only
|
351
|
+
# * "8To+": Same but offsuit only
|
352
|
+
#
|
353
|
+
# 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").
|
354
|
+
#
|
355
|
+
# The expression can be an array of expressions. In this case the method returns true if any expression matches.
|
356
|
+
#
|
357
|
+
# This method only works on hands with 2 cards.
|
358
|
+
#
|
359
|
+
# PokerHand.new('Ah Ad').match? 'AA' # => true
|
360
|
+
# PokerHand.new('Ah Kd').match? 'AQ+' # => true
|
361
|
+
# PokerHand.new('Jc Qc').match? '89s+' # => true
|
362
|
+
# PokerHand.new('Ah Jd').match? %w( 22+ A6s+ AJ+ ) # => true
|
363
|
+
# PokerHand.new('Ah Td').match? %w( 22+ A6s+ AJ+ ) # => false
|
364
|
+
#
|
365
|
+
def match? expression
|
366
|
+
raise "Hands with #{@hand.size} cards is not supported" unless @hand.size == 2
|
367
|
+
|
368
|
+
if expression.is_a? Array
|
369
|
+
return expression.any? { |e| match?(e) }
|
370
|
+
end
|
371
|
+
|
372
|
+
faces = @hand.map { |card| card.face }.sort.reverse
|
373
|
+
suited = @hand.map { |card| card.suit }.uniq.size == 1
|
374
|
+
if expression =~ /^(.)(.)(s|o|)(\+|)$/
|
375
|
+
face1 = Card.face_value($1)
|
376
|
+
face2 = Card.face_value($2)
|
377
|
+
raise ArgumentError, "Invalid expression: #{expression.inspect}" unless face1 and face2
|
378
|
+
suit_match = $3
|
379
|
+
plus = ($4 != "")
|
380
|
+
|
381
|
+
if plus
|
382
|
+
if face1 == face2
|
383
|
+
face_match = (faces.first == faces.last and faces.first >= face1)
|
384
|
+
elsif face1 > face2
|
385
|
+
face_match = (faces.first == face1 and faces.last >= face2)
|
386
|
+
else
|
387
|
+
face_match = ((faces.first - faces.last) == (face2 - face1) and faces.last >= face1)
|
388
|
+
end
|
389
|
+
else
|
390
|
+
expression_faces = [face1, face2].sort.reverse
|
391
|
+
face_match = (expression_faces == faces)
|
392
|
+
end
|
393
|
+
case suit_match
|
394
|
+
when ''
|
395
|
+
face_match
|
396
|
+
when 's'
|
397
|
+
face_match and suited
|
398
|
+
when 'o'
|
399
|
+
face_match and !suited
|
400
|
+
end
|
401
|
+
else
|
402
|
+
raise ArgumentError, "Invalid expression: #{expression.inspect}"
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def +(other)
|
407
|
+
cards = @hand.map { |card| Card.new(card) }
|
408
|
+
case other
|
409
|
+
when String
|
410
|
+
cards << Card.new(other)
|
411
|
+
when Card
|
412
|
+
cards << other
|
413
|
+
when PokerHand
|
414
|
+
cards += other.hand
|
415
|
+
else
|
416
|
+
raise ArgumentError, "Invalid argument: #{other.inspect}"
|
417
|
+
end
|
418
|
+
PokerHand.new(cards)
|
419
|
+
end
|
420
|
+
|
421
|
+
private
|
422
|
+
|
423
|
+
def check_for_duplicates
|
424
|
+
if @hand.size != @hand.uniq.size && !allow_duplicates
|
425
|
+
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."
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
# if md is a string, arrange_hand will remove extra white space
|
430
|
+
# if md is a MatchData, arrange_hand returns the matched segment
|
431
|
+
# followed by the pre_match and the post_match
|
432
|
+
def arrange_hand(md)
|
433
|
+
hand = if md.respond_to?(:to_str)
|
434
|
+
md
|
435
|
+
else
|
436
|
+
md[0] + ' ' + md.pre_match + md.post_match
|
437
|
+
end
|
438
|
+
hand.strip.squeeze(" ") # remove extra whitespace
|
439
|
+
end
|
440
|
+
|
441
|
+
# delta transform creates a version of the cards where the delta
|
442
|
+
# between card values is in the string, so a regexp can then match a
|
443
|
+
# straight and/or straight flush
|
444
|
+
def delta_transform(use_suit = false)
|
445
|
+
aces = @hand.select { |c| c.face == Card::face_value('A') }
|
446
|
+
aces.map! { |c| Card.new(1,c.suit) }
|
447
|
+
|
448
|
+
base = if (use_suit)
|
449
|
+
(@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
|
450
|
+
else
|
451
|
+
(@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
|
452
|
+
end
|
453
|
+
|
454
|
+
result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
|
455
|
+
if (prev_card)
|
456
|
+
delta = prev_card - card.face
|
457
|
+
else
|
458
|
+
delta = 0
|
459
|
+
end
|
460
|
+
# does not really matter for my needs
|
461
|
+
delta = 'x' if (delta > 9 || delta < 0)
|
462
|
+
delta_hand += delta.to_s + card.to_s + ' '
|
463
|
+
[delta_hand, card.face]
|
464
|
+
end
|
465
|
+
|
466
|
+
# we just want the delta transform, not the last cards face too
|
467
|
+
result[0].chop
|
468
|
+
end
|
469
|
+
|
470
|
+
def fix_low_ace_display(arranged_hand)
|
471
|
+
# remove card deltas (this routine is only used for straights)
|
472
|
+
arranged_hand.gsub!(/\S(\S\S)\s*/, "\\1 ")
|
473
|
+
|
474
|
+
# Fix "low aces"
|
475
|
+
arranged_hand.gsub!(/L(\S)/, "A\\1")
|
476
|
+
|
477
|
+
# Remove duplicate aces (this will not work if you have
|
478
|
+
# multiple decks or wild cards)
|
479
|
+
arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
|
480
|
+
|
481
|
+
# cleanup white space
|
482
|
+
arranged_hand.gsub!(/\s+/, ' ')
|
483
|
+
# careful to use gsub as gsub! can return nil here
|
484
|
+
arranged_hand.gsub(/\s+$/, '')
|
485
|
+
end
|
486
|
+
|
487
|
+
end
|