bhousel-ruby-poker 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +54 -0
- data/LICENSE.txt +20 -0
- data/README.md +50 -0
- data/Rakefile +11 -0
- data/examples/deck.rb +48 -0
- data/examples/quick_example.rb +11 -0
- data/lib/ruby-poker.rb +2 -0
- data/lib/ruby-poker/card.rb +142 -0
- data/lib/ruby-poker/poker_hand.rb +487 -0
- data/ruby-poker.gemspec +29 -0
- data/test/test_card.rb +72 -0
- data/test/test_helper.rb +7 -0
- data/test/test_poker_hand.rb +523 -0
- metadata +78 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
2012-10-30 (0.4.0)
|
2
|
+
* Forked from git://github.com/robolson/ruby-poker.git
|
3
|
+
|
4
|
+
2009-07-12 (0.3.2)
|
5
|
+
* Reorganized ruby-poker's lib folder to match the standard layout for gems. This makes ruby-poker compatible with Rip.
|
6
|
+
* Bug [#26276] improper two_pair? behavior. Applied patch by Uro.
|
7
|
+
* Changed protected methods in PokerHand to private
|
8
|
+
* Added natural_value method to Card
|
9
|
+
|
10
|
+
2009-01-24 (0.3.1)
|
11
|
+
* Bug [#23623] undefined method <=> for nil:NilClass
|
12
|
+
|
13
|
+
2008-12-30 (0.3.1)
|
14
|
+
* Bug (#20407) Raise an exception when creating a new hand with duplicates
|
15
|
+
* Added PokerHand#uniq method
|
16
|
+
* Removed deprecated `Gem::manage_gems` from Rakefile
|
17
|
+
|
18
|
+
2008-05-17 (0.3.0)
|
19
|
+
* Changed Card#== to compare based on card suit and face value. Before it only compared the face value of two cards. Warning: This change may potentially break your program if you were comparing Card objects directly.
|
20
|
+
* Replaced `PokerHand#arranged_hand` with `PokerHand#sort_using_rank` which is more descriptive. This loosely corresponds to bug #20194.
|
21
|
+
* Bug [#20196] 'rank' goes into an infinite loop.
|
22
|
+
* Bug [#20195] Allows the same card to be entered into the hand.
|
23
|
+
* Bug [#20344] sort_using_rank does not return expected results
|
24
|
+
|
25
|
+
2008-04-20 (0.2.4)
|
26
|
+
* Modernized the Rakefile
|
27
|
+
* Updated to be compatible with Ruby 1.9
|
28
|
+
|
29
|
+
2008-04-06 (0.2.2)
|
30
|
+
* Fixed bug where two hands that had the same values but different suits returned not equal
|
31
|
+
|
32
|
+
2008-02-08 (0.2.1)
|
33
|
+
* Cards can be added to a hand after it is created by using (<<) on a PokerHand
|
34
|
+
* Cards can be deleted from a hand with PokerHand.delete()
|
35
|
+
|
36
|
+
2008-01-21 (0.2.0)
|
37
|
+
* Merged Patrick Hurley's poker solver
|
38
|
+
* Added support for hands with >5 cards
|
39
|
+
* Straights with a low Ace count now
|
40
|
+
* to_s on a PokerHand now includes the rank after the card list
|
41
|
+
* Finally wrote the Unit Tests suite
|
42
|
+
|
43
|
+
2008-01-12 (0.1.2)
|
44
|
+
* Fixed critical bug that was stopping the whole program to not work
|
45
|
+
* Added some test cases as a result
|
46
|
+
* More test cases coming soon
|
47
|
+
|
48
|
+
2008-01-12 (0.1.1)
|
49
|
+
* Ranks are now a class.
|
50
|
+
* Extracted card, rank, and arrays methods to individual files
|
51
|
+
* Added gem packaging
|
52
|
+
|
53
|
+
2008-01-10 (0.1.0)
|
54
|
+
* Initial version
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (C) 2012 Bryan Housel
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Poker library in Ruby
|
2
|
+
|
3
|
+
## Forked From
|
4
|
+
|
5
|
+
git://github.com/robolson/ruby-poker.git
|
6
|
+
|
7
|
+
## Description
|
8
|
+
|
9
|
+
Ruby-Poker handles the logic for getting the rank of a poker hand. It can also
|
10
|
+
be used to compare two or more hands to determine which hand has the highest
|
11
|
+
poker value.
|
12
|
+
|
13
|
+
Card representations can be passed to the PokerHand constructor as a string or
|
14
|
+
an array. Face cards (cards ten, jack, queen, king, and ace) are created using
|
15
|
+
their letter representation (T, J, Q, K, A).
|
16
|
+
|
17
|
+
## Install
|
18
|
+
|
19
|
+
gem install ruby-poker
|
20
|
+
|
21
|
+
## Example
|
22
|
+
|
23
|
+
require 'rubygems'
|
24
|
+
require 'ruby-poker'
|
25
|
+
|
26
|
+
hand1 = PokerHand.new("8H 9C TC JD QH")
|
27
|
+
hand2 = PokerHand.new(["3D", "3C", "3S", "KD", "AH"])
|
28
|
+
puts hand1 => 8h 9c Tc Jd Qh (Straight)
|
29
|
+
puts hand1.just_cards => 8h 9c Tc Jd Qh
|
30
|
+
puts hand1.rank => Straight
|
31
|
+
puts hand2 => 3d 3c 3s Kd Ah (Three of a kind)
|
32
|
+
puts hand2.rank => Three of a kind
|
33
|
+
puts hand1 > hand2 => true
|
34
|
+
|
35
|
+
## Duplicates
|
36
|
+
|
37
|
+
By default ruby-poker will not raise an exception if you add the same card to
|
38
|
+
a hand twice. You can tell ruby-poker to not allow duplicates by doing the
|
39
|
+
following
|
40
|
+
|
41
|
+
PokerHand.allow_duplicates = false
|
42
|
+
|
43
|
+
Place that line near the beginning of your program. The change is program wide
|
44
|
+
so once allow_duplicates is set to false, _all_ poker hands will raise an
|
45
|
+
exception if a duplicate card is added to the hand.
|
46
|
+
|
47
|
+
## Compatibility
|
48
|
+
|
49
|
+
Ruby-Poker is compatible with Ruby 1.8 and Ruby 1.9
|
50
|
+
|
data/Rakefile
ADDED
data/examples/deck.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# This is a sample Deck implementation.
|
2
|
+
class Deck
|
3
|
+
def initialize
|
4
|
+
@cards = []
|
5
|
+
Card::SUITS.each_byte do |suit|
|
6
|
+
# careful not to double include the aces...
|
7
|
+
Card::FACES[1..-1].each_byte do |face|
|
8
|
+
@cards.push(Card.new(face.chr, suit.chr))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
shuffle
|
12
|
+
end
|
13
|
+
|
14
|
+
def shuffle
|
15
|
+
@cards = @cards.sort_by { rand }
|
16
|
+
return self
|
17
|
+
end
|
18
|
+
|
19
|
+
# removes a single card from the top of the deck and returns it
|
20
|
+
# synonymous to poping off a stack
|
21
|
+
def deal
|
22
|
+
@cards.pop
|
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
|
44
|
+
|
45
|
+
def empty?
|
46
|
+
@cards.empty?
|
47
|
+
end
|
48
|
+
end
|
data/lib/ruby-poker.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
class Card
|
2
|
+
SUITS = "cdhs"
|
3
|
+
FACES = "L23456789TJQKA"
|
4
|
+
SUIT_LOOKUP = {
|
5
|
+
'c' => 0,
|
6
|
+
'd' => 1,
|
7
|
+
'h' => 2,
|
8
|
+
's' => 3
|
9
|
+
}
|
10
|
+
FACE_VALUES = {
|
11
|
+
'L' => 1, # this is a magic low ace
|
12
|
+
'2' => 2,
|
13
|
+
'3' => 3,
|
14
|
+
'4' => 4,
|
15
|
+
'5' => 5,
|
16
|
+
'6' => 6,
|
17
|
+
'7' => 7,
|
18
|
+
'8' => 8,
|
19
|
+
'9' => 9,
|
20
|
+
'T' => 10,
|
21
|
+
'J' => 11,
|
22
|
+
'Q' => 12,
|
23
|
+
'K' => 13,
|
24
|
+
'A' => 14
|
25
|
+
}
|
26
|
+
|
27
|
+
def Card.face_value(face)
|
28
|
+
face.upcase!
|
29
|
+
if face == 'L' || !FACE_VALUES.has_key?(face)
|
30
|
+
nil
|
31
|
+
else
|
32
|
+
FACE_VALUES[face] - 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def build_from_value(value)
|
39
|
+
@value = value
|
40
|
+
@suit = value / FACES.size()
|
41
|
+
@face = (value % FACES.size())
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_from_face_suit(face, suit)
|
45
|
+
suit.downcase!
|
46
|
+
@face = Card::face_value(face)
|
47
|
+
@suit = SUIT_LOOKUP[suit]
|
48
|
+
raise ArgumentError, "Invalid card: \"#{face}#{suit}\"" unless @face and @suit
|
49
|
+
@value = (@suit * FACES.size()) + (@face - 1)
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_from_face_suit_values(face, suit)
|
53
|
+
build_from_value((face - 1) + (suit * FACES.size()))
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_from_string(card)
|
57
|
+
build_from_face_suit(card[0,1], card[1,1])
|
58
|
+
end
|
59
|
+
|
60
|
+
# Constructs this card object from another card object
|
61
|
+
def build_from_card(card)
|
62
|
+
@value = card.value
|
63
|
+
@suit = card.suit
|
64
|
+
@face = card.face
|
65
|
+
end
|
66
|
+
|
67
|
+
public
|
68
|
+
|
69
|
+
def initialize(*args)
|
70
|
+
if (args.size == 1)
|
71
|
+
value = args.first
|
72
|
+
if (value.respond_to?(:to_card))
|
73
|
+
build_from_card(value)
|
74
|
+
elsif (value.respond_to?(:to_str))
|
75
|
+
build_from_string(value)
|
76
|
+
elsif (value.respond_to?(:to_int))
|
77
|
+
build_from_value(value)
|
78
|
+
end
|
79
|
+
elsif (args.size == 2)
|
80
|
+
arg1, arg2 = args
|
81
|
+
if (arg1.respond_to?(:to_str) &&
|
82
|
+
arg2.respond_to?(:to_str))
|
83
|
+
build_from_face_suit(arg1, arg2)
|
84
|
+
elsif (arg1.respond_to?(:to_int) &&
|
85
|
+
arg2.respond_to?(:to_int))
|
86
|
+
build_from_face_suit_values(arg1, arg2)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :suit, :face, :value
|
92
|
+
include Comparable
|
93
|
+
|
94
|
+
# Returns a string containing the representation of Card
|
95
|
+
#
|
96
|
+
# Card.new("7c").to_s # => "7c"
|
97
|
+
def to_s
|
98
|
+
FACES[@face].chr + SUITS[@suit].chr
|
99
|
+
end
|
100
|
+
|
101
|
+
# If to_card is called on a `Card` it should return itself
|
102
|
+
def to_card
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Compare the face value of this card with another card. Returns:
|
107
|
+
# -1 if self is less than card2
|
108
|
+
# 0 if self is the same face value of card2
|
109
|
+
# 1 if self is greater than card2
|
110
|
+
def <=> card2
|
111
|
+
@face <=> card2.face
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns true if the cards are the same card. Meaning they
|
115
|
+
# have the same suit and the same face value.
|
116
|
+
def == card2
|
117
|
+
@value == card2.value
|
118
|
+
end
|
119
|
+
alias :eql? :==
|
120
|
+
|
121
|
+
# Compute a hash-code for this Card. Two Cards with the same
|
122
|
+
# content will have the same hash code (and will compare using eql?).
|
123
|
+
def hash
|
124
|
+
@value.hash
|
125
|
+
end
|
126
|
+
|
127
|
+
# A card's natural value is the closer to it's intuitive value in a deck
|
128
|
+
# in the range of 1 to 52. Aces are low with a value of 1. Uses the bridge
|
129
|
+
# order of suits: clubs, diamonds, hearts, and spades. The formula used is:
|
130
|
+
# If the suit is clubs, the natural value is the face value (remember
|
131
|
+
# Aces are low). If the suit is diamonds, it is the clubs value plus 13.
|
132
|
+
# If the suit is hearts, it is plus 26. If it is spades, it is plus 39.
|
133
|
+
#
|
134
|
+
# Card.new("Ac").natural_value # => 1
|
135
|
+
# Card.new("Kc").natural_value # => 12
|
136
|
+
# Card.new("Ad").natural_value # => 13
|
137
|
+
def natural_value
|
138
|
+
natural_face = @face == 13 ? 1 : @face+1 # flip Ace from 13 to 1 and
|
139
|
+
# increment everything else by 1
|
140
|
+
natural_face + @suit * 13
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,487 @@
|
|
1
|
+
class PokerHand
|
2
|
+
include Comparable
|
3
|
+
include Enumerable
|
4
|
+
attr_reader :hand
|
5
|
+
|
6
|
+
@@allow_duplicates = true # true by default
|
7
|
+
def self.allow_duplicates; @@allow_duplicates; end
|
8
|
+
def self.allow_duplicates=(v); @@allow_duplicates = v; end
|
9
|
+
|
10
|
+
# Returns a new PokerHand object. Accepts the cards represented
|
11
|
+
# in a string or an array
|
12
|
+
#
|
13
|
+
# PokerHand.new("3d 5c 8h Ks") # => #<PokerHand:0x5c673c ...
|
14
|
+
# PokerHand.new(["3d", "5c", "8h", "Ks"]) # => #<PokerHand:0x5c2d6c ...
|
15
|
+
def initialize(cards = [])
|
16
|
+
@hand = case cards
|
17
|
+
when Array
|
18
|
+
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
|
+
when String
|
26
|
+
cards.scan(/\S{2}/).map { |str| Card.new(str) }
|
27
|
+
else
|
28
|
+
cards
|
29
|
+
end
|
30
|
+
|
31
|
+
check_for_duplicates unless 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
|
+
result = [8, Card::face_value(md[1])]
|
100
|
+
result << Card::face_value($1) if (md.pre_match + md.post_match).match(/(\S)/)
|
101
|
+
return [result, arrange_hand(md)]
|
102
|
+
end
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
def full_house?
|
107
|
+
if (md = (by_face =~ /(.). \1. \1. (.*)(.). \3./))
|
108
|
+
arranged_hand = arrange_hand(md[0] + ' ' +
|
109
|
+
md.pre_match + ' ' + md[2] + ' ' + md.post_match)
|
110
|
+
[
|
111
|
+
[7, Card::face_value(md[1]), Card::face_value(md[3])],
|
112
|
+
arranged_hand
|
113
|
+
]
|
114
|
+
elsif (md = (by_face =~ /((.). \2.) (.*)((.). \5. \5.)/))
|
115
|
+
arranged_hand = arrange_hand(md[4] + ' ' + md[1] + ' ' +
|
116
|
+
md.pre_match + ' ' + md[3] + ' ' + md.post_match)
|
117
|
+
[
|
118
|
+
[7, Card::face_value(md[5]), Card::face_value(md[2])],
|
119
|
+
arranged_hand
|
120
|
+
]
|
121
|
+
else
|
122
|
+
false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def flush?
|
127
|
+
if (md = (by_suit =~ /(.)(.) (.)\2 (.)\2 (.)\2 (.)\2/))
|
128
|
+
[
|
129
|
+
[
|
130
|
+
6,
|
131
|
+
Card::face_value(md[1]),
|
132
|
+
*(md[3..6].map { |f| Card::face_value(f) })
|
133
|
+
],
|
134
|
+
arrange_hand(md)
|
135
|
+
]
|
136
|
+
else
|
137
|
+
false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def straight?
|
142
|
+
result = false
|
143
|
+
if hand.size >= 5
|
144
|
+
transform = delta_transform
|
145
|
+
# note we can have more than one delta 0 that we
|
146
|
+
# need to shuffle to the back of the hand
|
147
|
+
i = 0
|
148
|
+
until transform.match(/^\S{3}( [1-9x]\S\S)+( 0\S\S)*$/) or i >= hand.size do
|
149
|
+
# only do this once per card in the hand to avoid entering an
|
150
|
+
# infinite loop if all of the cards in the hand are the same
|
151
|
+
transform.gsub!(/(\s0\S\S)(.*)/, "\\2\\1") # moves the front card to the back of the string
|
152
|
+
i += 1
|
153
|
+
end
|
154
|
+
if (md = (/.(.). 1.. 1.. 1.. 1../.match(transform)))
|
155
|
+
high_card = Card::face_value(md[1])
|
156
|
+
arranged_hand = fix_low_ace_display(md[0] + ' ' + md.pre_match + ' ' + md.post_match)
|
157
|
+
result = [[5, high_card], arranged_hand]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def three_of_a_kind?
|
163
|
+
if (md = (by_face =~ /(.). \1. \1./))
|
164
|
+
# get kicker
|
165
|
+
arranged_hand = arrange_hand(md)
|
166
|
+
matches = arranged_hand.match(/(?:\S\S ){2}(\S\S)/)
|
167
|
+
if matches
|
168
|
+
result = [4, Card::face_value(md[1])]
|
169
|
+
matches = arranged_hand.match(/(?:\S\S ){3}(\S)/)
|
170
|
+
result << Card::face_value($1) if matches
|
171
|
+
matches = arranged_hand.match(/(?:\S\S ){3}(\S)\S (\S)/)
|
172
|
+
result << Card::face_value($2) if matches
|
173
|
+
return [result, arranged_hand]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
false
|
177
|
+
end
|
178
|
+
|
179
|
+
def two_pair?
|
180
|
+
# \1 is the face value of the first pair
|
181
|
+
# \2 is the card in between the first pair and the second pair
|
182
|
+
# \3 is the face value of the second pair
|
183
|
+
if (md = (by_face =~ /(.). \1.(.*?) (.). \3./))
|
184
|
+
# to get the kicker this does the following
|
185
|
+
# md[0] is the regex matched above which includes the first pair and
|
186
|
+
# the second pair but also some cards in the middle so we sub them out
|
187
|
+
# then we add on the cards that came before the first pair, the cards
|
188
|
+
# that were in-between, and the cards that came after.
|
189
|
+
arranged_hand = arrange_hand(md[0].sub(md[2], '') + ' ' +
|
190
|
+
md.pre_match + ' ' + md[2] + ' ' + md.post_match)
|
191
|
+
matches = arranged_hand.match(/(?:\S\S ){3}(\S\S)/)
|
192
|
+
if matches
|
193
|
+
result = []
|
194
|
+
result << 3
|
195
|
+
result << Card::face_value(md[1]) # face value of the first pair
|
196
|
+
result << Card::face_value(md[3]) # face value of the second pair
|
197
|
+
matches = arranged_hand.match(/(?:\S\S ){4}(\S)/)
|
198
|
+
result << Card::face_value($1) if matches # face value of the kicker
|
199
|
+
return [result, arranged_hand]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
false
|
203
|
+
end
|
204
|
+
|
205
|
+
def pair?
|
206
|
+
if (md = (by_face =~ /(.). \1./))
|
207
|
+
arranged_hand_str = arrange_hand(md)
|
208
|
+
arranged_hand = PokerHand.new(arranged_hand_str)
|
209
|
+
|
210
|
+
if arranged_hand.hand[0].face == arranged_hand.hand[1].face &&
|
211
|
+
arranged_hand.hand[0].suit != arranged_hand.hand[1].suit
|
212
|
+
result = [2, arranged_hand.hand[0].face]
|
213
|
+
result << arranged_hand.hand[2].face if arranged_hand.size > 2
|
214
|
+
result << arranged_hand.hand[3].face if arranged_hand.size > 3
|
215
|
+
result << arranged_hand.hand[4].face if arranged_hand.size > 4
|
216
|
+
|
217
|
+
return [result, arranged_hand_str]
|
218
|
+
end
|
219
|
+
else
|
220
|
+
false
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def highest_card?
|
225
|
+
result = by_face
|
226
|
+
[[1, *result.face_values[0..result.face_values.length]], result.hand.join(' ')]
|
227
|
+
end
|
228
|
+
|
229
|
+
OPS = [
|
230
|
+
['Royal Flush', :royal_flush? ],
|
231
|
+
['Straight Flush', :straight_flush? ],
|
232
|
+
['Four of a kind', :four_of_a_kind? ],
|
233
|
+
['Full house', :full_house? ],
|
234
|
+
['Flush', :flush? ],
|
235
|
+
['Straight', :straight? ],
|
236
|
+
['Three of a kind', :three_of_a_kind?],
|
237
|
+
['Two pair', :two_pair? ],
|
238
|
+
['Pair', :pair? ],
|
239
|
+
['Highest Card', :highest_card? ],
|
240
|
+
]
|
241
|
+
|
242
|
+
# Returns the verbose hand rating
|
243
|
+
#
|
244
|
+
# PokerHand.new("4s 5h 6c 7d 8s").hand_rating # => "Straight"
|
245
|
+
def hand_rating
|
246
|
+
OPS.map { |op|
|
247
|
+
(method(op[1]).call()) ? op[0] : false
|
248
|
+
}.find { |v| v }
|
249
|
+
end
|
250
|
+
|
251
|
+
alias :rank :hand_rating
|
252
|
+
|
253
|
+
def score
|
254
|
+
# OPS.map returns an array containing the result of calling each OPS method again
|
255
|
+
# the poker hand. The non-nil cell closest to the front of the array represents
|
256
|
+
# the highest ranking.
|
257
|
+
# find([0]) returns [0] instead of nil if the hand does not match any of the rankings
|
258
|
+
# which is not likely to occur since every hand should at least have a highest card
|
259
|
+
OPS.map { |op|
|
260
|
+
method(op[1]).call()
|
261
|
+
}.find([0]) { |score| score }
|
262
|
+
end
|
263
|
+
|
264
|
+
# Returns a string of the hand arranged based on its rank. Usually this will be the
|
265
|
+
# same as by_face but there are some cases where it makes a difference.
|
266
|
+
#
|
267
|
+
# ph = PokerHand.new("As 3s 5s 2s 4s")
|
268
|
+
# ph.sort_using_rank # => "5s 4s 3s 2s As"
|
269
|
+
# ph.by_face.just_cards # => "As 5s 4s 3s 2s"
|
270
|
+
def sort_using_rank
|
271
|
+
score[1]
|
272
|
+
end
|
273
|
+
|
274
|
+
# Returns string with a listing of the cards in the hand followed by the hand's rank.
|
275
|
+
#
|
276
|
+
# h = PokerHand.new("8c 8s")
|
277
|
+
# h.to_s # => "8c 8s (Pair)"
|
278
|
+
def to_s
|
279
|
+
just_cards + " (" + hand_rating + ")"
|
280
|
+
end
|
281
|
+
|
282
|
+
# Returns an array of `Card` objects that make up the `PokerHand`.
|
283
|
+
def to_a
|
284
|
+
@hand
|
285
|
+
end
|
286
|
+
alias :to_ary :to_a
|
287
|
+
|
288
|
+
def <=> other_hand
|
289
|
+
self.score[0].compact <=> other_hand.score[0].compact
|
290
|
+
end
|
291
|
+
|
292
|
+
# Add a card to the hand
|
293
|
+
#
|
294
|
+
# hand = PokerHand.new("5d")
|
295
|
+
# hand << "6s" # => Add a six of spades to the hand by passing a string
|
296
|
+
# hand << ["7h", "8d"] # => Add multiple cards to the hand using an array
|
297
|
+
def << new_cards
|
298
|
+
if new_cards.is_a?(Card) || new_cards.is_a?(String)
|
299
|
+
new_cards = [new_cards]
|
300
|
+
end
|
301
|
+
|
302
|
+
new_cards.each do |nc|
|
303
|
+
unless allow_duplicates
|
304
|
+
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}/
|
305
|
+
end
|
306
|
+
|
307
|
+
@hand << Card.new(nc)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Remove a card from the hand.
|
312
|
+
#
|
313
|
+
# hand = PokerHand.new("5d Jd")
|
314
|
+
# hand.delete("Jd") # => #<Card:0x5d0674 @value=23, @face=10, @suit=1>
|
315
|
+
# hand.just_cards # => "5d"
|
316
|
+
def delete card
|
317
|
+
@hand.delete(Card.new(card))
|
318
|
+
end
|
319
|
+
|
320
|
+
# Same concept as Array#uniq
|
321
|
+
def uniq
|
322
|
+
PokerHand.new(@hand.uniq)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Resolving methods are just passed directly down to the @hand array
|
326
|
+
RESOLVING_METHODS = [:each, :size, :-]
|
327
|
+
RESOLVING_METHODS.each do |method|
|
328
|
+
class_eval %{
|
329
|
+
def #{method}(*args, &block)
|
330
|
+
@hand.#{method}(*args, &block)
|
331
|
+
end
|
332
|
+
}
|
333
|
+
end
|
334
|
+
|
335
|
+
def allow_duplicates
|
336
|
+
@@allow_duplicates
|
337
|
+
end
|
338
|
+
|
339
|
+
# Checks whether the hand matches usual expressions like AA, AK, AJ+, 66+, AQs, AQo...
|
340
|
+
#
|
341
|
+
# Valid expressions:
|
342
|
+
# * "AJ": Matches exact faces (in this case an Ace and a Jack), suited or not
|
343
|
+
# * "AJs": Same but suited only
|
344
|
+
# * "AJo": Same but offsuit only
|
345
|
+
# * "AJ+": Matches an Ace with any card >= Jack, suited or not
|
346
|
+
# * "AJs+": Same but suited only
|
347
|
+
# * "AJo+": Same but offsuit only
|
348
|
+
# * "JJ+": Matches any pair >= "JJ".
|
349
|
+
# * "8T+": Matches connectors (in this case with 1 gap : 8T, 9J, TQ, JK, QA)
|
350
|
+
# * "8Ts+": Same but suited only
|
351
|
+
# * "8To+": Same but offsuit only
|
352
|
+
#
|
353
|
+
# The order of the cards in the expression is important (8T+ is not the same as T8+), but the order of the cards in the hand is not ("AK" will match "Ad Kc" and "Kc Ad").
|
354
|
+
#
|
355
|
+
# The expression can be an array of expressions. In this case the method returns true if any expression matches.
|
356
|
+
#
|
357
|
+
# This method only works on hands with 2 cards.
|
358
|
+
#
|
359
|
+
# PokerHand.new('Ah Ad').match? 'AA' # => true
|
360
|
+
# PokerHand.new('Ah Kd').match? 'AQ+' # => true
|
361
|
+
# PokerHand.new('Jc Qc').match? '89s+' # => true
|
362
|
+
# PokerHand.new('Ah Jd').match? %w( 22+ A6s+ AJ+ ) # => true
|
363
|
+
# PokerHand.new('Ah Td').match? %w( 22+ A6s+ AJ+ ) # => false
|
364
|
+
#
|
365
|
+
def match? expression
|
366
|
+
raise "Hands with #{@hand.size} cards is not supported" unless @hand.size == 2
|
367
|
+
|
368
|
+
if expression.is_a? Array
|
369
|
+
return expression.any? { |e| match?(e) }
|
370
|
+
end
|
371
|
+
|
372
|
+
faces = @hand.map { |card| card.face }.sort.reverse
|
373
|
+
suited = @hand.map { |card| card.suit }.uniq.size == 1
|
374
|
+
if expression =~ /^(.)(.)(s|o|)(\+|)$/
|
375
|
+
face1 = Card.face_value($1)
|
376
|
+
face2 = Card.face_value($2)
|
377
|
+
raise ArgumentError, "Invalid expression: #{expression.inspect}" unless face1 and face2
|
378
|
+
suit_match = $3
|
379
|
+
plus = ($4 != "")
|
380
|
+
|
381
|
+
if plus
|
382
|
+
if face1 == face2
|
383
|
+
face_match = (faces.first == faces.last and faces.first >= face1)
|
384
|
+
elsif face1 > face2
|
385
|
+
face_match = (faces.first == face1 and faces.last >= face2)
|
386
|
+
else
|
387
|
+
face_match = ((faces.first - faces.last) == (face2 - face1) and faces.last >= face1)
|
388
|
+
end
|
389
|
+
else
|
390
|
+
expression_faces = [face1, face2].sort.reverse
|
391
|
+
face_match = (expression_faces == faces)
|
392
|
+
end
|
393
|
+
case suit_match
|
394
|
+
when ''
|
395
|
+
face_match
|
396
|
+
when 's'
|
397
|
+
face_match and suited
|
398
|
+
when 'o'
|
399
|
+
face_match and !suited
|
400
|
+
end
|
401
|
+
else
|
402
|
+
raise ArgumentError, "Invalid expression: #{expression.inspect}"
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def +(other)
|
407
|
+
cards = @hand.map { |card| Card.new(card) }
|
408
|
+
case other
|
409
|
+
when String
|
410
|
+
cards << Card.new(other)
|
411
|
+
when Card
|
412
|
+
cards << other
|
413
|
+
when PokerHand
|
414
|
+
cards += other.hand
|
415
|
+
else
|
416
|
+
raise ArgumentError, "Invalid argument: #{other.inspect}"
|
417
|
+
end
|
418
|
+
PokerHand.new(cards)
|
419
|
+
end
|
420
|
+
|
421
|
+
private
|
422
|
+
|
423
|
+
def check_for_duplicates
|
424
|
+
if @hand.size != @hand.uniq.size && !allow_duplicates
|
425
|
+
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."
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
# if md is a string, arrange_hand will remove extra white space
|
430
|
+
# if md is a MatchData, arrange_hand returns the matched segment
|
431
|
+
# followed by the pre_match and the post_match
|
432
|
+
def arrange_hand(md)
|
433
|
+
hand = if md.respond_to?(:to_str)
|
434
|
+
md
|
435
|
+
else
|
436
|
+
md[0] + ' ' + md.pre_match + md.post_match
|
437
|
+
end
|
438
|
+
hand.strip.squeeze(" ") # remove extra whitespace
|
439
|
+
end
|
440
|
+
|
441
|
+
# delta transform creates a version of the cards where the delta
|
442
|
+
# between card values is in the string, so a regexp can then match a
|
443
|
+
# straight and/or straight flush
|
444
|
+
def delta_transform(use_suit = false)
|
445
|
+
aces = @hand.select { |c| c.face == Card::face_value('A') }
|
446
|
+
aces.map! { |c| Card.new(1,c.suit) }
|
447
|
+
|
448
|
+
base = if (use_suit)
|
449
|
+
(@hand + aces).sort_by { |c| [c.suit, c.face] }.reverse
|
450
|
+
else
|
451
|
+
(@hand + aces).sort_by { |c| [c.face, c.suit] }.reverse
|
452
|
+
end
|
453
|
+
|
454
|
+
result = base.inject(['',nil]) do |(delta_hand, prev_card), card|
|
455
|
+
if (prev_card)
|
456
|
+
delta = prev_card - card.face
|
457
|
+
else
|
458
|
+
delta = 0
|
459
|
+
end
|
460
|
+
# does not really matter for my needs
|
461
|
+
delta = 'x' if (delta > 9 || delta < 0)
|
462
|
+
delta_hand += delta.to_s + card.to_s + ' '
|
463
|
+
[delta_hand, card.face]
|
464
|
+
end
|
465
|
+
|
466
|
+
# we just want the delta transform, not the last cards face too
|
467
|
+
result[0].chop
|
468
|
+
end
|
469
|
+
|
470
|
+
def fix_low_ace_display(arranged_hand)
|
471
|
+
# remove card deltas (this routine is only used for straights)
|
472
|
+
arranged_hand.gsub!(/\S(\S\S)\s*/, "\\1 ")
|
473
|
+
|
474
|
+
# Fix "low aces"
|
475
|
+
arranged_hand.gsub!(/L(\S)/, "A\\1")
|
476
|
+
|
477
|
+
# Remove duplicate aces (this will not work if you have
|
478
|
+
# multiple decks or wild cards)
|
479
|
+
arranged_hand.gsub!(/((A\S).*)\2/, "\\1")
|
480
|
+
|
481
|
+
# cleanup white space
|
482
|
+
arranged_hand.gsub!(/\s+/, ' ')
|
483
|
+
# careful to use gsub as gsub! can return nil here
|
484
|
+
arranged_hand.gsub(/\s+$/, '')
|
485
|
+
end
|
486
|
+
|
487
|
+
end
|