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 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
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.verbose = true
8
+ test.warning = true
9
+ end
10
+
11
+ task :default => :test
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
@@ -0,0 +1,11 @@
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
data/lib/ruby-poker.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'ruby-poker/card'
2
+ require 'ruby-poker/poker_hand'
@@ -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