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