ruby-poker 0.3.1 → 0.3.2

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 CHANGED
@@ -1,3 +1,9 @@
1
+ 2009-07-12 (0.3.2)
2
+ * Reorganized ruby-poker's lib folder to match the standard layout for gems. This makes ruby-poker compatible with Rip.
3
+ * Bug [#26276] improper two_pair? behavior. Applied patch by Uro.
4
+ * Changed protected methods in PokerHand to private
5
+ * Added natural_value method to Card
6
+
1
7
  2009-01-24 (0.3.1)
2
8
  * Bug [#23623] undefined method <=> for nil:NilClass
3
9
 
@@ -43,11 +49,3 @@
43
49
 
44
50
  2008-01-10 (0.1.0)
45
51
  * Initial version
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
data/README.rdoc CHANGED
@@ -1,8 +1,9 @@
1
1
  = Poker library in Ruby
2
+ ===
2
3
 
3
- == Author
4
-
5
- Rob Olson (rko618 [at] gmail)
4
+ Author:: {Rob Olson}[http://thinkingdigitally.com]
5
+ Email:: [first name] [at] thinkingdigitally.com
6
+ GitHub:: http://github.com/robolson/ruby-poker
6
7
 
7
8
  == Description
8
9
 
@@ -14,9 +15,7 @@ Card representations can be passed to the PokerHand constructor as a string or a
14
15
 
15
16
  sudo gem install ruby-poker
16
17
 
17
- == Examples
18
-
19
- In this section some examples show what can be done with this class.
18
+ == Example
20
19
 
21
20
  require 'rubygems'
22
21
  require 'ruby-poker'
@@ -40,11 +39,11 @@ Place that line near the beginning of your program. The change is program wide s
40
39
 
41
40
  == Compatibility
42
41
 
43
- Ruby-Poker is compatible with Ruby 1.8 and Ruby 1.9.
42
+ Ruby-Poker is compatible with Ruby 1.8.6 and Ruby 1.9.1.
44
43
 
45
44
  == History
46
45
 
47
- In the 0.2.0 release Patrick Hurley's Texas Holdem code from http://rubyquiz.com/quiz24.html was merged into ruby-poker.
46
+ In the 0.2.0 release Patrick Hurley's Texas Holdem code from http://www.rubyquiz.com/quiz24.html was merged into ruby-poker.
48
47
 
49
48
  == License
50
49
 
data/Rakefile CHANGED
@@ -1,58 +1,55 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'rake/rdoctask'
4
- require "rake/testtask"
5
- require 'rake/gempackagetask'
1
+ require 'rubygems'
2
+ require 'rake'
6
3
 
7
4
  begin
8
- require "rubygems"
5
+ require 'metric_fu'
9
6
  rescue LoadError
10
- nil
11
7
  end
12
8
 
13
- RUBYPOKER_VERSION = "0.3.1"
9
+ RUBYPOKER_VERSION = "0.3.2"
14
10
 
15
11
  spec = Gem::Specification.new do |s|
16
12
  s.name = "ruby-poker"
17
13
  s.version = RUBYPOKER_VERSION
18
- s.date = "2009-01-24"
14
+ s.date = "2009-07-27"
19
15
  s.rubyforge_project = "rubypoker"
20
16
  s.platform = Gem::Platform::RUBY
21
17
  s.summary = "Poker library in Ruby"
22
18
  s.description = "Ruby library for comparing poker hands and determining the winner."
23
19
  s.author = "Rob Olson"
24
- s.email = "rko618@gmail.com"
20
+ s.email = "rob@thinkingdigitally.com"
25
21
  s.homepage = "http://github.com/robolson/ruby-poker"
26
22
  s.has_rdoc = true
27
23
  s.files = ["CHANGELOG",
28
- "examples/deck.rb",
29
- "examples/quick_example.rb",
30
- "lib/card.rb",
31
- "lib/ruby-poker.rb",
32
- "LICENSE",
33
- "Rakefile",
34
- "README.rdoc",
35
- "ruby-poker.gemspec"]
36
- s.test_files = ["test/test_card.rb",
37
- "test/test_poker_hand.rb"]
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"]
38
34
  s.require_paths << 'lib'
39
-
35
+
40
36
  s.extra_rdoc_files = ["README.rdoc", "CHANGELOG", "LICENSE"]
41
37
  s.rdoc_options << '--title' << 'Ruby Poker Documentation' <<
42
38
  '--main' << 'README.rdoc' <<
43
39
  '--inline-source' << '-q'
44
-
45
- # s.add_dependency("thoughtbot-shoulda", ["> 2.0.0"])
40
+
41
+ s.add_development_dependency('thoughtbot-shoulda', '> 2.0.0')
46
42
  end
47
43
 
44
+ require 'rake/gempackagetask'
48
45
  Rake::GemPackageTask.new(spec) do |pkg|
49
46
  pkg.need_tar = true
50
47
  pkg.need_zip = true
51
48
  end
52
49
 
53
- Rake::TestTask.new do |test|
54
- test.libs << "test"
55
- test.test_files = Dir[ "test/test_*.rb" ]
50
+ require 'rake/testtask'
51
+ Rake::TestTask.new(:test) do |test|
52
+ test.libs << 'lib' << 'test'
56
53
  test.verbose = true
57
54
  test.warning = true
58
55
  end
@@ -62,10 +59,12 @@ task :autotest do
62
59
  ruby "-I lib -w /usr/bin/autotest"
63
60
  end
64
61
 
62
+ require 'rake/rdoctask'
65
63
  Rake::RDocTask.new(:docs) do |rdoc|
66
- rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG', 'LICENSE', 'lib/')
67
64
  rdoc.main = 'README.rdoc'
68
- rdoc.rdoc_dir = 'doc/html'
69
- rdoc.title = 'Ruby Poker Documentation'
70
- rdoc.options << '--inline-source'
71
- end
65
+ rdoc.rdoc_dir = 'rdoc'
66
+ rdoc.title = "Ruby Poker #{RUBYPOKER_VERSION}"
67
+ rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG', 'LICENSE', 'lib/**/*.rb')
68
+ end
69
+
70
+ task :default => :test
data/examples/deck.rb CHANGED
@@ -1,13 +1,5 @@
1
1
  # This is a sample Deck implementation.
2
2
  class Deck
3
- def shuffle
4
- deck_size = @cards.size
5
- (deck_size * 2).times do
6
- pos1, pos2 = rand(deck_size), rand(deck_size)
7
- @cards[pos1], @cards[pos2] = @cards[pos2], @cards[pos1]
8
- end
9
- end
10
-
11
3
  def initialize
12
4
  @cards = []
13
5
  Card::SUITS.each_byte do |suit|
@@ -16,12 +8,39 @@ class Deck
16
8
  @cards.push(Card.new(face.chr, suit.chr))
17
9
  end
18
10
  end
19
- shuffle()
11
+ shuffle
12
+ end
13
+
14
+ def shuffle
15
+ @cards = @cards.sort_by { rand }
16
+ return self
20
17
  end
21
18
 
19
+ # removes a single card from the top of the deck and returns it
20
+ # synonymous to poping off a stack
22
21
  def deal
23
22
  @cards.pop
24
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
25
44
 
26
45
  def empty?
27
46
  @cards.empty?
data/lib/ruby-poker.rb CHANGED
@@ -1,401 +1,3 @@
1
- require 'card.rb'
2
-
3
- class PokerHand
4
- include Comparable
5
- attr_reader :hand
6
-
7
- @@allow_duplicates = true # true by default
8
- def self.allow_duplicates; @@allow_duplicates; end
9
- def self.allow_duplicates=(v); @@allow_duplicates = v; end
10
-
11
- # Returns a new PokerHand object. Accepts the cards represented
12
- # in a string or an array
13
- #
14
- # PokerHand.new("3d 5c 8h Ks") # => #<PokerHand:0x5c673c ...
15
- # PokerHand.new(["3d", "5c", "8h", "Ks"]) # => #<PokerHand:0x5c2d6c ...
16
- def initialize(cards = [])
17
- if cards.is_a? Array
18
- @hand = 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
- elsif cards.respond_to?(:to_str)
26
- @hand = cards.scan(/\S{2,3}/).map { |str| Card.new(str) }
27
- else
28
- @hand = cards
29
- end
30
-
31
- check_for_duplicates if !@@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
- (md.pre_match + md.post_match).match(/(\S)/)
100
- [
101
- [8, Card::face_value(md[1]), Card::face_value($1)],
102
- arrange_hand(md)
103
- ]
104
- else
105
- false
106
- end
107
- end
108
-
109
- def full_house?
110
- if (md = (by_face =~ /(.). \1. \1. (.*)(.). \3./))
111
- arranged_hand = arrange_hand(md[0] + ' ' +
112
- md.pre_match + ' ' + md[2] + ' ' + md.post_match)
113
- [
114
- [7, Card::face_value(md[1]), Card::face_value(md[3])],
115
- arranged_hand
116
- ]
117
- elsif (md = (by_face =~ /((.). \2.) (.*)((.). \5. \5.)/))
118
- arranged_hand = arrange_hand(md[4] + ' ' + md[1] + ' ' +
119
- md.pre_match + ' ' + md[3] + ' ' + md.post_match)
120
- [
121
- [7, Card::face_value(md[5]), Card::face_value(md[2])],
122
- arranged_hand
123
- ]
124
- else
125
- false
126
- end
127
- end
128
-
129
- def flush?
130
- if (md = (by_suit =~ /(.)(.) (.)\2 (.)\2 (.)\2 (.)\2/))
131
- [
132
- [
133
- 6,
134
- Card::face_value(md[1]),
135
- *(md[3..6].map { |f| Card::face_value(f) })
136
- ],
137
- arrange_hand(md)
138
- ]
139
- else
140
- false
141
- end
142
- end
143
-
144
- def straight?
145
- result = false
146
- if hand.size >= 5
147
- transform = delta_transform
148
- # note we can have more than one delta 0 that we
149
- # need to shuffle to the back of the hand
150
- i = 0
151
- until transform.match(/^\S{3}( [1-9x]\S\S)+( 0\S\S)*$/) or i >= hand.size do
152
- # only do this once per card in the hand to avoid entering an
153
- # infinite loop if all of the cards in the hand are the same
154
- transform.gsub!(/(\s0\S\S)(.*)/, "\\2\\1") # moves the front card to the back of the string
155
- i += 1
156
- end
157
- if (md = (/.(.). 1.. 1.. 1.. 1../.match(transform)))
158
- high_card = Card::face_value(md[1])
159
- arranged_hand = fix_low_ace_display(md[0] + ' ' + md.pre_match + ' ' + md.post_match)
160
- result = [[5, high_card], arranged_hand]
161
- end
162
- end
163
- end
164
-
165
- def three_of_a_kind?
166
- if (md = (by_face =~ /(.). \1. \1./))
167
- # get kicker
168
- arranged_hand = arrange_hand(md)
169
- arranged_hand.match(/(?:\S\S ){3}(\S)\S (\S)/)
170
- [
171
- [
172
- 4,
173
- Card::face_value(md[1]),
174
- Card::face_value($1),
175
- Card::face_value($2)
176
- ],
177
- arranged_hand
178
- ]
179
- else
180
- false
181
- end
182
- end
183
-
184
- def two_pair?
185
- # \1 is the face value of the first pair
186
- # \2 is the card in between the first pair and the second pair
187
- # \3 is the face value of the second pair
188
- if (md = (by_face =~ /(.). \1.(.*) (.). \3./))
189
- # to get the kicker this does the following
190
- # md[0] is the regex matched above which includes the first pair and
191
- # the second pair but also some cards in the middle so we sub them out
192
- # then we add on the cards that came before the first pair, the cards that
193
- # we in between, and the cards that came after.
194
- arranged_hand = arrange_hand(md[0].sub(md[2], '') + ' ' +
195
- md.pre_match + ' ' + md[2] + ' ' + md.post_match)
196
- arranged_hand.match(/(?:\S\S ){4}(\S)/)
197
- [
198
- [
199
- 3,
200
- Card::face_value(md[1]), # face value of the first pair
201
- Card::face_value(md[3]), # face value of the second pair
202
- Card::face_value($1) # face value of the kicker
203
- ],
204
- arranged_hand
205
- ]
206
- else
207
- false
208
- end
209
- end
210
-
211
- def pair?
212
- if (md = (by_face =~ /(.). \1./))
213
- # get kicker
214
- arranged_hand = arrange_hand(md)
215
- arranged_hand.match(/(?:\S\S ){2}(\S)\S\s+(\S)\S\s+(\S)/)
216
- [
217
- [
218
- 2,
219
- Card::face_value(md[1]),
220
- Card::face_value($1),
221
- Card::face_value($2),
222
- Card::face_value($3)
223
- ],
224
- arranged_hand
225
- ]
226
- else
227
- false
228
- end
229
- end
230
-
231
- def highest_card?
232
- result = by_face
233
- [[1, *result.face_values[0..4]], result.hand.join(' ')]
234
- end
235
-
236
- OPS = [
237
- ['Royal Flush', :royal_flush? ],
238
- ['Straight Flush', :straight_flush? ],
239
- ['Four of a kind', :four_of_a_kind? ],
240
- ['Full house', :full_house? ],
241
- ['Flush', :flush? ],
242
- ['Straight', :straight? ],
243
- ['Three of a kind', :three_of_a_kind?],
244
- ['Two pair', :two_pair? ],
245
- ['Pair', :pair? ],
246
- ['Highest Card', :highest_card? ],
247
- ]
248
-
249
- # Returns the verbose hand rating
250
- #
251
- # PokerHand.new("4s 5h 6c 7d 8s").hand_rating # => "Straight"
252
- def hand_rating
253
- OPS.map { |op|
254
- (method(op[1]).call()) ? op[0] : false
255
- }.find { |v| v }
256
- end
257
-
258
- alias :rank :hand_rating
259
-
260
- def score
261
- OPS.map { |op|
262
- method(op[1]).call()
263
- }.find([0]) { |score| score }
264
- end
265
-
266
- # Returns a string of the hand arranged based on its rank. Usually this will be the
267
- # same as by_face but there are some cases where it makes a difference.
268
- #
269
- # ph = PokerHand.new("AS 3S 5S 2S 4S")
270
- # ph.sort_using_rank # => "5s 4s 3s 2s As"
271
- # ph.by_face.just_cards # => "As 5s 4s 3s 2s"
272
- def sort_using_rank
273
- score[1]
274
- end
275
-
276
- # Returns string with a listing of the cards in the hand followed by the hand's rank.
277
- #
278
- # h = PokerHand.new("8c 8s")
279
- # h.to_s # => "8c 8s (Pair)"
280
- def to_s
281
- just_cards + " (" + hand_rating + ")"
282
- end
283
-
284
- # Returns an array of `Card` objects that make up the `PokerHand`.
285
- def to_a
286
- @hand
287
- end
288
-
289
- alias :to_ary :to_a
290
-
291
- def <=> other_hand
292
- self.score[0].compact <=> other_hand.score[0].compact
293
- end
294
-
295
- # Add a card to the hand
296
- #
297
- # hand = PokerHand.new("5d")
298
- # hand << "6s" # => Add a six of spades to the hand by passing a string
299
- # hand << ["7h", "8d"] # => Add multiple cards to the hand using an array
300
- def << new_cards
301
- if new_cards.is_a?(Card) || new_cards.is_a?(String)
302
- new_cards = [new_cards]
303
- end
304
-
305
- new_cards.each do |nc|
306
- unless @@allow_duplicates
307
- 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}/
308
- end
309
-
310
- @hand << Card.new(nc)
311
- end
312
- end
313
-
314
- # Remove a card from the hand.
315
- #
316
- # hand = PokerHand.new("5d Jd")
317
- # hand.delete("Jd") # => #<Card:0x5d0674 @value=23, @face=10, @suit=1>
318
- # hand.just_cards # => "5d"
319
- def delete card
320
- @hand.delete(Card.new(card))
321
- end
322
-
323
- # Same concept as Array#uniq
324
- def uniq
325
- PokerHand.new(@hand.uniq)
326
- end
327
-
328
- # Resolving methods are just passed directly down to the @hand array
329
- RESOLVING_METHODS = [:size, :+, :-]
330
- RESOLVING_METHODS.each do |method|
331
- class_eval %{
332
- def #{method}(*args, &block)
333
- @hand.#{method}(*args, &block)
334
- end
335
- }
336
- end
337
-
338
- protected
339
-
340
- def check_for_duplicates
341
- if @hand.size != @hand.uniq.size && !@@allow_duplicates
342
- raise "You are attempting to create a hand that contains duplicate cards. Set PokerHand.allow_duplicates to true if you do not want to ignore this error."
343
- end
344
- end
345
-
346
- # if md is a string, arrange_hand will remove extra white space
347
- # if md is a MatchData, arrange_hand returns the matched segment
348
- # followed by the pre_match and the post_match
349
- def arrange_hand(md)
350
- hand = if (md.respond_to?(:to_str))
351
- md
352
- else
353
- md[0] + ' ' + md.pre_match + md.post_match
354
- end
355
- hand.strip.squeeze(" ") # remove extra whitespace
356
- end
357
-
358
- def delta_transform(use_suit = false)
359
- aces = @hand.select { |c| c.face == Card::face_value('A') }
360
- aces.map! { |c| Card.new(1,c.suit) }
361
-
362
- base = if (use_suit)
363
- (@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
364
- else
365
- (@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
366
- end
367
-
368
- result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
369
- if (prev_card)
370
- delta = prev_card - card.face
371
- else
372
- delta = 0
373
- end
374
- # does not really matter for my needs
375
- delta = 'x' if (delta > 9 || delta < 0)
376
- delta_hand += delta.to_s + card.to_s + ' '
377
- [delta_hand, card.face]
378
- end
379
-
380
- # we just want the delta transform, not the last cards face too
381
- result[0].chop
382
- end
383
-
384
- def fix_low_ace_display(arranged_hand)
385
- # remove card deltas (this routine is only used for straights)
386
- arranged_hand.gsub!(/\S(\S\S)\s*/, "\\1 ")
387
-
388
- # Fix "low aces"
389
- arranged_hand.gsub!(/L(\S)/, "A\\1")
390
-
391
- # Remove duplicate aces (this will not work if you have
392
- # multiple decks or wild cards)
393
- arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
394
-
395
- # cleanup white space
396
- arranged_hand.gsub!(/\s+/, ' ')
397
- # careful to use gsub as gsub! can return nil here
398
- arranged_hand.gsub(/\s+$/, '')
399
- end
400
-
401
- end
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) # For use/testing when no gem is installed
2
+ require 'ruby-poker/card'
3
+ require 'ruby-poker/poker_hand'
@@ -5,11 +5,7 @@ class Card
5
5
  'c' => 0,
6
6
  'd' => 1,
7
7
  'h' => 2,
8
- 's' => 3,
9
- 'C' => 0,
10
- 'D' => 1,
11
- 'H' => 2,
12
- 'S' => 3,
8
+ 's' => 3
13
9
  }
14
10
  FACE_VALUES = {
15
11
  'L' => 1, # this is a magic low ace
@@ -25,23 +21,20 @@ class Card
25
21
  'J' => 11,
26
22
  'Q' => 12,
27
23
  'K' => 13,
28
- 'A' => 14,
24
+ 'A' => 14
29
25
  }
30
26
 
31
27
  def Card.face_value(face)
32
- if (face)
33
- FACE_VALUES[face.upcase] - 1
34
- else
28
+ face.upcase!
29
+ if face == 'L' || !FACE_VALUES.has_key?(face)
35
30
  nil
31
+ else
32
+ FACE_VALUES[face] - 1
36
33
  end
37
34
  end
38
35
 
39
- protected
36
+ private
40
37
 
41
- def build_from_string(card)
42
- build_from_face_suit(card[0,1], card[1,1])
43
- end
44
-
45
38
  def build_from_value(value)
46
39
  @value = value
47
40
  @suit = value / FACES.size()
@@ -49,6 +42,7 @@ class Card
49
42
  end
50
43
 
51
44
  def build_from_face_suit(face, suit)
45
+ suit.downcase!
52
46
  @face = Card::face_value(face)
53
47
  @suit = SUIT_LOOKUP[suit]
54
48
  @value = (@suit * FACES.size()) + (@face - 1)
@@ -58,6 +52,10 @@ class Card
58
52
  build_from_value((face - 1) + (suit * FACES.size()))
59
53
  end
60
54
 
55
+ def build_from_string(card)
56
+ build_from_face_suit(card[0,1], card[1,1])
57
+ end
58
+
61
59
  # Constructs this card object from another card object
62
60
  def build_from_card(card)
63
61
  @value = card.value
@@ -122,4 +120,20 @@ class Card
122
120
  def hash
123
121
  @value.hash
124
122
  end
123
+
124
+ # A card's natural value is the closer to it's intuitive value in a deck
125
+ # in the range of 1 to 52. Aces are low with a value of 1. Uses the bridge
126
+ # order of suits: clubs, diamonds, hearts, and spades. The formula used is:
127
+ # If the suit is clubs, the natural value is the face value (remember
128
+ # Aces are low). If the suit is diamonds, it is the clubs value plus 13.
129
+ # If the suit is hearts, it is plus 26. If it is spades, it is plus 39.
130
+ #
131
+ # Card.new("Ac").natural_value # => 1
132
+ # Card.new("Kc").natural_value # => 12
133
+ # Card.new("Ad").natural_value # => 13
134
+ def natural_value
135
+ natural_face = @face == 13 ? 1 : @face+1 # flip Ace from 13 to 1 and
136
+ # increment everything else by 1
137
+ natural_face + @suit * 13
138
+ end
125
139
  end
@@ -0,0 +1,407 @@
1
+ class PokerHand
2
+ include Comparable
3
+ attr_reader :hand
4
+
5
+ @@allow_duplicates = true # true by default
6
+ def self.allow_duplicates; @@allow_duplicates; end
7
+ def self.allow_duplicates=(v); @@allow_duplicates = v; end
8
+
9
+ # Returns a new PokerHand object. Accepts the cards represented
10
+ # in a string or an array
11
+ #
12
+ # PokerHand.new("3d 5c 8h Ks") # => #<PokerHand:0x5c673c ...
13
+ # PokerHand.new(["3d", "5c", "8h", "Ks"]) # => #<PokerHand:0x5c2d6c ...
14
+ def initialize(cards = [])
15
+ if cards.is_a? Array
16
+ @hand = cards.map do |card|
17
+ if card.is_a? Card
18
+ card
19
+ else
20
+ Card.new(card.to_s)
21
+ end
22
+ end
23
+ elsif cards.respond_to?(:to_str)
24
+ @hand = cards.scan(/\S{2,3}/).map { |str| Card.new(str) }
25
+ else
26
+ @hand = cards
27
+ end
28
+
29
+ check_for_duplicates if !@@allow_duplicates
30
+ end
31
+
32
+ # Returns a new PokerHand object with the cards sorted by suit
33
+ # The suit order is spades, hearts, diamonds, clubs
34
+ #
35
+ # PokerHand.new("3d 5c 8h Ks").by_suit.just_cards # => "Ks 8h 3d 5c"
36
+ def by_suit
37
+ PokerHand.new(@hand.sort_by { |c| [c.suit, c.face] }.reverse)
38
+ end
39
+
40
+ # Returns a new PokerHand object with the cards sorted by value
41
+ # with the highest value first.
42
+ #
43
+ # PokerHand.new("3d 5c 8h Ks").by_face.just_cards # => "Ks 8h 5c 3d"
44
+ def by_face
45
+ PokerHand.new(@hand.sort_by { |c| [c.face, c.suit] }.reverse)
46
+ end
47
+
48
+ # Returns string representation of the hand without the rank
49
+ #
50
+ # PokerHand.new(["3c", "Kh"]).just_cards # => "3c Kh"
51
+ def just_cards
52
+ @hand.join(" ")
53
+ end
54
+ alias :cards :just_cards
55
+
56
+ # Returns an array of the card values in the hand.
57
+ # The values returned are 1 less than the value on the card.
58
+ # For example: 2's will be shown as 1.
59
+ #
60
+ # PokerHand.new(["3c", "Kh"]).face_values # => [2, 12]
61
+ def face_values
62
+ @hand.map { |c| c.face }
63
+ end
64
+
65
+ # The =~ method does a regular expression match on the cards in this hand.
66
+ # This can be useful for many purposes. A common use is the check if a card
67
+ # exists in a hand.
68
+ #
69
+ # PokerHand.new("3d 4d 5d") =~ /8h/ # => nil
70
+ # PokerHand.new("3d 4d 5d") =~ /4d/ # => #<MatchData:0x615e18>
71
+ def =~ (re)
72
+ re.match(just_cards)
73
+ end
74
+
75
+ def royal_flush?
76
+ if (md = (by_suit =~ /A(.) K\1 Q\1 J\1 T\1/))
77
+ [[10], arrange_hand(md)]
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ def straight_flush?
84
+ if (md = (/.(.)(.)(?: 1.\2){4}/.match(delta_transform(true))))
85
+ high_card = Card::face_value(md[1])
86
+ arranged_hand = fix_low_ace_display(md[0] + ' ' +
87
+ md.pre_match + ' ' + md.post_match)
88
+ [[9, high_card], arranged_hand]
89
+ else
90
+ false
91
+ end
92
+ end
93
+
94
+ def four_of_a_kind?
95
+ if (md = (by_face =~ /(.). \1. \1. \1./))
96
+ # 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
104
+ end
105
+ end
106
+
107
+ def full_house?
108
+ if (md = (by_face =~ /(.). \1. \1. (.*)(.). \3./))
109
+ arranged_hand = arrange_hand(md[0] + ' ' +
110
+ md.pre_match + ' ' + md[2] + ' ' + md.post_match)
111
+ [
112
+ [7, Card::face_value(md[1]), Card::face_value(md[3])],
113
+ arranged_hand
114
+ ]
115
+ elsif (md = (by_face =~ /((.). \2.) (.*)((.). \5. \5.)/))
116
+ arranged_hand = arrange_hand(md[4] + ' ' + md[1] + ' ' +
117
+ md.pre_match + ' ' + md[3] + ' ' + md.post_match)
118
+ [
119
+ [7, Card::face_value(md[5]), Card::face_value(md[2])],
120
+ arranged_hand
121
+ ]
122
+ else
123
+ false
124
+ end
125
+ end
126
+
127
+ def flush?
128
+ if (md = (by_suit =~ /(.)(.) (.)\2 (.)\2 (.)\2 (.)\2/))
129
+ [
130
+ [
131
+ 6,
132
+ Card::face_value(md[1]),
133
+ *(md[3..6].map { |f| Card::face_value(f) })
134
+ ],
135
+ arrange_hand(md)
136
+ ]
137
+ else
138
+ false
139
+ end
140
+ end
141
+
142
+ def straight?
143
+ result = false
144
+ if hand.size >= 5
145
+ transform = delta_transform
146
+ # note we can have more than one delta 0 that we
147
+ # need to shuffle to the back of the hand
148
+ i = 0
149
+ until transform.match(/^\S{3}( [1-9x]\S\S)+( 0\S\S)*$/) or i >= hand.size do
150
+ # only do this once per card in the hand to avoid entering an
151
+ # infinite loop if all of the cards in the hand are the same
152
+ transform.gsub!(/(\s0\S\S)(.*)/, "\\2\\1") # moves the front card to the back of the string
153
+ i += 1
154
+ end
155
+ if (md = (/.(.). 1.. 1.. 1.. 1../.match(transform)))
156
+ high_card = Card::face_value(md[1])
157
+ arranged_hand = fix_low_ace_display(md[0] + ' ' + md.pre_match + ' ' + md.post_match)
158
+ result = [[5, high_card], arranged_hand]
159
+ end
160
+ end
161
+ end
162
+
163
+ def three_of_a_kind?
164
+ if (md = (by_face =~ /(.). \1. \1./))
165
+ # get kicker
166
+ 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
179
+ end
180
+ end
181
+
182
+ def two_pair?
183
+ # \1 is the face value of the first pair
184
+ # \2 is the card in between the first pair and the second pair
185
+ # \3 is the face value of the second pair
186
+ if (md = (by_face =~ /(.). \1.(.*?) (.). \3./))
187
+ # to get the kicker this does the following
188
+ # md[0] is the regex matched above which includes the first pair and
189
+ # the second pair but also some cards in the middle so we sub them out
190
+ # then we add on the cards that came before the first pair, the cards
191
+ # that were in-between, and the cards that came after.
192
+ arranged_hand = arrange_hand(md[0].sub(md[2], '') + ' ' +
193
+ 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
206
+ end
207
+ end
208
+
209
+ def pair?
210
+ 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
+ ]
224
+ else
225
+ false
226
+ end
227
+ end
228
+
229
+ def highest_card?
230
+ result = by_face
231
+ [[1, *result.face_values[0..4]], result.hand.join(' ')]
232
+ end
233
+
234
+ OPS = [
235
+ ['Royal Flush', :royal_flush? ],
236
+ ['Straight Flush', :straight_flush? ],
237
+ ['Four of a kind', :four_of_a_kind? ],
238
+ ['Full house', :full_house? ],
239
+ ['Flush', :flush? ],
240
+ ['Straight', :straight? ],
241
+ ['Three of a kind', :three_of_a_kind?],
242
+ ['Two pair', :two_pair? ],
243
+ ['Pair', :pair? ],
244
+ ['Highest Card', :highest_card? ],
245
+ ]
246
+
247
+ # Returns the verbose hand rating
248
+ #
249
+ # PokerHand.new("4s 5h 6c 7d 8s").hand_rating # => "Straight"
250
+ def hand_rating
251
+ OPS.map { |op|
252
+ (method(op[1]).call()) ? op[0] : false
253
+ }.find { |v| v }
254
+ end
255
+
256
+ alias :rank :hand_rating
257
+
258
+ 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
261
+ # 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
+ OPS.map { |op|
265
+ method(op[1]).call()
266
+ }.find([0]) { |score| score }
267
+ end
268
+
269
+ # Returns a string of the hand arranged based on its rank. Usually this will be the
270
+ # same as by_face but there are some cases where it makes a difference.
271
+ #
272
+ # ph = PokerHand.new("As 3s 5s 2s 4s")
273
+ # ph.sort_using_rank # => "5s 4s 3s 2s As"
274
+ # ph.by_face.just_cards # => "As 5s 4s 3s 2s"
275
+ def sort_using_rank
276
+ score[1]
277
+ end
278
+
279
+ # Returns string with a listing of the cards in the hand followed by the hand's rank.
280
+ #
281
+ # h = PokerHand.new("8c 8s")
282
+ # h.to_s # => "8c 8s (Pair)"
283
+ def to_s
284
+ just_cards + " (" + hand_rating + ")"
285
+ end
286
+
287
+ # Returns an array of `Card` objects that make up the `PokerHand`.
288
+ def to_a
289
+ @hand
290
+ end
291
+
292
+ alias :to_ary :to_a
293
+
294
+ def <=> other_hand
295
+ self.score[0].compact <=> other_hand.score[0].compact
296
+ end
297
+
298
+ # Add a card to the hand
299
+ #
300
+ # hand = PokerHand.new("5d")
301
+ # hand << "6s" # => Add a six of spades to the hand by passing a string
302
+ # hand << ["7h", "8d"] # => Add multiple cards to the hand using an array
303
+ def << new_cards
304
+ if new_cards.is_a?(Card) || new_cards.is_a?(String)
305
+ new_cards = [new_cards]
306
+ end
307
+
308
+ new_cards.each do |nc|
309
+ unless @@allow_duplicates
310
+ 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
+ end
312
+
313
+ @hand << Card.new(nc)
314
+ end
315
+ end
316
+
317
+ # Remove a card from the hand.
318
+ #
319
+ # hand = PokerHand.new("5d Jd")
320
+ # hand.delete("Jd") # => #<Card:0x5d0674 @value=23, @face=10, @suit=1>
321
+ # hand.just_cards # => "5d"
322
+ def delete card
323
+ @hand.delete(Card.new(card))
324
+ end
325
+
326
+ # Same concept as Array#uniq
327
+ def uniq
328
+ PokerHand.new(@hand.uniq)
329
+ end
330
+
331
+ # Resolving methods are just passed directly down to the @hand array
332
+ RESOLVING_METHODS = [:size, :+, :-]
333
+ RESOLVING_METHODS.each do |method|
334
+ class_eval %{
335
+ def #{method}(*args, &block)
336
+ @hand.#{method}(*args, &block)
337
+ end
338
+ }
339
+ end
340
+
341
+ private
342
+
343
+ def check_for_duplicates
344
+ if @hand.size != @hand.uniq.size && !@@allow_duplicates
345
+ 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
+ end
347
+ end
348
+
349
+ # if md is a string, arrange_hand will remove extra white space
350
+ # if md is a MatchData, arrange_hand returns the matched segment
351
+ # followed by the pre_match and the post_match
352
+ def arrange_hand(md)
353
+ hand = if (md.respond_to?(:to_str))
354
+ md
355
+ else
356
+ md[0] + ' ' + md.pre_match + md.post_match
357
+ end
358
+ hand.strip.squeeze(" ") # remove extra whitespace
359
+ end
360
+
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
364
+ def delta_transform(use_suit = false)
365
+ aces = @hand.select { |c| c.face == Card::face_value('A') }
366
+ aces.map! { |c| Card.new(1,c.suit) }
367
+
368
+ base = if (use_suit)
369
+ (@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
370
+ else
371
+ (@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
372
+ end
373
+
374
+ result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
375
+ if (prev_card)
376
+ delta = prev_card - card.face
377
+ else
378
+ delta = 0
379
+ end
380
+ # does not really matter for my needs
381
+ delta = 'x' if (delta > 9 || delta < 0)
382
+ delta_hand += delta.to_s + card.to_s + ' '
383
+ [delta_hand, card.face]
384
+ end
385
+
386
+ # we just want the delta transform, not the last cards face too
387
+ result[0].chop
388
+ end
389
+
390
+ def fix_low_ace_display(arranged_hand)
391
+ # remove card deltas (this routine is only used for straights)
392
+ arranged_hand.gsub!(/\S(\S\S)\s*/, "\\1 ")
393
+
394
+ # Fix "low aces"
395
+ arranged_hand.gsub!(/L(\S)/, "A\\1")
396
+
397
+ # Remove duplicate aces (this will not work if you have
398
+ # multiple decks or wild cards)
399
+ arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
400
+
401
+ # cleanup white space
402
+ arranged_hand.gsub!(/\s+/, ' ')
403
+ # careful to use gsub as gsub! can return nil here
404
+ arranged_hand.gsub(/\s+$/, '')
405
+ end
406
+
407
+ end
data/test/test_card.rb CHANGED
@@ -1,5 +1,4 @@
1
- require 'test/unit'
2
- require 'card.rb'
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
3
2
 
4
3
  class TestCard < Test::Unit::TestCase
5
4
  def setup
@@ -11,16 +10,21 @@ class TestCard < Test::Unit::TestCase
11
10
  end
12
11
 
13
12
  def test_build_from_card
14
- c1 = Card.new("2c")
15
- c2 = Card.new(c1)
16
- assert_equal("2c", c2.to_s)
13
+ assert_equal("9c", Card.new(@c1).to_s)
17
14
  end
18
15
 
19
16
  def test_class_face_value
20
- assert_equal(0, Card.face_value('L'))
17
+ assert_nil(Card.face_value('L'))
21
18
  assert_equal(13, Card.face_value('A'))
22
19
  end
23
20
 
21
+ def test_build_from_value
22
+ assert_equal(@c1, Card.new(7))
23
+ assert_equal(@c2, Card.new(22))
24
+ assert_equal(@c3, Card.new(37))
25
+ assert_equal(@c4, Card.new(52))
26
+ end
27
+
24
28
  def test_face
25
29
  assert_equal(8, @c1.face)
26
30
  assert_equal(9, @c2.face)
@@ -42,6 +46,12 @@ class TestCard < Test::Unit::TestCase
42
46
  assert_equal(52, @c4.value)
43
47
  end
44
48
 
49
+ def test_natural_value
50
+ assert_equal(1, Card.new("AC").natural_value)
51
+ assert_equal(15, Card.new("2D").natural_value)
52
+ assert_equal(52, Card.new("KS").natural_value)
53
+ end
54
+
45
55
  def test_comparison
46
56
  assert(@c1 < @c2)
47
57
  assert(@c3 > @c2)
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'ruby-poker'
@@ -1,6 +1,4 @@
1
- require 'ruby-poker.rb'
2
- require 'rubygems'
3
- require 'shoulda'
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
4
2
 
5
3
  class TestPokerHand < Test::Unit::TestCase
6
4
  context "A PokerHand instance" do
@@ -114,6 +112,8 @@ class TestPokerHand < Test::Unit::TestCase
114
112
  end
115
113
 
116
114
  should "return the correct number of cards in the hand" do
115
+ assert_equal(0, PokerHand.new.size)
116
+ assert_equal(1, PokerHand.new("2c").size)
117
117
  assert_equal(2, PokerHand.new("2c 3d").size)
118
118
  end
119
119
 
@@ -145,6 +145,16 @@ class TestPokerHand < Test::Unit::TestCase
145
145
  ph.delete("Ac")
146
146
  assert_equal(Array.new, ph.hand)
147
147
  end
148
+
149
+ should "detect the two highest pairs when there are more than two" do
150
+ ph = PokerHand.new("7d 7s 4d 4c 2h 2d")
151
+ assert_equal([3, 6, 3, 1], ph.two_pair?[0])
152
+ # Explanation of [3, 6, 3, 1]
153
+ # 3: the number for a two pair
154
+ # 6: highest pair is two 7's
155
+ # 3: second highest pair is two 4's
156
+ # 1: kicker is a 2
157
+ end
148
158
 
149
159
  context "when duplicates are allowed" do
150
160
  setup do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-poker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Olson
@@ -9,12 +9,21 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-01-24 00:00:00 -08:00
12
+ date: 2009-07-27 00:00:00 -07:00
13
13
  default_executable:
14
- dependencies: []
15
-
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thoughtbot-shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 2.0.0
24
+ version:
16
25
  description: Ruby library for comparing poker hands and determining the winner.
17
- email: rko618@gmail.com
26
+ email: rob@thinkingdigitally.com
18
27
  executables: []
19
28
 
20
29
  extensions: []
@@ -27,14 +36,17 @@ files:
27
36
  - CHANGELOG
28
37
  - examples/deck.rb
29
38
  - examples/quick_example.rb
30
- - lib/card.rb
31
39
  - lib/ruby-poker.rb
40
+ - lib/ruby-poker/card.rb
41
+ - lib/ruby-poker/poker_hand.rb
32
42
  - LICENSE
33
43
  - Rakefile
34
44
  - README.rdoc
35
45
  - ruby-poker.gemspec
36
46
  has_rdoc: true
37
47
  homepage: http://github.com/robolson/ruby-poker
48
+ licenses: []
49
+
38
50
  post_install_message:
39
51
  rdoc_options:
40
52
  - --title
@@ -61,10 +73,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
73
  requirements: []
62
74
 
63
75
  rubyforge_project: rubypoker
64
- rubygems_version: 1.3.1
76
+ rubygems_version: 1.3.2
65
77
  signing_key:
66
- specification_version: 2
78
+ specification_version: 3
67
79
  summary: Poker library in Ruby
68
80
  test_files:
81
+ - test/test_helper.rb
69
82
  - test/test_card.rb
70
83
  - test/test_poker_hand.rb