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
@@ -0,0 +1,153 @@
|
|
1
|
+
module Hands
|
2
|
+
class Hand
|
3
|
+
# @return [Hash] A hash with `:type` and `:cards` keys.
|
4
|
+
def best_hand
|
5
|
+
response = {}
|
6
|
+
HAND_ORDER.reverse.each do |type|
|
7
|
+
cards = self.send(type.to_sym)
|
8
|
+
next unless cards
|
9
|
+
response[:type] = type
|
10
|
+
response[:cards] = cards
|
11
|
+
break
|
12
|
+
end
|
13
|
+
response
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Array] Array of {Card} objects with the highest {Card} first
|
17
|
+
def high_card
|
18
|
+
self.cards.sort.reverse
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Array, Nil] Array of {Card} objects with the pair first and kickers in decending order or `nil` if there isn't a pair in the {Hand}
|
22
|
+
def pair
|
23
|
+
self.pairs(1)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Array, Nil] Array of {Card} objects with the pairs first and kickers in decending order or `nil` if there isn't two pair in the {Hand}
|
27
|
+
def two_pair
|
28
|
+
self.pairs(2)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Array, Nil] Array of {Card} objects with the three of a kind first and kickers in decending order or `nil` if there isn't three of a kind in the {Hand}
|
32
|
+
def three_of_a_kind
|
33
|
+
self.kinds(3)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Array, Nil] Array of {Card} objects with the straight in decending order or `nil` if there isn't a straight in the {Hand}
|
37
|
+
def straight
|
38
|
+
return nil unless self.cards.length == 5
|
39
|
+
cs = self.cards.sort.reverse
|
40
|
+
|
41
|
+
# Ace's low
|
42
|
+
if cs.first.value == 'a' and cs[1].value == '5'
|
43
|
+
# Move ace to end
|
44
|
+
ace = cs.first
|
45
|
+
cs = cs[1..4]
|
46
|
+
cs << ace
|
47
|
+
|
48
|
+
# Check succession
|
49
|
+
csr = cs.reverse
|
50
|
+
4.times do |i|
|
51
|
+
next if i == 0
|
52
|
+
return nil unless csr[i].value_index == i - 1
|
53
|
+
end
|
54
|
+
|
55
|
+
# Normal
|
56
|
+
else
|
57
|
+
# Check range
|
58
|
+
return nil unless cs.first.value_index - cs.last.value_index == 4
|
59
|
+
|
60
|
+
# Check succession
|
61
|
+
4.times do |i|
|
62
|
+
return nil unless cs[i].value_index == cs[i + 1].value_index + 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
cs
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Array, Nil] Array of {Card} objects with the flush in decending order or `nil` if there isn't a flush in the {Hand}
|
69
|
+
def flush
|
70
|
+
# If all of the {Card}s are the same suit, we have a flush
|
71
|
+
return nil unless self.suits.length == 1
|
72
|
+
self.cards.sort.reverse
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Array, Nil] Array of {Card} objects with the full house in decending order or `nil` if there isn't a full house in the {Hand}
|
76
|
+
def full_house
|
77
|
+
dupes = self.duplicates
|
78
|
+
return nil unless dupes.length == 2
|
79
|
+
|
80
|
+
a = []
|
81
|
+
b = []
|
82
|
+
|
83
|
+
hand = self.cards.select do |card|
|
84
|
+
if dupes.first == card.value
|
85
|
+
a << card
|
86
|
+
elsif dupes.last == card.value
|
87
|
+
b << card
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
return nil unless a.length + b.length == 5
|
92
|
+
self.cards.sort.reverse
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Array, Nil] Array of {Card} objects with the four of a kind first the kicker in decending order or `nil` if there isn't four of a kind in the {Hand}
|
96
|
+
def four_of_a_kind
|
97
|
+
self.kinds(4)
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Array, Nil] Array of {Card} objects with the straight flush in decending order or `nil` if there isn't a straight flush in the {Hand}
|
101
|
+
def straight_flush
|
102
|
+
return nil unless self.flush
|
103
|
+
self.straight
|
104
|
+
end
|
105
|
+
|
106
|
+
# Hand's index
|
107
|
+
#
|
108
|
+
# Mainly used for internal reasons when comparing {Hand}.
|
109
|
+
#
|
110
|
+
# @return [Integer] index of the {Hand}'s rank
|
111
|
+
# @see HAND_ORDER
|
112
|
+
def hand_index
|
113
|
+
best = self.best_hand
|
114
|
+
return -1 if best.nil?
|
115
|
+
HAND_ORDER.index(best[:type].to_s)
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
def duplicates
|
121
|
+
pairs = self.cards.collect(&:value)
|
122
|
+
pairs.uniq.select{ |e| (pairs - [e]).size < pairs.size - 1 }
|
123
|
+
end
|
124
|
+
|
125
|
+
def pairs(min)
|
126
|
+
dupes = self.duplicates
|
127
|
+
return nil if dupes.length < min
|
128
|
+
|
129
|
+
hand = self.cards.select do |card|
|
130
|
+
dupes.include?(card.value)
|
131
|
+
end
|
132
|
+
|
133
|
+
hand = hand.sort.reverse
|
134
|
+
hand << (self.cards - hand).sort.reverse
|
135
|
+
hand.flatten
|
136
|
+
end
|
137
|
+
|
138
|
+
def kinds(num)
|
139
|
+
dupes = self.duplicates
|
140
|
+
return nil unless dupes.length == 1
|
141
|
+
|
142
|
+
hand = self.cards.select do |card|
|
143
|
+
dupes.include?(card.value)
|
144
|
+
end
|
145
|
+
|
146
|
+
return nil unless hand.length == num
|
147
|
+
|
148
|
+
hand = hand.sort.reverse
|
149
|
+
hand << (self.cards - hand).sort.reverse
|
150
|
+
hand.flatten
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/hands/player.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Hands
|
2
|
+
class Player
|
3
|
+
# @return [String] Their name
|
4
|
+
attr_accessor :name
|
5
|
+
|
6
|
+
# @return [Hand] Their hand
|
7
|
+
attr_accessor :hand
|
8
|
+
|
9
|
+
# @return [Table] The {Table} they are sitting at
|
10
|
+
attr_accessor :table
|
11
|
+
|
12
|
+
# Initialize a new {Player}
|
13
|
+
# @param [String] name {Player}'s name
|
14
|
+
def initialize(name = nil)
|
15
|
+
@hand = Hand.new
|
16
|
+
self.name = name if name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/hands/table.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Hands
|
2
|
+
# Represents a poker table.
|
3
|
+
class Table
|
4
|
+
# @return [Deck] the {Deck}
|
5
|
+
attr_reader :deck
|
6
|
+
|
7
|
+
# @return [Array] {Card}s on the table
|
8
|
+
attr_reader :community
|
9
|
+
|
10
|
+
# @return [Array] {Card}s in the muck
|
11
|
+
attr_reader :muck
|
12
|
+
|
13
|
+
# @return [Array] {Player}s at the {Table}
|
14
|
+
attr_accessor :players
|
15
|
+
|
16
|
+
# @return [Integer] index of the {Player} that has the dealer button
|
17
|
+
attr_accessor :dealer_position
|
18
|
+
|
19
|
+
# Initializes the table with a {Deck}
|
20
|
+
def initialize
|
21
|
+
@deck = Deck.new
|
22
|
+
@community = []
|
23
|
+
@muck = []
|
24
|
+
@dealer_position = 0
|
25
|
+
@deck.shuffle!
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Array] {Player}s ordered by position. Dealer is last.
|
29
|
+
def ordered_players
|
30
|
+
[@players.slice(@dealer_position + 1, @players.length - @dealer_position - 1) + @players.slice(0, @dealer_position + 1)]
|
31
|
+
end
|
32
|
+
|
33
|
+
def deal_player_cards!(number_of_cards = 2)
|
34
|
+
number_of_cards.times do
|
35
|
+
self.ordered_players.flatten.each do |player|
|
36
|
+
player.hand << @deck.pop
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def deal_flop!
|
42
|
+
self.burn_card!
|
43
|
+
self.turn_cards!(3)
|
44
|
+
end
|
45
|
+
|
46
|
+
def deal_turn!
|
47
|
+
self.burn_card!
|
48
|
+
self.turn_card!
|
49
|
+
end
|
50
|
+
|
51
|
+
alias_method :deal_river!, :deal_turn!
|
52
|
+
|
53
|
+
def burn_card!
|
54
|
+
@muck << @deck.pop
|
55
|
+
end
|
56
|
+
|
57
|
+
def turn_cards!(number_of_cards = 1)
|
58
|
+
@community << @deck.pop(number_of_cards)
|
59
|
+
@community.flatten!
|
60
|
+
end
|
61
|
+
|
62
|
+
alias_method :turn_card!, :turn_cards!
|
63
|
+
|
64
|
+
def new_hand!
|
65
|
+
@deck = Deck.new
|
66
|
+
@community = []
|
67
|
+
@muck = []
|
68
|
+
@players.each do |player|
|
69
|
+
player.hand.empty!
|
70
|
+
end
|
71
|
+
@deck.shuffle!
|
72
|
+
|
73
|
+
# Move the button
|
74
|
+
@dealer_position = @dealer_position + 1
|
75
|
+
@dealer_position = 0 if @dealer_position == @players.length
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/hands/version.rb
CHANGED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CardTest < Hands::TestCase
|
4
|
+
def test_that_it_validates_cards
|
5
|
+
card = Hands::Card.new
|
6
|
+
refute card.is_valid?
|
7
|
+
assert card.is_invalid?
|
8
|
+
|
9
|
+
card.suit = :hearts
|
10
|
+
refute card.is_valid?
|
11
|
+
|
12
|
+
card.value = 9
|
13
|
+
assert card.is_valid?
|
14
|
+
|
15
|
+
card.suit = 17
|
16
|
+
refute card.is_valid?
|
17
|
+
|
18
|
+
card.suit = :hearts
|
19
|
+
card.value = 19
|
20
|
+
refute card.is_valid?
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_that_it_allows_integers_for_high_cards
|
24
|
+
card1 = Hands::Card.new(value: 11, suit: :clubs)
|
25
|
+
assert card1.is_valid?
|
26
|
+
assert_equal 'j', card1.value
|
27
|
+
|
28
|
+
card2 = Hands::Card.new(value: 'j', suit: :clubs)
|
29
|
+
assert_equal card1, card2
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_that_its_comparable
|
33
|
+
card1 = Hands::Card.new(value: 2, suit: :hearts)
|
34
|
+
card2 = Hands::Card.new(value: 3, suit: :clubs)
|
35
|
+
|
36
|
+
assert card2 > card1
|
37
|
+
assert card1 < card2
|
38
|
+
|
39
|
+
card1.value = 3
|
40
|
+
assert card1 == card2
|
41
|
+
assert_equal 1, card1.<=>(card2, true)
|
42
|
+
assert_equal -1, card2.<=>(card1, true)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_that_it_is_sortable
|
46
|
+
c2 = Hands::Card.new(value: 2, suit: :hearts)
|
47
|
+
c3 = Hands::Card.new(value: 3, suit: :hearts)
|
48
|
+
c4 = Hands::Card.new(value: 4, suit: :hearts)
|
49
|
+
c5 = Hands::Card.new(value: 5, suit: :hearts)
|
50
|
+
c6 = Hands::Card.new(value: 6, suit: :hearts)
|
51
|
+
c7 = Hands::Card.new(value: 7, suit: :hearts)
|
52
|
+
c8 = Hands::Card.new(value: 8, suit: :hearts)
|
53
|
+
c9 = Hands::Card.new(value: 9, suit: :hearts)
|
54
|
+
c10 = Hands::Card.new(value: 10, suit: :hearts)
|
55
|
+
cJ = Hands::Card.new(value: 'j', suit: :hearts)
|
56
|
+
cQ = Hands::Card.new(value: 'q', suit: :hearts)
|
57
|
+
cK = Hands::Card.new(value: 'k', suit: :hearts)
|
58
|
+
cA = Hands::Card.new(value: 'a', suit: :hearts)
|
59
|
+
|
60
|
+
cards = [c2, c3, c4, c5, c6, c7, c8, c9, c10, cJ, cQ, cK, cA]
|
61
|
+
assert_equal cards, cards.sort
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_that_it_includes_the_description_in_inspect
|
65
|
+
card = Hands::Card.new(value: 2, suit: :hearts)
|
66
|
+
assert_includes card.inspect, 'Two of Hearts'
|
67
|
+
|
68
|
+
card = Hands::Card.new
|
69
|
+
assert_equal 'invalid', card.description
|
70
|
+
refute_includes card.inspect, 'invalid'
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class DeckTest < Hands::TestCase
|
4
|
+
def test_shuffle
|
5
|
+
deck = Hands::Deck.new
|
6
|
+
cards = deck.cards.collect(&:description)
|
7
|
+
deck.shuffle!
|
8
|
+
refute_equal cards, deck.cards.collect(&:description)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_popping
|
12
|
+
deck = Hands::Deck.new
|
13
|
+
card = deck.pop
|
14
|
+
refute_includes deck.cards, card
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class HandDetectionTest < Hands::TestCase
|
4
|
+
|
5
|
+
def test_best_hand
|
6
|
+
hand = Hands::Hand.new
|
7
|
+
hand << Hands::Card[7, :hearts]
|
8
|
+
hand << Hands::Card[7, :spades]
|
9
|
+
hand << Hands::Card[4, :diamonds]
|
10
|
+
hand << Hands::Card[9, :hearts]
|
11
|
+
hand << Hands::Card[9, :clubs]
|
12
|
+
assert_equal 'two_pair', hand.best_hand[:type]
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
#### High Card
|
17
|
+
|
18
|
+
def test_high_card
|
19
|
+
hand = Hands::Hand.new
|
20
|
+
hand << Hands::Card[2, :hearts]
|
21
|
+
hand << Hands::Card[9, :clubs]
|
22
|
+
hand << Hands::Card[7, :hearts]
|
23
|
+
hand << Hands::Card['a', :spades]
|
24
|
+
hand << Hands::Card[4, :diamonds]
|
25
|
+
assert_equal %w{a 9 7 4 2}, hand.high_card.collect(&:value)
|
26
|
+
refute hand.pair
|
27
|
+
refute hand.two_pair
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
#### Pair
|
32
|
+
|
33
|
+
def test_pair
|
34
|
+
hand = Hands::Hand.new
|
35
|
+
hand << Hands::Card[9, :hearts]
|
36
|
+
hand << Hands::Card[9, :clubs]
|
37
|
+
hand << Hands::Card[7, :hearts]
|
38
|
+
hand << Hands::Card[2, :spades]
|
39
|
+
hand << Hands::Card[4, :diamonds]
|
40
|
+
assert_equal %w{9 9 7 4 2}, hand.pair.collect(&:value)
|
41
|
+
refute hand.two_pair
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
#### Two Pair
|
46
|
+
|
47
|
+
def test_two_pair
|
48
|
+
hand = Hands::Hand.new
|
49
|
+
hand << Hands::Card[7, :hearts]
|
50
|
+
hand << Hands::Card[7, :spades]
|
51
|
+
hand << Hands::Card[4, :diamonds]
|
52
|
+
hand << Hands::Card[9, :hearts]
|
53
|
+
hand << Hands::Card[9, :clubs]
|
54
|
+
assert_equal %w{9 9 7 7 4}, hand.two_pair.collect(&:value)
|
55
|
+
assert_equal hand.pair, hand.two_pair
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
#### Three of a Kind
|
60
|
+
|
61
|
+
def test_three_of_a_kind
|
62
|
+
hand = Hands::Hand.new
|
63
|
+
hand << Hands::Card[7, :hearts]
|
64
|
+
hand << Hands::Card[7, :spades]
|
65
|
+
hand << Hands::Card[7, :diamonds]
|
66
|
+
hand << Hands::Card[3, :hearts]
|
67
|
+
hand << Hands::Card[9, :clubs]
|
68
|
+
assert_equal 'three_of_a_kind', hand.best_hand[:type]
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
#### Straight
|
73
|
+
|
74
|
+
def test_straight
|
75
|
+
hand = Hands::Hand.new
|
76
|
+
hand << Hands::Card[2, :hearts]
|
77
|
+
hand << Hands::Card[3, :spades]
|
78
|
+
hand << Hands::Card[4, :diamonds]
|
79
|
+
hand << Hands::Card[5, :hearts]
|
80
|
+
hand << Hands::Card[6, :clubs]
|
81
|
+
assert_equal 'straight', hand.best_hand[:type]
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_straight_when_ace_is_low
|
85
|
+
hand = Hands::Hand.new
|
86
|
+
hand << Hands::Card['A', :hearts]
|
87
|
+
hand << Hands::Card[2, :spades]
|
88
|
+
hand << Hands::Card[3, :diamonds]
|
89
|
+
hand << Hands::Card[4, :hearts]
|
90
|
+
hand << Hands::Card[5, :clubs]
|
91
|
+
assert_equal 'straight', hand.best_hand[:type]
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_straight_when_ace_is_high
|
95
|
+
hand = Hands::Hand.new
|
96
|
+
hand << Hands::Card[10, :hearts]
|
97
|
+
hand << Hands::Card['J', :spades]
|
98
|
+
hand << Hands::Card['Q', :diamonds]
|
99
|
+
hand << Hands::Card['K', :hearts]
|
100
|
+
hand << Hands::Card['A', :clubs]
|
101
|
+
assert_equal 'straight', hand.best_hand[:type]
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_straights_dont_wrap_around
|
105
|
+
hand = Hands::Hand.new
|
106
|
+
hand << Hands::Card['J', :spades]
|
107
|
+
hand << Hands::Card['Q', :diamonds]
|
108
|
+
hand << Hands::Card['K', :hearts]
|
109
|
+
hand << Hands::Card['A', :clubs]
|
110
|
+
hand << Hands::Card[2, :hearts]
|
111
|
+
assert_equal 'high_card', hand.best_hand[:type]
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
#### Flush
|
116
|
+
|
117
|
+
def test_flush
|
118
|
+
hand = Hands::Hand.new
|
119
|
+
hand << Hands::Card[6, :hearts]
|
120
|
+
hand << Hands::Card[7, :hearts]
|
121
|
+
hand << Hands::Card[8, :hearts]
|
122
|
+
hand << Hands::Card[2, :hearts]
|
123
|
+
hand << Hands::Card[4, :hearts]
|
124
|
+
assert_equal 'flush', hand.best_hand[:type]
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
#### Full House
|
129
|
+
|
130
|
+
def test_full_house
|
131
|
+
hand = Hands::Hand.new
|
132
|
+
hand << Hands::Card[7, :hearts]
|
133
|
+
hand << Hands::Card[7, :spades]
|
134
|
+
hand << Hands::Card[7, :diamonds]
|
135
|
+
hand << Hands::Card[9, :spades]
|
136
|
+
hand << Hands::Card[9, :clubs]
|
137
|
+
assert_equal 'full_house', hand.best_hand[:type]
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
#### Four of a Kind
|
142
|
+
|
143
|
+
def test_four_of_a_kind
|
144
|
+
hand = Hands::Hand.new
|
145
|
+
hand << Hands::Card[7, :hearts]
|
146
|
+
hand << Hands::Card[7, :spades]
|
147
|
+
hand << Hands::Card[7, :diamonds]
|
148
|
+
hand << Hands::Card[7, :clubs]
|
149
|
+
hand << Hands::Card[9, :clubs]
|
150
|
+
assert_equal 'four_of_a_kind', hand.best_hand[:type]
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
#### Straight Flush
|
155
|
+
|
156
|
+
def test_straight_flush
|
157
|
+
hand = Hands::Hand.new
|
158
|
+
hand << Hands::Card[2, :hearts]
|
159
|
+
hand << Hands::Card[3, :hearts]
|
160
|
+
hand << Hands::Card[4, :hearts]
|
161
|
+
hand << Hands::Card[5, :hearts]
|
162
|
+
hand << Hands::Card[6, :hearts]
|
163
|
+
assert_equal 'straight_flush', hand.best_hand[:type]
|
164
|
+
end
|
165
|
+
end
|