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 +6 -8
- data/README.rdoc +7 -8
- data/Rakefile +30 -31
- data/examples/deck.rb +28 -9
- data/lib/ruby-poker.rb +3 -401
- data/lib/{card.rb → ruby-poker/card.rb} +28 -14
- data/lib/ruby-poker/poker_hand.rb +407 -0
- data/test/test_card.rb +16 -6
- data/test/test_helper.rb +7 -0
- data/test/test_poker_hand.rb +13 -3
- metadata +21 -8
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
|
-
|
4
|
-
|
5
|
-
|
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
|
-
==
|
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
|
-
|
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
|
5
|
+
require 'metric_fu'
|
9
6
|
rescue LoadError
|
10
|
-
nil
|
11
7
|
end
|
12
8
|
|
13
|
-
RUBYPOKER_VERSION = "0.3.
|
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-
|
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 = "
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
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
|
-
|
54
|
-
|
55
|
-
test.
|
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 = '
|
69
|
-
rdoc.title =
|
70
|
-
rdoc.
|
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
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
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 '
|
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
|
-
|
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
|
-
|
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)
|
data/test/test_helper.rb
ADDED
data/test/test_poker_hand.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
require
|
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.
|
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-
|
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:
|
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.
|
76
|
+
rubygems_version: 1.3.2
|
65
77
|
signing_key:
|
66
|
-
specification_version:
|
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
|