bhousel-ruby-poker 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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