hands 0.0.2 → 0.1.0
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/.yardopts +6 -0
- data/Gemfile +11 -2
- data/LICENSE +1 -1
- data/Rakefile +9 -5
- data/Readme.markdown +12 -8
- data/Todo.markdown +19 -0
- data/hands.gemspec +2 -2
- data/lib/hands.rb +13 -4
- data/lib/hands/card.rb +74 -19
- data/lib/hands/deck.rb +27 -0
- data/lib/hands/hand.rb +25 -139
- data/lib/hands/hand_detection.rb +153 -0
- data/lib/hands/player.rb +19 -0
- data/lib/hands/table.rb +78 -0
- data/lib/hands/version.rb +2 -1
- data/test/test_helper.rb +11 -0
- data/test/units/card_test.rb +72 -0
- data/test/units/deck_test.rb +16 -0
- data/test/units/hand_detection_test.rb +165 -0
- data/test/units/hand_test.rb +53 -0
- data/test/units/player_test.rb +11 -0
- data/test/units/table_test.rb +75 -0
- metadata +32 -11
- data/spec/models/card_spec.rb +0 -74
- data/spec/models/hand_spec.rb +0 -181
- data/spec/spec_helper.rb +0 -5
data/.yardopts
ADDED
data/Gemfile
CHANGED
@@ -2,6 +2,15 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
+
# Utility
|
5
6
|
gem 'rake'
|
6
|
-
|
7
|
-
|
7
|
+
|
8
|
+
# Documentation
|
9
|
+
gem 'yard'
|
10
|
+
gem 'redcarpet'
|
11
|
+
|
12
|
+
group :test do
|
13
|
+
gem 'minitest'
|
14
|
+
gem 'minitest-wscolor'
|
15
|
+
gem 'simplecov'
|
16
|
+
end
|
data/LICENSE
CHANGED
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
-
#!/usr/bin/env rake
|
2
1
|
require 'bundler/gem_tasks'
|
3
|
-
require '
|
2
|
+
require 'rake/testtask'
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
t.
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << 'test'
|
6
|
+
t.pattern = 'test/**/*_test.rb'
|
7
|
+
end
|
8
|
+
task default: :test
|
9
|
+
|
10
|
+
task :coverage do
|
11
|
+
`open coverage/index.html`
|
8
12
|
end
|
data/Readme.markdown
CHANGED
@@ -4,6 +4,8 @@ Simple library for calculating poker hands.
|
|
4
4
|
|
5
5
|
Currently this gem is very limited. I plan on adding outs, odds, and other actually useful stuff. I started writing this on a plane as a personal challenge. It's current state is crude, although tested and works.
|
6
6
|
|
7
|
+
[](https://gemnasium.com/soffes/hands)
|
8
|
+
|
7
9
|
## Installation
|
8
10
|
|
9
11
|
Add this line to your application's Gemfile:
|
@@ -22,6 +24,8 @@ Or install it yourself as:
|
|
22
24
|
|
23
25
|
## Usage
|
24
26
|
|
27
|
+
Read the [documentation](http://rubydoc.info/github/samsoffes/hands/master/frames).
|
28
|
+
|
25
29
|
``` ruby
|
26
30
|
# Best hand detection
|
27
31
|
straight = Hands::Hand.new
|
@@ -49,8 +53,8 @@ flush << Hands::Card[4, :hearts]
|
|
49
53
|
flush > pair # true
|
50
54
|
|
51
55
|
# Card comparison
|
52
|
-
card1 = Hands::Card.new(:value => 2, :
|
53
|
-
card2 = Hands::Card.new(:value => 3, :
|
56
|
+
card1 = Hands::Card.new(:value => 2, :suit => :hearts)
|
57
|
+
card2 = Hands::Card.new(:value => 3, :suit => :clubs)
|
54
58
|
card2 > card1 # true
|
55
59
|
```
|
56
60
|
|
@@ -59,15 +63,15 @@ card2 > card1 # true
|
|
59
63
|
Running and reading the tests is (for now) the best way to see the functionality of this gem.
|
60
64
|
|
61
65
|
```
|
62
|
-
$ bundle
|
63
|
-
$ bundle exec rake spec
|
66
|
+
$ bundle exec rake
|
64
67
|
```
|
65
68
|
|
66
69
|
## Contributing
|
67
70
|
|
68
71
|
1. Fork it
|
69
72
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
70
|
-
3. Write
|
71
|
-
4.
|
72
|
-
5.
|
73
|
-
6.
|
73
|
+
3. Write passing specs
|
74
|
+
4. Write documentation
|
75
|
+
5. Commit your changes (`git commit -am 'Added some feature'`)
|
76
|
+
6. Push to the branch (`git push origin my-new-feature`)
|
77
|
+
7. Create new Pull Request
|
data/Todo.markdown
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Hands Todo
|
2
|
+
|
3
|
+
## Features
|
4
|
+
|
5
|
+
* Best 5 card hand in hold'em
|
6
|
+
* Blinds
|
7
|
+
* Betting
|
8
|
+
* Missed blinds
|
9
|
+
* Outs
|
10
|
+
* Odds for completing each hand
|
11
|
+
* Pot odds
|
12
|
+
* Games other than hold'em
|
13
|
+
* AI
|
14
|
+
|
15
|
+
## To Document and Test
|
16
|
+
|
17
|
+
* Player
|
18
|
+
* Table
|
19
|
+
* Deck
|
data/hands.gemspec
CHANGED
@@ -3,10 +3,10 @@ require File.expand_path('../lib/hands/version', __FILE__)
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
5
|
gem.authors = ['Sam Soffes']
|
6
|
-
gem.email = ['sam@
|
6
|
+
gem.email = ['sam@soff.es']
|
7
7
|
gem.description = 'Simple library for various poker hands calculations.'
|
8
8
|
gem.summary = 'Simple library for various poker hands calculations.'
|
9
|
-
gem.homepage = 'http://github.com/
|
9
|
+
gem.homepage = 'http://github.com/soffes/hands'
|
10
10
|
|
11
11
|
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
12
|
gem.files = `git ls-files`.split("\n")
|
data/lib/hands.rb
CHANGED
@@ -1,12 +1,21 @@
|
|
1
1
|
require 'hands/version'
|
2
2
|
require 'hands/card'
|
3
3
|
require 'hands/hand'
|
4
|
+
require 'hands/player'
|
5
|
+
require 'hands/deck'
|
6
|
+
require 'hands/table'
|
4
7
|
|
8
|
+
# See <http://www.pagat.com/poker/rules/ranking.html> for ranking references.
|
5
9
|
module Hands
|
6
|
-
|
10
|
+
# All card values
|
11
|
+
VALUES = %w{ 2 3 4 5 6 7 8 9 10 j q k a }.freeze
|
12
|
+
|
13
|
+
# All card value descriptions
|
7
14
|
VALUE_DESCRIPTIONS = %w{ two three four five six seven eight nine ten jack queen king ace }
|
8
15
|
|
9
|
-
#
|
10
|
-
SUITES =
|
11
|
-
|
16
|
+
# Reverse alphabetically ordered suits
|
17
|
+
SUITES = [:clubs, :diamonds, :hearts, :spades].freeze
|
18
|
+
|
19
|
+
# Ranks of poker hands
|
20
|
+
HAND_ORDER = %w{ high_card pair two_pair three_of_a_kind straight flush full_house four_of_a_kind straight_flush }.freeze
|
12
21
|
end
|
data/lib/hands/card.rb
CHANGED
@@ -1,27 +1,46 @@
|
|
1
1
|
module Hands
|
2
|
+
# Represents a poker playing card.
|
3
|
+
#
|
4
|
+
# You can use `Card[]` as a quick initializer. For example,
|
5
|
+
# `Card[2, :clubs].description` results in `"Two of Clubs"`.
|
2
6
|
class Card
|
3
7
|
include Comparable
|
4
8
|
|
5
|
-
|
9
|
+
# @return [Symbol] Card's suit
|
10
|
+
# @see SUITES
|
11
|
+
attr_accessor :suit
|
12
|
+
|
13
|
+
# Card's value
|
14
|
+
#
|
15
|
+
# If an invalid value is set, the value will be set to `nil`.
|
16
|
+
# @return [String] Card's value
|
17
|
+
# @see VALUES
|
6
18
|
attr_accessor :value
|
7
19
|
|
8
|
-
|
9
|
-
|
20
|
+
# (see #initialize)
|
21
|
+
def self.[](value = nil, suit = nil)
|
22
|
+
self.new(value, suit)
|
10
23
|
end
|
11
24
|
|
12
|
-
|
25
|
+
# Initialize a Card
|
26
|
+
#
|
27
|
+
# @param [String, Integer, Hash] value If an `Integer` or `String` are provided, this will be set to the value. If a `Hash` is provided, its value for `:value` will be set to the `Card`'s value and its `:suit` value will be set to the `Card`'s suit.
|
28
|
+
# @param [Symbol] suit Sets the `Card`'s suit.
|
29
|
+
# @return [Card] A new instance of Card
|
30
|
+
# @see Card.[]
|
31
|
+
def initialize(value = nil, suit = nil)
|
13
32
|
# Value provided
|
14
|
-
if
|
15
|
-
self.value =
|
33
|
+
if value.is_a?(Integer) or value.is_a?(String)
|
34
|
+
self.value = value
|
16
35
|
|
17
36
|
# Hash provided
|
18
|
-
elsif
|
19
|
-
self.value =
|
20
|
-
self.
|
37
|
+
elsif value.is_a?(Hash)
|
38
|
+
self.value = value[:value] if value[:value]
|
39
|
+
self.suit = value[:suit] if value[:suit]
|
21
40
|
end
|
22
41
|
|
23
|
-
# Set
|
24
|
-
self.
|
42
|
+
# Set suit
|
43
|
+
self.suit = suit if suit
|
25
44
|
end
|
26
45
|
|
27
46
|
def value=(val)
|
@@ -29,7 +48,7 @@ module Hands
|
|
29
48
|
if val.is_a?(Integer)
|
30
49
|
# Number range
|
31
50
|
if val > 0 and val <= 10
|
32
|
-
@value = val
|
51
|
+
@value = val.to_s
|
33
52
|
return
|
34
53
|
|
35
54
|
# Face card or ace range
|
@@ -48,6 +67,17 @@ module Hands
|
|
48
67
|
@value = nil
|
49
68
|
end
|
50
69
|
|
70
|
+
def suit=(suit)
|
71
|
+
if (suit.is_a?(String) or suit.is_a?(Symbol)) and SUITES.include?(suit.to_sym)
|
72
|
+
@suit = suit.to_sym
|
73
|
+
else
|
74
|
+
@suit = nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Standard inspect
|
79
|
+
#
|
80
|
+
# @return [String] `super`'s implementation and the receiver's `description` if it `is_valid?`
|
51
81
|
def inspect
|
52
82
|
if self.is_valid?
|
53
83
|
"#{super} #{self.description}"
|
@@ -56,35 +86,60 @@ module Hands
|
|
56
86
|
end
|
57
87
|
end
|
58
88
|
|
89
|
+
# @return [Boolean] Does the receiver contain a valid value and suit combination
|
59
90
|
def is_valid?
|
60
|
-
SUITES.include?(
|
91
|
+
SUITES.include?(@suit) and VALUES.include?(@value.to_s.downcase)
|
61
92
|
end
|
62
93
|
|
94
|
+
# @return [Boolean] Does the receiver contain an invalid value and suit combination
|
63
95
|
def is_invalid?
|
64
96
|
!self.is_valid?
|
65
97
|
end
|
66
98
|
|
99
|
+
# Get a string representation of the card
|
100
|
+
#
|
101
|
+
# @return [String] string representation of the card
|
67
102
|
def description
|
68
103
|
if self.is_valid?
|
69
|
-
"#{VALUE_DESCRIPTIONS[self.value_index].capitalize} of #{self.
|
104
|
+
"#{VALUE_DESCRIPTIONS[self.value_index].capitalize} of #{self.suit.to_s.capitalize}"
|
70
105
|
else
|
71
106
|
'invalid'
|
72
107
|
end
|
73
108
|
end
|
74
109
|
|
75
|
-
|
110
|
+
# Compares the card with another card
|
111
|
+
#
|
112
|
+
# By default `check_suit` is `false`. If set to `true`, it will order cards that have the same value by their suit.
|
113
|
+
#
|
114
|
+
# @param [Card] other_card the card to compare the receiver to
|
115
|
+
# @param [Boolean] check_suit a boolean indicating if the suit should be accounted for
|
116
|
+
# @return [Integer] `-1` if `other_card` is less than the receiver, `0` for equal to, and `1` for greater than
|
117
|
+
# @see SUITES
|
118
|
+
def <=>(other_card, check_suit = false)
|
76
119
|
# TODO: Handle invalid cards
|
77
120
|
result = self.value_index <=> other_card.value_index
|
78
|
-
return self.
|
121
|
+
return self.suit_index <=> other_card.suit_index if result == 0 and check_suit
|
79
122
|
result
|
80
123
|
end
|
81
124
|
|
82
|
-
|
83
|
-
|
125
|
+
# Suite's index
|
126
|
+
#
|
127
|
+
# Mainly used for internal reasons when comparing cards.
|
128
|
+
#
|
129
|
+
# @return [Integer] index of the card's suit
|
130
|
+
# @see SUITES
|
131
|
+
def suit_index
|
132
|
+
SUITES.index(self.suit.downcase)
|
84
133
|
end
|
85
134
|
|
135
|
+
# Values's index
|
136
|
+
#
|
137
|
+
# Mainly used for internal reasons when comparing cards.
|
138
|
+
#
|
139
|
+
# @return [Integer] index of the card's value
|
140
|
+
# @see VALUES
|
86
141
|
def value_index
|
87
|
-
VALUES.index(self.value.
|
142
|
+
VALUES.index(self.value.downcase)
|
88
143
|
end
|
89
144
|
end
|
90
145
|
end
|
data/lib/hands/deck.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Hands
|
2
|
+
class Deck
|
3
|
+
# @return [Array] {Card}s in the {Deck}
|
4
|
+
attr_reader :cards
|
5
|
+
|
6
|
+
# Initialize the deck with 52 {Card}s
|
7
|
+
def initialize
|
8
|
+
@cards = []
|
9
|
+
VALUES.each do |value|
|
10
|
+
SUITES.each do |suit|
|
11
|
+
@cards << Card[value, suit]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Shuffle the {Deck}
|
17
|
+
def shuffle!
|
18
|
+
@cards.shuffle!
|
19
|
+
end
|
20
|
+
|
21
|
+
# Pop a card off the {Deck}
|
22
|
+
# @return [Card] the {Card}(s) popped off the {Deck}
|
23
|
+
def pop(number_of_cards = 1)
|
24
|
+
@cards.pop(number_of_cards)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/hands/hand.rb
CHANGED
@@ -1,164 +1,50 @@
|
|
1
|
+
require 'hands/hand_detection'
|
2
|
+
|
1
3
|
module Hands
|
4
|
+
# Represents a poker hand.
|
2
5
|
class Hand
|
3
6
|
include Comparable
|
4
7
|
|
8
|
+
# @return [Array] {Card}s in the {Hand}
|
5
9
|
attr_accessor :cards
|
6
10
|
|
7
|
-
def
|
8
|
-
@cards
|
11
|
+
def initialize
|
12
|
+
@cards = []
|
9
13
|
end
|
10
14
|
|
15
|
+
# Compares the {Hand} with another {Hand}.
|
16
|
+
#
|
17
|
+
# @param [Hand] other_hand the {Hand} to compare the receiver to
|
18
|
+
# @return [Integer] `-1` if `other_hand` is less than the receiver, `0` for equal to, and `1` for greater than
|
19
|
+
# @see HAND_ORDER
|
20
|
+
# @see Card#<=>
|
11
21
|
def <=>(other_hand)
|
12
22
|
response = (self.hand_index <=> other_hand.hand_index)
|
13
23
|
|
14
|
-
# If the
|
24
|
+
# If the {Hand}s tie, see which is higher (i.e. higher pair)
|
15
25
|
if response == 0
|
16
|
-
|
26
|
+
@cards <=> other_hand.cards
|
17
27
|
else
|
18
28
|
response
|
19
29
|
end
|
20
30
|
end
|
21
31
|
|
32
|
+
# Add a {Card}
|
33
|
+
#
|
34
|
+
# @return [Card] the {Card} added
|
22
35
|
def <<(card)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
def suites
|
27
|
-
self.cards.collect(&:suite).uniq
|
28
|
-
end
|
29
|
-
|
30
|
-
def best_hand
|
31
|
-
response = {}
|
32
|
-
HAND_ORDER.reverse.each do |type|
|
33
|
-
cards = self.send(type.to_sym)
|
34
|
-
next unless cards
|
35
|
-
response[:type] = type
|
36
|
-
response[:cards] = cards
|
37
|
-
break
|
38
|
-
end
|
39
|
-
response
|
40
|
-
end
|
41
|
-
|
42
|
-
def high_card
|
43
|
-
self.cards.sort.reverse
|
44
|
-
end
|
45
|
-
|
46
|
-
def pair
|
47
|
-
self.pairs(1)
|
48
|
-
end
|
49
|
-
|
50
|
-
def two_pair
|
51
|
-
self.pairs(2)
|
52
|
-
end
|
53
|
-
|
54
|
-
def three_of_a_kind
|
55
|
-
self.kinds(3)
|
56
|
-
end
|
57
|
-
|
58
|
-
def straight
|
59
|
-
return nil unless self.cards.length == 5
|
60
|
-
cs = self.cards.sort.reverse
|
61
|
-
|
62
|
-
# Ace's low
|
63
|
-
if cs.first.value == 'a' and cs[1].value == 5
|
64
|
-
# Move ace to end
|
65
|
-
ace = cs.first
|
66
|
-
cs = cs[1..4]
|
67
|
-
cs << ace
|
68
|
-
|
69
|
-
# Check succession
|
70
|
-
csr = cs.reverse
|
71
|
-
4.times do |i|
|
72
|
-
next if i == 0
|
73
|
-
return nil unless csr[i].value_index == i - 1
|
74
|
-
end
|
75
|
-
|
76
|
-
# Normal
|
77
|
-
else
|
78
|
-
# Check range
|
79
|
-
return nil unless cs.first.value_index - cs.last.value_index == 4
|
80
|
-
|
81
|
-
# Check succession
|
82
|
-
4.times do |i|
|
83
|
-
return nil unless cs[i].value_index == cs[i + 1].value_index + 1
|
84
|
-
end
|
85
|
-
end
|
86
|
-
cs
|
87
|
-
end
|
88
|
-
|
89
|
-
def flush
|
90
|
-
# If all of the cards are the same suite, we have a flush
|
91
|
-
return nil unless self.suites.length == 1
|
92
|
-
self.cards.sort.reverse
|
93
|
-
end
|
94
|
-
|
95
|
-
def full_house
|
96
|
-
dupes = self.duplicates
|
97
|
-
return nil unless dupes.length == 2
|
98
|
-
|
99
|
-
a = []
|
100
|
-
b = []
|
101
|
-
|
102
|
-
hand = self.cards.select do |card|
|
103
|
-
if dupes.first == card.value
|
104
|
-
a << card
|
105
|
-
elsif dupes.last == card.value
|
106
|
-
b << card
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
return nil unless a.length + b.length == 5
|
111
|
-
self.cards.sort.reverse
|
112
|
-
end
|
113
|
-
|
114
|
-
def four_of_a_kind
|
115
|
-
self.kinds(4)
|
36
|
+
@cards << card
|
37
|
+
@cards.flatten! # TODO: Figure out why this is necessary
|
116
38
|
end
|
117
39
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
122
|
-
|
123
|
-
protected
|
124
|
-
|
125
|
-
def hand_index
|
126
|
-
best = self.best_hand
|
127
|
-
return -1 if best.nil?
|
128
|
-
HAND_ORDER.index(best[:type].to_s)
|
40
|
+
# @return [Array] All of the suits contained in the {Hand}
|
41
|
+
def suits
|
42
|
+
@cards.collect(&:suit).uniq
|
129
43
|
end
|
130
44
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
end
|
135
|
-
|
136
|
-
def pairs(min)
|
137
|
-
dupes = self.duplicates
|
138
|
-
return nil if dupes.length < min
|
139
|
-
|
140
|
-
hand = self.cards.select do |card|
|
141
|
-
dupes.include?(card.value)
|
142
|
-
end
|
143
|
-
|
144
|
-
hand = hand.sort.reverse
|
145
|
-
hand << (self.cards - hand).sort.reverse
|
146
|
-
hand.flatten
|
147
|
-
end
|
148
|
-
|
149
|
-
def kinds(num)
|
150
|
-
dupes = self.duplicates
|
151
|
-
return nil unless dupes.length == 1
|
152
|
-
|
153
|
-
hand = self.cards.select do |card|
|
154
|
-
dupes.include?(card.value)
|
155
|
-
end
|
156
|
-
|
157
|
-
return nil unless hand.length == num
|
158
|
-
|
159
|
-
hand = hand.sort.reverse
|
160
|
-
hand << (self.cards - hand).sort.reverse
|
161
|
-
hand.flatten
|
45
|
+
# Empties the hand
|
46
|
+
def empty!
|
47
|
+
@cards = []
|
162
48
|
end
|
163
49
|
end
|
164
50
|
end
|