hands 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Dependency Status](https://gemnasium.com/soffes/hands.png)](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
|