ruby-poker 0.3.2 → 1.0.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.
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