ruby-poker 0.3.1 → 0.3.2

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