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 +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
|