rholdem 0.0.1
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/History.txt +3 -0
- data/Manifest.txt +16 -0
- data/README.txt +117 -0
- data/Rakefile +35 -0
- data/lib/card.rb +77 -0
- data/lib/deck.rb +26 -0
- data/lib/game.rb +239 -0
- data/lib/hand.rb +236 -0
- data/lib/player.rb +56 -0
- data/lib/rholdem.rb +9 -0
- data/spec/card_spec.rb +95 -0
- data/spec/deck_spec.rb +53 -0
- data/spec/game_spec.rb +565 -0
- data/spec/hand_spec.rb +350 -0
- data/spec/player_spec.rb +103 -0
- data/spec/rholdem_spec.rb +24 -0
- metadata +72 -0
data/lib/hand.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/deck"
|
2
|
+
|
3
|
+
module Holdem
|
4
|
+
class Hand
|
5
|
+
RANKINGS = [
|
6
|
+
:straight_flush,
|
7
|
+
:four_of_a_kind,
|
8
|
+
:full_house,
|
9
|
+
:flush,
|
10
|
+
:straight,
|
11
|
+
:three_of_a_kind,
|
12
|
+
:two_pair,
|
13
|
+
:pair,
|
14
|
+
:high_card
|
15
|
+
]
|
16
|
+
|
17
|
+
attr_reader :cards, :sorted_cards, :ranking, :high_card
|
18
|
+
|
19
|
+
def initialize(*cards)
|
20
|
+
@cards = []
|
21
|
+
if(cards.length == 1 && cards[0].is_a?(String))
|
22
|
+
# a string to split up, Card class takes care of raising errors.
|
23
|
+
cards[0].split(' ').map { |c| @cards << Card.new(c) }
|
24
|
+
else
|
25
|
+
# an array of cards, need to verify class.
|
26
|
+
cards = cards[0] if cards.length == 1 && cards[0].is_a?(Array)
|
27
|
+
cards.map { |c| c.is_a?(Card) ? @cards << c :
|
28
|
+
raise("Cannot initialize Hand from #{c.class} objects") }
|
29
|
+
end
|
30
|
+
|
31
|
+
unless (2..7).include? @cards.length
|
32
|
+
raise "#{@cards.length} cards given (2 to 7 cards required)"
|
33
|
+
end
|
34
|
+
|
35
|
+
@cards.sort!
|
36
|
+
|
37
|
+
find_best(@cards)
|
38
|
+
@high_card = @sorted_cards[@sorted_cards.length-1]
|
39
|
+
end
|
40
|
+
|
41
|
+
def >(compare_to)
|
42
|
+
return true if (self <=> compare_to) == 1
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
def <(compare_to)
|
47
|
+
return true if (self <=> compare_to) == -1
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def ==(compare_to)
|
52
|
+
return true if (self <=> compare_to) == 0
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def <=>(compare_to)
|
57
|
+
# must have 5 sorted cards or be same length.
|
58
|
+
if(@sorted_cards.length != 5 && compare_to.sorted_cards.length != 5)
|
59
|
+
unless @sorted_cards.length == compare_to.sorted_cards.length
|
60
|
+
raise "Cannot compare mismatched, less then 5 card hand lengths"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
my_rank = RANKINGS.index(@ranking)
|
65
|
+
comp_rank = RANKINGS.index(compare_to.ranking)
|
66
|
+
|
67
|
+
return 1 if my_rank < comp_rank
|
68
|
+
return -1 if my_rank > comp_rank
|
69
|
+
|
70
|
+
# otherwise must compare by cards.
|
71
|
+
i = @sorted_cards.length-1
|
72
|
+
while(i >= 0)
|
73
|
+
return 1 if @sorted_cards[i] > compare_to.sorted_cards[i]
|
74
|
+
return -1 if @sorted_cards[i] < compare_to.sorted_cards[i]
|
75
|
+
i -= 1
|
76
|
+
end
|
77
|
+
|
78
|
+
return 0
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def find_best(cards)
|
84
|
+
case cards.length
|
85
|
+
|
86
|
+
when 7
|
87
|
+
(0..6).each do |i|
|
88
|
+
copy = cards.dup
|
89
|
+
copy.delete_at(i)
|
90
|
+
find_best(copy)
|
91
|
+
end
|
92
|
+
|
93
|
+
when 6
|
94
|
+
(0..5).each do |i|
|
95
|
+
copy = cards.dup
|
96
|
+
copy.delete_at(i)
|
97
|
+
find_best(copy)
|
98
|
+
end
|
99
|
+
|
100
|
+
else
|
101
|
+
# testing a hand of 5 or less
|
102
|
+
new_rank = find_ranking(cards)
|
103
|
+
cards = sort_by_ranking(cards, new_rank)
|
104
|
+
|
105
|
+
if(@ranking.nil? || RANKINGS.index(new_rank) < RANKINGS.index(@ranking))
|
106
|
+
update_ranking(new_rank, cards)
|
107
|
+
elsif(RANKINGS.index(new_rank) == RANKINGS.index(@ranking))
|
108
|
+
(0...cards.length).each do |i|
|
109
|
+
if(cards[i].rank > @sorted_cards[i].rank)
|
110
|
+
update_ranking(new_rank, cards)
|
111
|
+
break
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def find_ranking(cards)
|
119
|
+
RANKINGS.each do |r|
|
120
|
+
return r if method(r.to_s + '?').call(cards)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def sort_by_ranking(cards, ranking)
|
125
|
+
case ranking
|
126
|
+
|
127
|
+
when :straight_flush, :straight
|
128
|
+
if(cards[4].rank - cards[0].rank == 12)
|
129
|
+
card = cards.delete_at(4)
|
130
|
+
cards.insert(0, card)
|
131
|
+
end
|
132
|
+
|
133
|
+
when :four_of_a_kind, :full_house
|
134
|
+
over_rank = cards[2].rank
|
135
|
+
cards = cards.sort_by do |card|
|
136
|
+
card.rank == over_rank ? 1 : 0
|
137
|
+
end
|
138
|
+
|
139
|
+
when :three_of_a_kind
|
140
|
+
trips_rank = cards[2].rank
|
141
|
+
cards = cards.sort_by do |card|
|
142
|
+
card.rank == trips_rank ? 100 : card.rank
|
143
|
+
end
|
144
|
+
|
145
|
+
when :two_pair, :pair
|
146
|
+
rank_counts = Hash.new
|
147
|
+
cards.each do |c|
|
148
|
+
rank_counts[c.rank] ||= 0
|
149
|
+
rank_counts[c.rank] += 1
|
150
|
+
end
|
151
|
+
|
152
|
+
cards = cards.sort_by do |card|
|
153
|
+
rank_counts[card.rank] > 1 ? card.rank+100 : card.rank
|
154
|
+
end
|
155
|
+
end
|
156
|
+
cards
|
157
|
+
end
|
158
|
+
|
159
|
+
def update_ranking(ranking, cards)
|
160
|
+
@sorted_cards = cards
|
161
|
+
@ranking = ranking
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Evaluators
|
166
|
+
#
|
167
|
+
|
168
|
+
def straight_flush?(cards)
|
169
|
+
return false if cards.length != 5
|
170
|
+
return true if (straight?(cards) && flush?(cards))
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
174
|
+
def four_of_a_kind?(cards)
|
175
|
+
return false unless (4..5).include?(cards.length)
|
176
|
+
return true if cards.join(' ') =~ /(.). \1. \1. \1./
|
177
|
+
false
|
178
|
+
end
|
179
|
+
|
180
|
+
def full_house?(cards)
|
181
|
+
return false if cards.length != 5
|
182
|
+
return true if cards.join(' ') =~ /(.). \1. \1. (.). \2./
|
183
|
+
return true if cards.join(' ') =~ /(.). \1. (.). \2. \2./
|
184
|
+
|
185
|
+
false
|
186
|
+
end
|
187
|
+
|
188
|
+
def flush?(cards)
|
189
|
+
return false if cards.length != 5
|
190
|
+
return true if cards.join(' ') =~ /(.)(.) (.)\2 (.)\2 (.)\2 (.)\2/
|
191
|
+
|
192
|
+
false
|
193
|
+
end
|
194
|
+
|
195
|
+
def straight?(cards)
|
196
|
+
return false if cards.length != 5
|
197
|
+
|
198
|
+
ranks = cards.map { |card| card.rank }
|
199
|
+
return true if ranks == [0, 1, 2, 3, 12]
|
200
|
+
|
201
|
+
result = true
|
202
|
+
(0..3).each do |i|
|
203
|
+
if(ranks[i+1] - ranks[i] != 1)
|
204
|
+
result = false
|
205
|
+
break
|
206
|
+
end
|
207
|
+
end
|
208
|
+
result
|
209
|
+
end
|
210
|
+
|
211
|
+
def three_of_a_kind?(cards)
|
212
|
+
return false unless (3..5).include?(cards.length)
|
213
|
+
return true if cards.join(' ') =~ /(.). \1. \1./
|
214
|
+
|
215
|
+
false
|
216
|
+
end
|
217
|
+
|
218
|
+
def two_pair?(cards)
|
219
|
+
return false unless (4..5).include?(cards.length)
|
220
|
+
return true if cards.join(' ') =~ /(.). \1. (.). \2./
|
221
|
+
|
222
|
+
false
|
223
|
+
end
|
224
|
+
|
225
|
+
def pair?(cards)
|
226
|
+
return false unless (2..5).include?(cards.length)
|
227
|
+
return true if cards.join(' ') =~ /(.). \1./
|
228
|
+
|
229
|
+
false
|
230
|
+
end
|
231
|
+
|
232
|
+
def high_card?(cards)
|
233
|
+
true
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
data/lib/player.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/hand"
|
2
|
+
|
3
|
+
module Holdem
|
4
|
+
class Player
|
5
|
+
MAX_ACTIONS = 300
|
6
|
+
|
7
|
+
attr_reader :hole_cards, :hand, :in_pot, :actions, :in_round, :folded, :delta
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@hole_cards = []
|
11
|
+
@actions = []
|
12
|
+
@in_pot = 0.0
|
13
|
+
@in_round = 0.0
|
14
|
+
@delta = 0.0
|
15
|
+
@folded = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def deal(card)
|
19
|
+
raise "Player already has 2 hole cards" if @hole_cards.length >= 2
|
20
|
+
@hole_cards << card
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_hand(board)
|
24
|
+
all_cards = (board + @hole_cards).flatten
|
25
|
+
@hand = Hand.new(all_cards)
|
26
|
+
end
|
27
|
+
|
28
|
+
def pay(amount)
|
29
|
+
@in_pot += amount
|
30
|
+
@in_round += amount
|
31
|
+
@delta -= amount
|
32
|
+
end
|
33
|
+
|
34
|
+
def award(amount)
|
35
|
+
@delta += amount
|
36
|
+
end
|
37
|
+
|
38
|
+
# override this for subclass Players
|
39
|
+
def act(game)
|
40
|
+
return :neutral
|
41
|
+
end
|
42
|
+
|
43
|
+
def record_action(action)
|
44
|
+
@actions << action
|
45
|
+
@actions.delete_at(0) if @actions.length > MAX_ACTIONS
|
46
|
+
end
|
47
|
+
|
48
|
+
def next_round
|
49
|
+
@in_round = 0.0
|
50
|
+
end
|
51
|
+
|
52
|
+
def fold
|
53
|
+
@folded = true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/rholdem.rb
ADDED
data/spec/card_spec.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../lib/card"
|
2
|
+
include Holdem
|
3
|
+
|
4
|
+
describe Card do
|
5
|
+
before(:each) do
|
6
|
+
@c = Card.new('As')
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be creatable from a 2-char string" do
|
10
|
+
@c.should_not be_nil
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should assign the rank when created" do
|
14
|
+
# RANKS = "23456789TJQKA"
|
15
|
+
@c.rank.should == 12
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should assign the suit when created" do
|
19
|
+
# SUITS = "cdhs"
|
20
|
+
@c.suit.should == 3
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should output the abbreviated card name" do
|
24
|
+
@c.to_s.should == 'As'
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should show error for bad card initializers" do
|
28
|
+
lambda { Card.new('Za') }.should raise_error
|
29
|
+
lambda { Card.new('0') }.should raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should evaluate two cards as equal if their ranks and suits match" do
|
33
|
+
c1 = Card.new('3c')
|
34
|
+
c2 = Card.new('3c')
|
35
|
+
|
36
|
+
c1.should == c2
|
37
|
+
c1.should eql?(c2)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should evaluate two cards as equal if their ranks match" do
|
41
|
+
c1 = Card.new('3d')
|
42
|
+
c2 = Card.new('3c')
|
43
|
+
|
44
|
+
c1.should == c2
|
45
|
+
c1.should eql?(c2)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should evaluate two cards as not equal if their ranks don't match" do
|
49
|
+
c1 = Card.new('3h')
|
50
|
+
c2 = Card.new('4h')
|
51
|
+
c1.should_not == c2
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should evaluate two cards as === if their suits and ranks match" do
|
55
|
+
c1 = Card.new('3d')
|
56
|
+
c2 = Card.new('3d')
|
57
|
+
c1.should === c2
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should evaluate two cards as not === if their suits don't match" do
|
61
|
+
c1 = Card.new('3d')
|
62
|
+
c2 = Card.new('3h')
|
63
|
+
c1.should_not === c2
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should evaluate two cards as not === if their ranks don't match" do
|
67
|
+
c1 = Card.new('5d')
|
68
|
+
c2 = Card.new('3d')
|
69
|
+
c1.should_not === c2
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should return -1 when comparing lower to higher card" do
|
73
|
+
c1 = Card.new('3c')
|
74
|
+
c2 = Card.new('4d')
|
75
|
+
(c1 <=> c2).should == -1
|
76
|
+
(c1 > c2).should == false
|
77
|
+
(c1 < c2).should == true
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should return 1 when comparing higher to lower card" do
|
81
|
+
c1 = Card.new('8c')
|
82
|
+
c2 = Card.new('4d')
|
83
|
+
(c1 <=> c2).should == 1
|
84
|
+
(c1 > c2).should == true
|
85
|
+
(c1 < c2).should == false
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should return 0 when cards are equal" do
|
89
|
+
c1 = Card.new('8c')
|
90
|
+
c2 = Card.new('8d')
|
91
|
+
(c1 <=> c2).should == 0
|
92
|
+
(c1 > c2).should == false
|
93
|
+
(c1 < c2).should == false
|
94
|
+
end
|
95
|
+
end
|
data/spec/deck_spec.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../lib/deck"
|
2
|
+
include Holdem
|
3
|
+
|
4
|
+
describe Deck do
|
5
|
+
before(:each) do
|
6
|
+
@d = Deck.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should have all 52 cards" do
|
10
|
+
@d.cards.length.should == 52
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should not have repeated cards" do
|
14
|
+
cards = Hash.new
|
15
|
+
@d.cards.each do |c|
|
16
|
+
cards[c].should be_nil
|
17
|
+
cards[c] = 1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should rearrange the cards after a shuffle" do
|
22
|
+
old_sort = @d.cards.dup
|
23
|
+
@d.shuffle
|
24
|
+
old_sort.should_not == @d.cards
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should be shuffled when created" do
|
28
|
+
ordered_cards = []
|
29
|
+
Card::SUITS.each_byte do |suit|
|
30
|
+
Card::RANKS.each_byte do |rank|
|
31
|
+
ordered_cards << Card.new(rank.chr + suit.chr)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@d.cards.should_not == ordered_cards
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should return a card from the deck when dealing" do
|
39
|
+
@d.deal.class.should == Card
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should remove the card from the deck when dealing" do
|
43
|
+
c = @d.deal
|
44
|
+
@d.cards.each do |card|
|
45
|
+
card.should_not === c
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should have 1 less card after dealing" do
|
50
|
+
@d.deal
|
51
|
+
@d.cards.length.should == 51
|
52
|
+
end
|
53
|
+
end
|