ruby-poker 0.3.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9070959501edf045c9d745325c51573314ecbc11
4
+ data.tar.gz: 8616e190cbcce32c2ad4f9455df760f0649fbb59
5
+ SHA512:
6
+ metadata.gz: f0dae2e671b57b3c98d092b200544b8e4423adedf4d66a4c360f202e2df39b9ef96f1e62b66286c1a78d1bd8f2cbd4f335df2c8831bffbd14fbccf292bc6a0ba
7
+ data.tar.gz: deb4dba594571df9ec74a1b368f11d86e10db12003bc849cecf19810eed73d94c252d14c4d77354332399d3876dfb2c5bfd3ef0b8346d0badc893019ddf001bc
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ 2013-12-28 (1.0.0)
2
+ * Add Gemfile
3
+ * Add "Empty Hand" rank for empty hands. Fixes #1.
4
+ * Simplify internal calculation of `value` in Card class. Fixes #2.
5
+
1
6
  2009-07-12 (0.3.2)
2
7
  * Reorganized ruby-poker's lib folder to match the standard layout for gems. This makes ruby-poker compatible with Rip.
3
8
  * Bug [#26276] improper two_pair? behavior. Applied patch by Uro.
@@ -6,7 +11,7 @@
6
11
 
7
12
  2009-01-24 (0.3.1)
8
13
  * Bug [#23623] undefined method <=> for nil:NilClass
9
-
14
+
10
15
  2008-12-30 (0.3.1)
11
16
  * Bug (#20407) Raise an exception when creating a new hand with duplicates
12
17
  * Added PokerHand#uniq method
@@ -18,34 +23,34 @@
18
23
  * Bug [#20196] 'rank' goes into an infinite loop.
19
24
  * Bug [#20195] Allows the same card to be entered into the hand.
20
25
  * Bug [#20344] sort_using_rank does not return expected results
21
-
26
+
22
27
  2008-04-20 (0.2.4)
23
28
  * Modernized the Rakefile
24
29
  * Updated to be compatible with Ruby 1.9
25
-
30
+
26
31
  2008-04-06 (0.2.2)
27
32
  * Fixed bug where two hands that had the same values but different suits returned not equal
28
-
33
+
29
34
  2008-02-08 (0.2.1)
30
35
  * Cards can be added to a hand after it is created by using (<<) on a PokerHand
31
36
  * Cards can be deleted from a hand with PokerHand.delete()
32
-
37
+
33
38
  2008-01-21 (0.2.0)
34
39
  * Merged Patrick Hurley's poker solver
35
40
  * Added support for hands with >5 cards
36
41
  * Straights with a low Ace count now
37
42
  * to_s on a PokerHand now includes the rank after the card list
38
43
  * Finally wrote the Unit Tests suite
39
-
44
+
40
45
  2008-01-12 (0.1.2)
41
- * Fixed critical bug that was stopping the whole program to not work
46
+ * Fixed critical bug that was causing the whole program to not work
42
47
  * Added some test cases as a result
43
48
  * More test cases coming soon
44
-
49
+
45
50
  2008-01-12 (0.1.1)
46
51
  * Ranks are now a class.
47
52
  * Extracted card, rank, and arrays methods to individual files
48
53
  * Added gem packaging
49
-
54
+
50
55
  2008-01-10 (0.1.0)
51
56
  * Initial version
data/README.rdoc CHANGED
@@ -1,9 +1,9 @@
1
1
  = Poker library in Ruby
2
2
  ===
3
3
 
4
- Author:: {Rob Olson}[http://thinkingdigitally.com]
4
+ Author:: {Rob Olson}[https://github.com/robolson]
5
5
  Email:: [first name] [at] thinkingdigitally.com
6
- GitHub:: http://github.com/robolson/ruby-poker
6
+ GitHub:: https://github.com/robolson/ruby-poker
7
7
 
8
8
  == Description
9
9
 
@@ -13,13 +13,13 @@ Card representations can be passed to the PokerHand constructor as a string or a
13
13
 
14
14
  == Install
15
15
 
16
- sudo gem install ruby-poker
16
+ gem install ruby-poker
17
17
 
18
18
  == Example
19
19
 
20
20
  require 'rubygems'
21
21
  require 'ruby-poker'
22
-
22
+
23
23
  hand1 = PokerHand.new("8H 9C TC JD QH")
24
24
  hand2 = PokerHand.new(["3D", "3C", "3S", "KD", "AH"])
25
25
  puts hand1 => 8h 9c Tc Jd Qh (Straight)
@@ -34,12 +34,16 @@ Card representations can be passed to the PokerHand constructor as a string or a
34
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
35
 
36
36
  PokerHand.allow_duplicates = false
37
-
37
+
38
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
39
 
40
40
  == Compatibility
41
41
 
42
- Ruby-Poker is compatible with Ruby 1.8.6 and Ruby 1.9.1.
42
+ Ruby-Poker is compatible with Ruby 1.8, Ruby 1.9, Ruby 2.0.
43
+
44
+ == RDoc
45
+
46
+ View the {generated documentation for ruby-poker}[http://rdoc.info/gems/ruby-poker/frames] on rdoc.info.
43
47
 
44
48
  == History
45
49
 
@@ -47,4 +51,4 @@ In the 0.2.0 release Patrick Hurley's Texas Holdem code from http://www.rubyquiz
47
51
 
48
52
  == License
49
53
 
50
- This is free software; you can redistribute it and/or modify it under the terms of the BSD license. See LICENSE for more details.
54
+ This is free software; you can redistribute it and/or modify it under the terms of the BSD license. See LICENSE for more details.
data/Rakefile CHANGED
@@ -1,70 +1,9 @@
1
- require 'rubygems'
2
- require 'rake'
3
-
4
- begin
5
- require 'metric_fu'
6
- rescue LoadError
7
- end
8
-
9
- RUBYPOKER_VERSION = "0.3.2"
10
-
11
- spec = Gem::Specification.new do |s|
12
- s.name = "ruby-poker"
13
- s.version = RUBYPOKER_VERSION
14
- s.date = "2009-07-27"
15
- s.rubyforge_project = "rubypoker"
16
- s.platform = Gem::Platform::RUBY
17
- s.summary = "Poker library in Ruby"
18
- s.description = "Ruby library for comparing poker hands and determining the winner."
19
- s.author = "Rob Olson"
20
- s.email = "rob@thinkingdigitally.com"
21
- s.homepage = "http://github.com/robolson/ruby-poker"
22
- s.has_rdoc = true
23
- s.files = ["CHANGELOG",
24
- "examples/deck.rb",
25
- "examples/quick_example.rb",
26
- "lib/ruby-poker.rb",
27
- "lib/ruby-poker/card.rb",
28
- "lib/ruby-poker/poker_hand.rb",
29
- "LICENSE",
30
- "Rakefile",
31
- "README.rdoc",
32
- "ruby-poker.gemspec"]
33
- s.test_files = ["test/test_helper.rb", "test/test_card.rb", "test/test_poker_hand.rb"]
34
- s.require_paths << 'lib'
35
-
36
- s.extra_rdoc_files = ["README.rdoc", "CHANGELOG", "LICENSE"]
37
- s.rdoc_options << '--title' << 'Ruby Poker Documentation' <<
38
- '--main' << 'README.rdoc' <<
39
- '--inline-source' << '-q'
40
-
41
- s.add_development_dependency('thoughtbot-shoulda', '> 2.0.0')
42
- end
43
-
44
- require 'rake/gempackagetask'
45
- Rake::GemPackageTask.new(spec) do |pkg|
46
- pkg.need_tar = true
47
- pkg.need_zip = true
48
- end
49
-
50
1
  require 'rake/testtask'
51
- Rake::TestTask.new(:test) do |test|
52
- test.libs << 'lib' << 'test'
53
- test.verbose = true
54
- test.warning = true
55
- end
56
-
57
- desc "Start autotest"
58
- task :autotest do
59
- ruby "-I lib -w /usr/bin/autotest"
60
- end
61
-
62
- require 'rake/rdoctask'
63
- Rake::RDocTask.new(:docs) do |rdoc|
64
- rdoc.main = 'README.rdoc'
65
- rdoc.rdoc_dir = 'rdoc'
66
- rdoc.title = "Ruby Poker #{RUBYPOKER_VERSION}"
67
- rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG', 'LICENSE', 'lib/**/*.rb')
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']
68
7
  end
69
8
 
70
- task :default => :test
9
+ task :default => :test
data/examples/deck.rb CHANGED
@@ -10,7 +10,7 @@ class Deck
10
10
  end
11
11
  shuffle
12
12
  end
13
-
13
+
14
14
  def shuffle
15
15
  @cards = @cards.sort_by { rand }
16
16
  return self
@@ -21,7 +21,7 @@ class Deck
21
21
  def deal
22
22
  @cards.pop
23
23
  end
24
-
24
+
25
25
  # delete an array or a single card from the deck
26
26
  # converts a string to a new card, if a string is given
27
27
  def burn(burn_cards)
@@ -29,7 +29,7 @@ class Deck
29
29
  if burn_cards.is_a?(Card) || burn_cards.is_a?(String)
30
30
  burn_cards = [burn_cards]
31
31
  end
32
-
32
+
33
33
  burn_cards.map! do |c|
34
34
  c = Card.new(c) unless c.class == Card
35
35
  @cards.delete(c)
data/lib/ruby-poker.rb CHANGED
@@ -1,3 +1,2 @@
1
- $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) # For use/testing when no gem is installed
2
1
  require 'ruby-poker/card'
3
2
  require 'ruby-poker/poker_hand'
@@ -8,98 +8,98 @@ class Card
8
8
  's' => 3
9
9
  }
10
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
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
25
  }
26
26
 
27
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
28
+ FACE_VALUES[face.upcase]
34
29
  end
35
30
 
36
31
  private
37
-
38
- def build_from_value(value)
39
- @value = value
40
- @suit = value / FACES.size()
41
- @face = (value % FACES.size())
32
+
33
+ def build_from_value(given_value)
34
+ @suit = given_value / 13
35
+ @face = given_value % 13
42
36
  end
43
37
 
44
38
  def build_from_face_suit(face, suit)
45
39
  suit.downcase!
46
40
  @face = Card::face_value(face)
47
41
  @suit = SUIT_LOOKUP[suit]
48
- @value = (@suit * FACES.size()) + (@face - 1)
42
+ raise ArgumentError, "Invalid card: \"#{face}#{suit}\"" unless @face and @suit
49
43
  end
50
44
 
51
- def build_from_face_suit_values(face, suit)
52
- build_from_value((face - 1) + (suit * FACES.size()))
45
+ def build_from_face_suit_values(face_int, suit_int)
46
+ @face = face_int
47
+ @suit = suit_int
53
48
  end
54
-
49
+
55
50
  def build_from_string(card)
56
51
  build_from_face_suit(card[0,1], card[1,1])
57
52
  end
58
-
53
+
59
54
  # Constructs this card object from another card object
60
55
  def build_from_card(card)
61
- @value = card.value
62
56
  @suit = card.suit
63
57
  @face = card.face
64
58
  end
65
-
59
+
66
60
  public
67
61
 
68
- def initialize(*value)
69
- if (value.size == 1)
70
- if (value[0].respond_to?(:to_card))
71
- build_from_card(value[0])
72
- elsif (value[0].respond_to?(:to_str))
73
- build_from_string(value[0])
74
- elsif (value[0].respond_to?(:to_int))
75
- build_from_value(value[0])
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)
76
71
  end
77
- elsif (value.size == 2)
78
- if (value[0].respond_to?(:to_str) &&
79
- value[1].respond_to?(:to_str))
80
- build_from_face_suit(value[0], value[1])
81
- elsif (value[0].respond_to?(:to_int) &&
82
- value[1].respond_to?(:to_int))
83
- build_from_face_suit_values(value[0], value[1])
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)
84
80
  end
85
81
  end
86
82
  end
87
83
 
88
- attr_reader :suit, :face, :value
84
+ attr_reader :suit, :face
89
85
  include Comparable
90
86
 
87
+ def value
88
+ (@suit * 13) + @face
89
+ end
90
+
91
91
  # Returns a string containing the representation of Card
92
92
  #
93
93
  # Card.new("7c").to_s # => "7c"
94
94
  def to_s
95
95
  FACES[@face].chr + SUITS[@suit].chr
96
96
  end
97
-
97
+
98
98
  # If to_card is called on a `Card` it should return itself
99
99
  def to_card
100
100
  self
101
101
  end
102
-
102
+
103
103
  # Compare the face value of this card with another card. Returns:
104
104
  # -1 if self is less than card2
105
105
  # 0 if self is the same face value of card2
@@ -107,20 +107,20 @@ class Card
107
107
  def <=> card2
108
108
  @face <=> card2.face
109
109
  end
110
-
110
+
111
111
  # Returns true if the cards are the same card. Meaning they
112
112
  # have the same suit and the same face value.
113
113
  def == card2
114
- @value == card2.value
114
+ value == card2.value
115
115
  end
116
116
  alias :eql? :==
117
-
117
+
118
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?).
119
+ # content will have the same hash code (and will compare using eql?).
120
120
  def hash
121
- @value.hash
121
+ value.hash
122
122
  end
123
-
123
+
124
124
  # A card's natural value is the closer to it's intuitive value in a deck
125
125
  # in the range of 1 to 52. Aces are low with a value of 1. Uses the bridge
126
126
  # order of suits: clubs, diamonds, hearts, and spades. The formula used is:
@@ -1,32 +1,34 @@
1
1
  class PokerHand
2
2
  include Comparable
3
+ include Enumerable
3
4
  attr_reader :hand
4
-
5
+
5
6
  @@allow_duplicates = true # true by default
6
7
  def self.allow_duplicates; @@allow_duplicates; end
7
8
  def self.allow_duplicates=(v); @@allow_duplicates = v; end
8
-
9
+
9
10
  # Returns a new PokerHand object. Accepts the cards represented
10
11
  # in a string or an array
11
12
  #
12
13
  # PokerHand.new("3d 5c 8h Ks") # => #<PokerHand:0x5c673c ...
13
14
  # PokerHand.new(["3d", "5c", "8h", "Ks"]) # => #<PokerHand:0x5c2d6c ...
14
15
  def initialize(cards = [])
15
- if cards.is_a? Array
16
- @hand = cards.map do |card|
16
+ @hand = case cards
17
+ when Array
18
+ cards.map do |card|
17
19
  if card.is_a? Card
18
20
  card
19
21
  else
20
22
  Card.new(card.to_s)
21
23
  end
22
24
  end
23
- elsif cards.respond_to?(:to_str)
24
- @hand = cards.scan(/\S{2,3}/).map { |str| Card.new(str) }
25
+ when String
26
+ cards.scan(/\S{2}/).map { |str| Card.new(str) }
25
27
  else
26
- @hand = cards
28
+ cards
27
29
  end
28
-
29
- check_for_duplicates if !@@allow_duplicates
30
+
31
+ check_for_duplicates unless allow_duplicates
30
32
  end
31
33
 
32
34
  # Returns a new PokerHand object with the cards sorted by suit
@@ -37,14 +39,14 @@ class PokerHand
37
39
  PokerHand.new(@hand.sort_by { |c| [c.suit, c.face] }.reverse)
38
40
  end
39
41
 
40
- # Returns a new PokerHand object with the cards sorted by value
42
+ # Returns a new PokerHand object with the cards sorted by face value
41
43
  # with the highest value first.
42
44
  #
43
45
  # PokerHand.new("3d 5c 8h Ks").by_face.just_cards # => "Ks 8h 5c 3d"
44
46
  def by_face
45
47
  PokerHand.new(@hand.sort_by { |c| [c.face, c.suit] }.reverse)
46
48
  end
47
-
49
+
48
50
  # Returns string representation of the hand without the rank
49
51
  #
50
52
  # PokerHand.new(["3c", "Kh"]).just_cards # => "3c Kh"
@@ -52,7 +54,7 @@ class PokerHand
52
54
  @hand.join(" ")
53
55
  end
54
56
  alias :cards :just_cards
55
-
57
+
56
58
  # Returns an array of the card values in the hand.
57
59
  # The values returned are 1 less than the value on the card.
58
60
  # For example: 2's will be shown as 1.
@@ -94,14 +96,11 @@ class PokerHand
94
96
  def four_of_a_kind?
95
97
  if (md = (by_face =~ /(.). \1. \1. \1./))
96
98
  # get kicker
97
- (md.pre_match + md.post_match).match(/(\S)/)
98
- [
99
- [8, Card::face_value(md[1]), Card::face_value($1)],
100
- arrange_hand(md)
101
- ]
102
- else
103
- false
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)]
104
102
  end
103
+ false
105
104
  end
106
105
 
107
106
  def full_house?
@@ -164,19 +163,17 @@ class PokerHand
164
163
  if (md = (by_face =~ /(.). \1. \1./))
165
164
  # get kicker
166
165
  arranged_hand = arrange_hand(md)
167
- arranged_hand.match(/(?:\S\S ){3}(\S)\S (\S)/)
168
- [
169
- [
170
- 4,
171
- Card::face_value(md[1]),
172
- Card::face_value($1),
173
- Card::face_value($2)
174
- ],
175
- arranged_hand
176
- ]
177
- else
178
- false
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
179
175
  end
176
+ false
180
177
  end
181
178
 
182
179
  def two_pair?
@@ -191,44 +188,52 @@ class PokerHand
191
188
  # that were in-between, and the cards that came after.
192
189
  arranged_hand = arrange_hand(md[0].sub(md[2], '') + ' ' +
193
190
  md.pre_match + ' ' + md[2] + ' ' + md.post_match)
194
- arranged_hand.match(/(?:\S\S ){4}(\S)/)
195
- [
196
- [
197
- 3,
198
- Card::face_value(md[1]), # face value of the first pair
199
- Card::face_value(md[3]), # face value of the second pair
200
- Card::face_value($1) # face value of the kicker
201
- ],
202
- arranged_hand
203
- ]
204
- else
205
- false
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
206
201
  end
202
+ false
207
203
  end
208
204
 
209
205
  def pair?
210
206
  if (md = (by_face =~ /(.). \1./))
211
- # get kicker
212
- arranged_hand = arrange_hand(md)
213
- arranged_hand.match(/(?:\S\S ){2}(\S)\S\s+(\S)\S\s+(\S)/)
214
- [
215
- [
216
- 2,
217
- Card::face_value(md[1]),
218
- Card::face_value($1),
219
- Card::face_value($2),
220
- Card::face_value($3)
221
- ],
222
- arranged_hand
223
- ]
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
224
219
  else
225
220
  false
226
221
  end
227
222
  end
228
223
 
229
224
  def highest_card?
230
- result = by_face
231
- [[1, *result.face_values[0..4]], result.hand.join(' ')]
225
+ if size > 0
226
+ result = by_face
227
+ [[1, *result.face_values[0..result.face_values.length]], result.hand.join(' ')]
228
+ else
229
+ false
230
+ end
231
+ end
232
+
233
+ def empty_hand?
234
+ if size == 0
235
+ [[0]]
236
+ end
232
237
  end
233
238
 
234
239
  OPS = [
@@ -242,6 +247,7 @@ class PokerHand
242
247
  ['Two pair', :two_pair? ],
243
248
  ['Pair', :pair? ],
244
249
  ['Highest Card', :highest_card? ],
250
+ ['Empty Hand', :empty_hand? ],
245
251
  ]
246
252
 
247
253
  # Returns the verbose hand rating
@@ -252,18 +258,16 @@ class PokerHand
252
258
  (method(op[1]).call()) ? op[0] : false
253
259
  }.find { |v| v }
254
260
  end
255
-
261
+
256
262
  alias :rank :hand_rating
257
-
263
+
258
264
  def score
259
- # OPS.map returns an array containing the result of calling each OPS method again
260
- # the poker hand. The non-nil cell closest to the front of the array represents
265
+ # OPS.map returns an array containing the result of calling each OPS method against
266
+ # the poker hand. The truthy cell closest to the front of the array represents
261
267
  # the highest ranking.
262
- # find([0]) returns [0] instead of nil if the hand does not match any of the rankings
263
- # which is not likely to occur since every hand should at least have a highest card
264
268
  OPS.map { |op|
265
269
  method(op[1]).call()
266
- }.find([0]) { |score| score }
270
+ }.find { |score| score }
267
271
  end
268
272
 
269
273
  # Returns a string of the hand arranged based on its rank. Usually this will be the
@@ -271,11 +275,11 @@ class PokerHand
271
275
  #
272
276
  # ph = PokerHand.new("As 3s 5s 2s 4s")
273
277
  # ph.sort_using_rank # => "5s 4s 3s 2s As"
274
- # ph.by_face.just_cards # => "As 5s 4s 3s 2s"
278
+ # ph.by_face.just_cards # => "As 5s 4s 3s 2s"
275
279
  def sort_using_rank
276
280
  score[1]
277
281
  end
278
-
282
+
279
283
  # Returns string with a listing of the cards in the hand followed by the hand's rank.
280
284
  #
281
285
  # h = PokerHand.new("8c 8s")
@@ -283,20 +287,19 @@ class PokerHand
283
287
  def to_s
284
288
  just_cards + " (" + hand_rating + ")"
285
289
  end
286
-
290
+
287
291
  # Returns an array of `Card` objects that make up the `PokerHand`.
288
292
  def to_a
289
293
  @hand
290
294
  end
291
-
292
295
  alias :to_ary :to_a
293
-
296
+
294
297
  def <=> other_hand
295
298
  self.score[0].compact <=> other_hand.score[0].compact
296
299
  end
297
-
300
+
298
301
  # Add a card to the hand
299
- #
302
+ #
300
303
  # hand = PokerHand.new("5d")
301
304
  # hand << "6s" # => Add a six of spades to the hand by passing a string
302
305
  # hand << ["7h", "8d"] # => Add multiple cards to the hand using an array
@@ -304,16 +307,16 @@ class PokerHand
304
307
  if new_cards.is_a?(Card) || new_cards.is_a?(String)
305
308
  new_cards = [new_cards]
306
309
  end
307
-
310
+
308
311
  new_cards.each do |nc|
309
- unless @@allow_duplicates
312
+ unless allow_duplicates
310
313
  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}/
311
314
  end
312
-
315
+
313
316
  @hand << Card.new(nc)
314
317
  end
315
318
  end
316
-
319
+
317
320
  # Remove a card from the hand.
318
321
  #
319
322
  # hand = PokerHand.new("5d Jd")
@@ -322,14 +325,14 @@ class PokerHand
322
325
  def delete card
323
326
  @hand.delete(Card.new(card))
324
327
  end
325
-
328
+
326
329
  # Same concept as Array#uniq
327
330
  def uniq
328
331
  PokerHand.new(@hand.uniq)
329
332
  end
330
-
333
+
331
334
  # Resolving methods are just passed directly down to the @hand array
332
- RESOLVING_METHODS = [:size, :+, :-]
335
+ RESOLVING_METHODS = [:each, :size, :-]
333
336
  RESOLVING_METHODS.each do |method|
334
337
  class_eval %{
335
338
  def #{method}(*args, &block)
@@ -337,20 +340,106 @@ class PokerHand
337
340
  end
338
341
  }
339
342
  end
340
-
343
+
344
+ def allow_duplicates
345
+ @@allow_duplicates
346
+ end
347
+
348
+ # Checks whether the hand matches usual expressions like AA, AK, AJ+, 66+, AQs, AQo...
349
+ #
350
+ # Valid expressions:
351
+ # * "AJ": Matches exact faces (in this case an Ace and a Jack), suited or not
352
+ # * "AJs": Same but suited only
353
+ # * "AJo": Same but offsuit only
354
+ # * "AJ+": Matches an Ace with any card >= Jack, suited or not
355
+ # * "AJs+": Same but suited only
356
+ # * "AJo+": Same but offsuit only
357
+ # * "JJ+": Matches any pair >= "JJ".
358
+ # * "8T+": Matches connectors (in this case with 1 gap : 8T, 9J, TQ, JK, QA)
359
+ # * "8Ts+": Same but suited only
360
+ # * "8To+": Same but offsuit only
361
+ #
362
+ # 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").
363
+ #
364
+ # The expression can be an array of expressions. In this case the method returns true if any expression matches.
365
+ #
366
+ # This method only works on hands with 2 cards.
367
+ #
368
+ # PokerHand.new('Ah Ad').match? 'AA' # => true
369
+ # PokerHand.new('Ah Kd').match? 'AQ+' # => true
370
+ # PokerHand.new('Jc Qc').match? '89s+' # => true
371
+ # PokerHand.new('Ah Jd').match? %w( 22+ A6s+ AJ+ ) # => true
372
+ # PokerHand.new('Ah Td').match? %w( 22+ A6s+ AJ+ ) # => false
373
+ #
374
+ def match? expression
375
+ raise "Hands with #{@hand.size} cards is not supported" unless @hand.size == 2
376
+
377
+ if expression.is_a? Array
378
+ return expression.any? { |e| match?(e) }
379
+ end
380
+
381
+ faces = @hand.map { |card| card.face }.sort.reverse
382
+ suited = @hand.map { |card| card.suit }.uniq.size == 1
383
+ if expression =~ /^(.)(.)(s|o|)(\+|)$/
384
+ face1 = Card.face_value($1)
385
+ face2 = Card.face_value($2)
386
+ raise ArgumentError, "Invalid expression: #{expression.inspect}" unless face1 and face2
387
+ suit_match = $3
388
+ plus = ($4 != "")
389
+
390
+ if plus
391
+ if face1 == face2
392
+ face_match = (faces.first == faces.last and faces.first >= face1)
393
+ elsif face1 > face2
394
+ face_match = (faces.first == face1 and faces.last >= face2)
395
+ else
396
+ face_match = ((faces.first - faces.last) == (face2 - face1) and faces.last >= face1)
397
+ end
398
+ else
399
+ expression_faces = [face1, face2].sort.reverse
400
+ face_match = (expression_faces == faces)
401
+ end
402
+ case suit_match
403
+ when ''
404
+ face_match
405
+ when 's'
406
+ face_match and suited
407
+ when 'o'
408
+ face_match and !suited
409
+ end
410
+ else
411
+ raise ArgumentError, "Invalid expression: #{expression.inspect}"
412
+ end
413
+ end
414
+
415
+ def +(other)
416
+ cards = @hand.map { |card| Card.new(card) }
417
+ case other
418
+ when String
419
+ cards << Card.new(other)
420
+ when Card
421
+ cards << other
422
+ when PokerHand
423
+ cards += other.hand
424
+ else
425
+ raise ArgumentError, "Invalid argument: #{other.inspect}"
426
+ end
427
+ PokerHand.new(cards)
428
+ end
429
+
341
430
  private
342
-
431
+
343
432
  def check_for_duplicates
344
- if @hand.size != @hand.uniq.size && !@@allow_duplicates
433
+ if @hand.size != @hand.uniq.size && !allow_duplicates
345
434
  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."
346
435
  end
347
436
  end
348
-
437
+
349
438
  # if md is a string, arrange_hand will remove extra white space
350
439
  # if md is a MatchData, arrange_hand returns the matched segment
351
440
  # followed by the pre_match and the post_match
352
441
  def arrange_hand(md)
353
- hand = if (md.respond_to?(:to_str))
442
+ hand = if md.respond_to?(:to_str)
354
443
  md
355
444
  else
356
445
  md[0] + ' ' + md.pre_match + md.post_match
@@ -358,12 +447,23 @@ class PokerHand
358
447
  hand.strip.squeeze(" ") # remove extra whitespace
359
448
  end
360
449
 
361
- # delta transform creates a version of the cards where the delta
362
- # between card values is in the string, so a regexp can then match a
363
- # straight and/or straight flush
450
+ # delta transform returns a string representation of the cards where the
451
+ # delta between card values is in the string. This is necessary so a regexp
452
+ # can then match a straight and/or straight flush
453
+ #
454
+ # Examples
455
+ #
456
+ # PokerHand.new("As Qc Jh Ts 9d 8h")
457
+ # # => '0As 2Qc 1Jh 1Ts 19d 18h'
458
+ #
459
+ # PokerHand.new("Ah Qd Td 5d 4d")
460
+ # # => '0Ah 2Qd 2Td 55d 14d'
461
+ #
364
462
  def delta_transform(use_suit = false)
463
+ # In order to check for both ace high and ace low straights we create low
464
+ # ace duplicates of all of the high aces.
365
465
  aces = @hand.select { |c| c.face == Card::face_value('A') }
366
- aces.map! { |c| Card.new(1,c.suit) }
466
+ aces.map! { |c| Card.new(0, c.suit) } # hack to give the appearance of a low ace
367
467
 
368
468
  base = if (use_suit)
369
469
  (@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
@@ -371,6 +471,7 @@ class PokerHand
371
471
  (@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
372
472
  end
373
473
 
474
+ # Insert delta in front of each card
374
475
  result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
375
476
  if (prev_card)
376
477
  delta = prev_card - card.face
@@ -403,5 +504,5 @@ class PokerHand
403
504
  # careful to use gsub as gsub! can return nil here
404
505
  arranged_hand.gsub(/\s+$/, '')
405
506
  end
406
-
407
- end
507
+
508
+ end