acpc_poker_types 5.0.3 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +1 -1
- data/acpc_poker_types.gemspec +3 -2
- data/lib/acpc_poker_types.rb +1 -1
- data/lib/acpc_poker_types/acpc_dealer_data/hand_results.rb +5 -5
- data/lib/acpc_poker_types/acpc_dealer_data/poker_match_data.rb +290 -323
- data/lib/acpc_poker_types/game_definition.rb +54 -37
- data/lib/acpc_poker_types/hand_player.rb +119 -0
- data/lib/acpc_poker_types/match_state.rb +323 -204
- data/lib/acpc_poker_types/player_group.rb +62 -0
- data/lib/acpc_poker_types/poker_action.rb +13 -6
- data/lib/acpc_poker_types/version.rb +1 -1
- data/spec/game_definition_spec.rb +23 -4
- data/spec/hand_player_spec.rb +254 -0
- data/spec/match_state_spec.rb +655 -112
- data/spec/player_group_spec.rb +57 -0
- data/spec/poker_match_data_spec.rb +24 -21
- data/spec/support/spec_helper.rb +2 -16
- metadata +68 -148
- data/lib/acpc_poker_types/player.rb +0 -182
- data/spec/coverage/assets/0.7.1/application.css +0 -1110
- data/spec/coverage/assets/0.7.1/application.js +0 -626
- data/spec/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
- data/spec/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
- data/spec/coverage/assets/0.7.1/favicon_green.png +0 -0
- data/spec/coverage/assets/0.7.1/favicon_red.png +0 -0
- data/spec/coverage/assets/0.7.1/favicon_yellow.png +0 -0
- data/spec/coverage/assets/0.7.1/loading.gif +0 -0
- data/spec/coverage/assets/0.7.1/magnify.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/spec/coverage/index.html +0 -72
- data/spec/match_state_perf.rb +0 -14
- data/spec/player_spec.rb +0 -372
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'set'
|
2
|
-
|
3
1
|
require 'acpc_poker_types/chip_stack'
|
4
2
|
require 'acpc_poker_types/suit'
|
5
3
|
require 'acpc_poker_types/rank'
|
@@ -28,9 +26,6 @@ module AcpcPokerTypes
|
|
28
26
|
# number_of_board_cards == [0, 3, 1, 1]
|
29
27
|
attr_reader :number_of_board_cards
|
30
28
|
|
31
|
-
# @return [Array] The minimum wager in each round.
|
32
|
-
attr_reader :min_wagers
|
33
|
-
|
34
29
|
# @return [Array] The position relative to the dealer that is first to act
|
35
30
|
# in each round, indexed from 0.
|
36
31
|
# @example The usual Texas hold'em sequence would look like this:
|
@@ -99,14 +94,6 @@ module AcpcPokerTypes
|
|
99
94
|
:@number_of_board_cards => 'numBoardCards'
|
100
95
|
}
|
101
96
|
|
102
|
-
ALL_PLAYER_ALL_ROUND_DEFS = [
|
103
|
-
DEFINITIONS[:@number_of_players],
|
104
|
-
DEFINITIONS[:@number_of_rounds],
|
105
|
-
DEFINITIONS[:@number_of_suits],
|
106
|
-
DEFINITIONS[:@number_of_ranks],
|
107
|
-
DEFINITIONS[:@number_of_hole_cards]
|
108
|
-
]
|
109
|
-
|
110
97
|
def self.default_first_player_positions(number_of_rounds)
|
111
98
|
number_of_rounds.to_i.times.inject([]) do |list, i|
|
112
99
|
list << 0
|
@@ -128,6 +115,12 @@ module AcpcPokerTypes
|
|
128
115
|
end
|
129
116
|
end
|
130
117
|
|
118
|
+
# @param [Array] array The array to flatten into a single element.
|
119
|
+
# @return +array+ if +array+ has more than one element, the single element in +array+ otherwise.
|
120
|
+
def self.flatten_if_single_element_array(array)
|
121
|
+
if 1 == array.length then array[0] else array end
|
122
|
+
end
|
123
|
+
|
131
124
|
# Checks if the given line is a comment beginning with '#' or ';', or empty.
|
132
125
|
#
|
133
126
|
# @param [String] line
|
@@ -136,7 +129,7 @@ module AcpcPokerTypes
|
|
136
129
|
line.nil? || line.match(/^\s*[#;]/) || line.empty?
|
137
130
|
end
|
138
131
|
|
139
|
-
# Checks a given line
|
132
|
+
# Checks a given line from a game definition file for a game
|
140
133
|
# definition name and returns the given default value unless there is a match.
|
141
134
|
#
|
142
135
|
# @param [String, #match] line A line from a game definition file.
|
@@ -146,12 +139,11 @@ module AcpcPokerTypes
|
|
146
139
|
# referred to by +definition_name+, +nil+ otherwise.
|
147
140
|
def self.check_game_def_line_for_definition(line, definition_name)
|
148
141
|
if line.match(/^\s*#{definition_name}\s*=\s*([\d\s]+)/i)
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
else
|
153
|
-
value
|
142
|
+
values = $1.chomp.split(/\s+/)
|
143
|
+
(0..values.length-1).each do |i|
|
144
|
+
values[i] = values[i].to_i
|
154
145
|
end
|
146
|
+
flatten_if_single_element_array values
|
155
147
|
end
|
156
148
|
end
|
157
149
|
|
@@ -174,7 +166,12 @@ module AcpcPokerTypes
|
|
174
166
|
|
175
167
|
def initialize(definitions)
|
176
168
|
initialize_members!
|
177
|
-
|
169
|
+
|
170
|
+
if definitions.is_a?(Hash)
|
171
|
+
assign_definitions! definitions
|
172
|
+
else
|
173
|
+
parse_definitions! definitions
|
174
|
+
end
|
178
175
|
|
179
176
|
@chip_stacks = AcpcPokerTypes::GameDefinition.default_chip_stacks(@number_of_players) if @chip_stacks.empty?
|
180
177
|
|
@@ -191,29 +188,38 @@ module AcpcPokerTypes
|
|
191
188
|
|
192
189
|
alias_method :to_str, :to_s
|
193
190
|
|
191
|
+
def to_h
|
192
|
+
@hash ||= DEFINITIONS.keys.inject({betting_type: @betting_type}) do |h, instance_variable_symbol|
|
193
|
+
h[instance_variable_symbol[1..-1].to_sym] = instance_variable_get(instance_variable_symbol)
|
194
|
+
h
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
194
198
|
def to_a
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
199
|
+
@array ||= -> do
|
200
|
+
list_of_lines = []
|
201
|
+
list_of_lines << BETTING_TYPES[@betting_type] if @betting_type
|
202
|
+
list_of_lines << "stack = #{@chip_stacks.join(' ')}" unless @chip_stacks.empty?
|
203
|
+
list_of_lines << "numPlayers = #{@number_of_players}" if @number_of_players
|
204
|
+
list_of_lines << "blind = #{@blinds.join(' ')}" unless @blinds.empty?
|
205
|
+
list_of_lines << "raiseSize = #{min_wagers.join(' ')}" unless min_wagers.empty?
|
206
|
+
list_of_lines << "numRounds = #{@number_of_rounds}" if @number_of_rounds
|
207
|
+
list_of_lines << "firstPlayer = #{(@first_player_positions.map{|p| p + 1}).join(' ')}" unless @first_player_positions.empty?
|
208
|
+
list_of_lines << "maxRaises = #{@max_number_of_wagers.join(' ')}" unless @max_number_of_wagers.empty?
|
209
|
+
list_of_lines << "numSuits = #{@number_of_suits}" if @number_of_suits
|
210
|
+
list_of_lines << "numRanks = #{@number_of_ranks}" if @number_of_ranks
|
211
|
+
list_of_lines << "numHoleCards = #{@number_of_hole_cards}" if @number_of_hole_cards
|
212
|
+
list_of_lines << "numBoardCards = #{@number_of_board_cards.join(' ')}" unless @number_of_board_cards.empty?
|
213
|
+
list_of_lines
|
214
|
+
end.call
|
209
215
|
end
|
210
216
|
|
211
217
|
def ==(other_game_definition)
|
212
|
-
|
218
|
+
to_h == other_game_definition.to_h
|
213
219
|
end
|
214
220
|
|
215
221
|
def min_wagers
|
216
|
-
if @raise_sizes
|
222
|
+
@min_wagers ||= if @raise_sizes
|
217
223
|
@raise_sizes
|
218
224
|
else
|
219
225
|
@number_of_rounds.times.map { |i| @blinds.max }
|
@@ -251,6 +257,12 @@ module AcpcPokerTypes
|
|
251
257
|
end
|
252
258
|
end
|
253
259
|
|
260
|
+
def assign_definitions!(definitions)
|
261
|
+
definitions.each do |key, value|
|
262
|
+
instance_variable_set("@#{key}", value)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
254
266
|
def parse_definitions!(definitions)
|
255
267
|
definitions.each do |line|
|
256
268
|
break if line.match(/\bend\s*gamedef\b/i)
|
@@ -308,7 +320,7 @@ module AcpcPokerTypes
|
|
308
320
|
unless Seat.in_bounds?(@first_player_positions[i], @number_of_players)
|
309
321
|
raise(
|
310
322
|
ParseError,
|
311
|
-
"Invalid first player #{@first_player_positions[i]} on round #{i+1}"
|
323
|
+
"Invalid first player #{@first_player_positions[i]} on round #{i+1} for #{@number_of_players} players"
|
312
324
|
)
|
313
325
|
end
|
314
326
|
end
|
@@ -333,6 +345,11 @@ module AcpcPokerTypes
|
|
333
345
|
end
|
334
346
|
|
335
347
|
def adjust_definitions_if_necessary!
|
348
|
+
if @number_of_players < @chip_stacks.length
|
349
|
+
@number_of_players = @chip_stacks.length
|
350
|
+
elsif @number_of_players < @blinds.length
|
351
|
+
@number_of_players = @blinds.length
|
352
|
+
end
|
336
353
|
@number_of_players.times do |i|
|
337
354
|
@blinds << 0 unless @blinds.length > i
|
338
355
|
@chip_stacks << DEFAULT_CHIP_STACK unless @chip_stacks.length > i
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'acpc_poker_types/chip_stack'
|
2
|
+
require 'acpc_poker_types/hand'
|
3
|
+
|
4
|
+
require 'contextual_exceptions'
|
5
|
+
using ContextualExceptions::ClassRefinement
|
6
|
+
|
7
|
+
module AcpcPokerTypes
|
8
|
+
|
9
|
+
# Class to model a player during a hand from information in a +MatchState+
|
10
|
+
class HandPlayer
|
11
|
+
exceptions :unable_to_pay_ante, :inactive
|
12
|
+
|
13
|
+
# @return [AcpcPokerTypes::ChipStack] This player's chip stack at the beginning of the
|
14
|
+
# hand before paying their ante.
|
15
|
+
attr_reader :initial_stack
|
16
|
+
|
17
|
+
# @return [AcpcPokerTypes::ChipStack] The ante this player paid at the beginning of
|
18
|
+
# the hand.
|
19
|
+
attr_reader :ante
|
20
|
+
|
21
|
+
# @return [Hand] This player's hole cards or nil if this player is not
|
22
|
+
# holding cards.
|
23
|
+
attr_reader :hand
|
24
|
+
|
25
|
+
# @return [Array<PokerAction>] The actions this player has taken
|
26
|
+
attr_reader :actions
|
27
|
+
|
28
|
+
attr_accessor :winnings
|
29
|
+
|
30
|
+
# @param hand [Hand]
|
31
|
+
# @param initial_chip_stack [#to_i]
|
32
|
+
# @param ante [#to_i]
|
33
|
+
def initialize(hand, initial_stack, ante)
|
34
|
+
raise UnableToPayAnte if ante > initial_stack
|
35
|
+
|
36
|
+
@hand = hand
|
37
|
+
@initial_stack = ChipStack.new initial_stack
|
38
|
+
@ante = ChipStack.new ante
|
39
|
+
@actions = [[]]
|
40
|
+
@winnings = ChipStack.new 0
|
41
|
+
end
|
42
|
+
|
43
|
+
def stack
|
44
|
+
@initial_stack + balance
|
45
|
+
end
|
46
|
+
|
47
|
+
def balance
|
48
|
+
@winnings - total_contribution
|
49
|
+
end
|
50
|
+
|
51
|
+
def contributions
|
52
|
+
@actions.map do |actions_per_round|
|
53
|
+
actions_per_round.inject(0) { |sum, action| sum += action.cost }
|
54
|
+
end.unshift @ante
|
55
|
+
end
|
56
|
+
|
57
|
+
def total_contribution
|
58
|
+
@ante + @actions.flatten.inject(0) { |sum, action| sum += action.cost }
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param amount_to_call [#to_r] The amount to call for this player
|
62
|
+
# @param wager_illegal [Boolean]
|
63
|
+
# @return [Array<PokerAction>] The list of legal actions for this player. If a wager is legal,
|
64
|
+
# the largest possible wager will be returned in the list.
|
65
|
+
def legal_actions(
|
66
|
+
round,
|
67
|
+
amount_to_call: ChipStack.new(0),
|
68
|
+
wager_illegal: false
|
69
|
+
)
|
70
|
+
l_actions = []
|
71
|
+
return l_actions if inactive?
|
72
|
+
|
73
|
+
if amount_to_call.to_r > 0
|
74
|
+
l_actions << PokerAction.new(PokerAction::CALL) << PokerAction.new(PokerAction::FOLD)
|
75
|
+
else
|
76
|
+
l_actions << PokerAction.new(PokerAction::CHECK)
|
77
|
+
end
|
78
|
+
if !wager_illegal && stack > amount_to_call.to_r
|
79
|
+
l_actions << if contributions[round] > 0 || amount_to_call.to_r > 0
|
80
|
+
PokerAction.new(PokerAction::RAISE, cost: stack - amount_to_call.to_r)
|
81
|
+
else
|
82
|
+
PokerAction.new(PokerAction::BET, cost: stack - amount_to_call.to_r)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
l_actions
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param round [Integer] The round in which the largest wager by size is desired.
|
90
|
+
# defaults to +nil+.
|
91
|
+
# @return [ChipStack] The largest wager by size this player has made.
|
92
|
+
# Checks only in the specified +round+ or over the entire hand if round is +nil+.
|
93
|
+
def largest_wager_by(round=nil)
|
94
|
+
# @todo
|
95
|
+
end
|
96
|
+
|
97
|
+
def append_action!(action, round = @actions.length - 1)
|
98
|
+
raise Inactive if inactive?
|
99
|
+
|
100
|
+
@actions[round] ||= []
|
101
|
+
@actions[round] << action
|
102
|
+
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
def inactive?
|
107
|
+
folded? || all_in?
|
108
|
+
end
|
109
|
+
|
110
|
+
def all_in?
|
111
|
+
stack <= 0
|
112
|
+
end
|
113
|
+
|
114
|
+
def folded?
|
115
|
+
@actions.flatten.last == PokerAction::FOLD
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -3,263 +3,382 @@ require 'acpc_poker_types/hand'
|
|
3
3
|
require 'acpc_poker_types/rank'
|
4
4
|
require 'acpc_poker_types/suit'
|
5
5
|
require 'acpc_poker_types/poker_action'
|
6
|
+
require 'acpc_poker_types/hand_player'
|
7
|
+
require 'acpc_poker_types/player_group'
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
+
module AcpcPokerTypes
|
10
|
+
module Indices
|
11
|
+
refine Array do
|
12
|
+
def inject_with_index(init)
|
13
|
+
i = 0
|
14
|
+
inject(init) do |accum, elem|
|
15
|
+
yield accum, elem, i if block_given?
|
16
|
+
i += 1
|
17
|
+
accum
|
18
|
+
end
|
19
|
+
end
|
20
|
+
def indices(elem_to_find=nil)
|
21
|
+
if elem_to_find
|
22
|
+
indices { |elem| elem == elem_to_find }
|
23
|
+
else
|
24
|
+
inject_with_index([]) do |array, elem, i|
|
25
|
+
array << i if yield elem
|
26
|
+
array
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
using AcpcPokerTypes::Indices
|
9
34
|
|
10
|
-
# Model to parse and manage information from a given match state string.
|
11
35
|
module AcpcPokerTypes
|
12
|
-
class MatchState
|
13
|
-
exceptions :incomplete_match_state
|
14
36
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
37
|
+
# Model to parse and manage information from a given match state string.
|
38
|
+
class MatchState
|
39
|
+
# @return [Integer] The position relative to the dealer of the player that
|
40
|
+
# received the match state string, indexed from 0, modulo the
|
41
|
+
# number of players.
|
42
|
+
# @example The player immediately to the left of the dealer has
|
43
|
+
# +position_relative_to_dealer+ == 0
|
44
|
+
# @example The dealer has
|
45
|
+
# +position_relative_to_dealer+ == <number of players> - 1
|
46
|
+
attr_reader :position_relative_to_dealer
|
23
47
|
|
24
|
-
|
25
|
-
|
48
|
+
# @return [Integer] The hand number.
|
49
|
+
attr_reader :hand_number
|
26
50
|
|
27
|
-
|
28
|
-
|
51
|
+
# @return [String] The ACPC string created by the given betting sequence.
|
52
|
+
attr_reader :betting_sequence_string
|
29
53
|
|
30
|
-
|
31
|
-
attr_reader :list_of_hole_card_hands
|
54
|
+
# @todo Move this @return [Array<Hand>] The list of visible hole card sets for each player.
|
32
55
|
|
33
|
-
|
34
|
-
attr_reader :board_cards
|
56
|
+
attr_reader :hands_string
|
35
57
|
|
36
|
-
|
37
|
-
attr_reader :first_seat_in_each_round
|
58
|
+
# @todo Move this @return [BoardCards] All visible community cards on the board.
|
38
59
|
|
39
|
-
|
40
|
-
LABEL = 'MATCHSTATE'
|
60
|
+
attr_reader :community_cards_string
|
41
61
|
|
42
|
-
|
62
|
+
# @return [String] Label for match state strings.
|
63
|
+
LABEL = 'MATCHSTATE'
|
43
64
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
65
|
+
COMMUNITY_CARD_SEPARATOR = '/'
|
66
|
+
BETTING_SEQUENCE_SEPARATOR = COMMUNITY_CARD_SEPARATOR
|
67
|
+
HAND_SEPARATOR = '|'
|
68
|
+
|
69
|
+
class << self; alias_method(:parse, :new) end
|
70
|
+
|
71
|
+
# Receives a match state string from the given +connection+.
|
72
|
+
# @param [#gets] connection The connection from which a match state string should be received.
|
73
|
+
# @return [MatchState] The match state string that was received from the +connection+ or +nil+ if none could be received.
|
74
|
+
def self.receive(connection)
|
75
|
+
MatchState.parse connection.gets
|
76
|
+
end
|
50
77
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
78
|
+
# Builds a match state string from its given component parts.
|
79
|
+
#
|
80
|
+
# @param [#to_s] position_relative_to_dealer The position relative to the dealer.
|
81
|
+
# @param [#to_s] hand_number The hand number.
|
82
|
+
# @param [#to_s] betting_sequence The betting sequence.
|
83
|
+
# @param [#to_s] all_hole_cards All the hole cards visible.
|
84
|
+
# @param [#to_s, #empty?] board_cards All the community cards on the board.
|
85
|
+
# @return [String] The constructed match state string.
|
86
|
+
def self.build_match_state_string(
|
87
|
+
position_relative_to_dealer,
|
88
|
+
hand_number,
|
89
|
+
betting_sequence,
|
90
|
+
all_hole_cards,
|
91
|
+
board_cards
|
92
|
+
)
|
93
|
+
string = "#{LABEL}:#{position_relative_to_dealer}:#{hand_number}:#{betting_sequence}:#{all_hole_cards}"
|
94
|
+
string << "/#{board_cards.to_s}" if board_cards && !board_cards.empty?
|
95
|
+
string
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param [String] raw_match_state A raw match state string to be parsed.
|
99
|
+
# @raise IncompleteMatchState
|
100
|
+
def initialize(raw_match_state)
|
101
|
+
if raw_match_state.match(
|
102
|
+
/#{LABEL}:(\d+):(\d+):([^:]*):([^#{COMMUNITY_CARD_SEPARATOR}]+)#{COMMUNITY_CARD_SEPARATOR}*([^\s:]*)/
|
65
103
|
)
|
66
|
-
|
67
|
-
|
68
|
-
|
104
|
+
@position_relative_to_dealer = $1.to_i
|
105
|
+
@hand_number = $2.to_i
|
106
|
+
@betting_sequence_string = $3
|
107
|
+
@hands_string = $4
|
108
|
+
@community_cards_string = $5
|
69
109
|
end
|
110
|
+
end
|
70
111
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
112
|
+
# @return [String] The MatchState in raw text form.
|
113
|
+
def to_str
|
114
|
+
@str ||= MatchState.build_match_state_string(
|
115
|
+
@position_relative_to_dealer,
|
116
|
+
@hand_number,
|
117
|
+
@betting_sequence_string,
|
118
|
+
@hands_string,
|
119
|
+
@community_cards_string
|
120
|
+
)
|
121
|
+
end
|
78
122
|
|
79
|
-
|
80
|
-
|
81
|
-
def initialize(raw_match_state)
|
82
|
-
raise IncompleteMatchState, raw_match_state if AcpcPokerTypes::MatchState.line_is_comment_or_empty? raw_match_state
|
83
|
-
|
84
|
-
if raw_match_state.match(
|
85
|
-
/#{LABEL}:(\d+):(\d+):([\d#{AcpcPokerTypes::PokerAction::CONCATONATED_ACTIONS}\/]*):([|#{AcpcPokerTypes::Card::CONCATONATED_CARDS}]+)\/*([\/#{AcpcPokerTypes::Card::CONCATONATED_CARDS}]*)/)
|
86
|
-
@position_relative_to_dealer = $1.to_i
|
87
|
-
@hand_number = $2.to_i
|
88
|
-
@betting_sequence = parse_betting_sequence $3
|
89
|
-
@list_of_hole_card_hands = parse_list_of_hole_card_hands $4
|
90
|
-
@board_cards = parse_board_cards $5
|
91
|
-
end
|
123
|
+
# @see to_str
|
124
|
+
alias_method :to_s, :to_str
|
92
125
|
|
93
|
-
|
94
|
-
|
126
|
+
# @param [MatchState] another_match_state A match state string to compare against this one.
|
127
|
+
# @return [Boolean] +true+ if this match state string is equivalent to +another_match_state+, +false+ otherwise.
|
128
|
+
def ==(another_match_state)
|
129
|
+
another_match_state.to_s == to_s
|
130
|
+
end
|
95
131
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
132
|
+
def all_hands
|
133
|
+
@all_hands ||= -> do
|
134
|
+
lcl_hole_card_hands = all_string_hands(@hands_string).map do |string_hand|
|
135
|
+
Hand.from_acpc string_hand
|
136
|
+
end
|
137
|
+
while lcl_hole_card_hands.length < number_of_players
|
138
|
+
lcl_hole_card_hands.push Hand.new
|
139
|
+
end
|
140
|
+
lcl_hole_card_hands
|
141
|
+
end.call
|
142
|
+
end
|
105
143
|
|
106
|
-
|
107
|
-
|
144
|
+
# @return [Array<Array<PokerAction>>] The sequence of betting actions.
|
145
|
+
def betting_sequence
|
146
|
+
@betting_sequence ||= if @betting_sequence_string.empty?
|
147
|
+
[[]]
|
148
|
+
else
|
149
|
+
lcl_betting_sequence = @betting_sequence_string.split(
|
150
|
+
BETTING_SEQUENCE_SEPARATOR
|
151
|
+
).map do |betting_string_per_round|
|
152
|
+
actions_from_acpc_characters(betting_string_per_round).map do |action|
|
153
|
+
PokerAction.new(action)
|
154
|
+
end
|
155
|
+
end
|
108
156
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
another_match_state.to_s == to_s
|
157
|
+
# Adjust the number of rounds if the last action was the last action in the round
|
158
|
+
lcl_betting_sequence << [] if first_state_of_round?
|
159
|
+
lcl_betting_sequence
|
113
160
|
end
|
161
|
+
end
|
114
162
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
if
|
123
|
-
|
124
|
-
elsif betting_sequence.last.last
|
125
|
-
betting_sequence.last.last
|
126
|
-
else
|
127
|
-
last_action(betting_sequence.reject{ |elem| elem.equal?(betting_sequence.last) })
|
163
|
+
def community_cards
|
164
|
+
@community_cards ||= -> do
|
165
|
+
lcl_community_cards = BoardCards.new(
|
166
|
+
all_sets_of_community_cards(@community_cards_string).map do |cards_in_round|
|
167
|
+
Card.cards(cards_in_round)
|
168
|
+
end
|
169
|
+
)
|
170
|
+
if lcl_community_cards.round < @community_cards_string.count(COMMUNITY_CARD_SEPARATOR)
|
171
|
+
lcl_community_cards.next_round!
|
128
172
|
end
|
129
|
-
|
173
|
+
lcl_community_cards
|
174
|
+
end.call
|
175
|
+
end
|
130
176
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@list_of_hole_card_hands[@position_relative_to_dealer]
|
136
|
-
end
|
177
|
+
# @return [Integer] The zero indexed current round number.
|
178
|
+
def round
|
179
|
+
@round ||= @betting_sequence_string.count BETTING_SEQUENCE_SEPARATOR
|
180
|
+
end
|
137
181
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
local_list_of_hole_card_hands
|
145
|
-
end
|
182
|
+
# @return [Hand] The user's hand.
|
183
|
+
# @example An ace of diamonds and a 4 of clubs is represented as
|
184
|
+
# 'Ad4c'
|
185
|
+
def hand
|
186
|
+
@hand ||= all_hands[@position_relative_to_dealer]
|
187
|
+
end
|
146
188
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
189
|
+
# @return [Array] The list of opponent hole card hands.
|
190
|
+
# @example If there are two opponents, one with AhKs and the other with QdJc, then
|
191
|
+
# list_of_opponents_hole_cards == [AhKs:Hand, QdJc:Hand]
|
192
|
+
def opponent_hands
|
193
|
+
@opponent_hands ||= -> do
|
194
|
+
hands = all_hands.dup
|
195
|
+
hands.delete_at @position_relative_to_dealer
|
196
|
+
hands
|
197
|
+
end.call
|
198
|
+
end
|
151
199
|
|
152
|
-
|
153
|
-
|
200
|
+
# @return [Boolean] Reports whether or not it is the first state of the first round.
|
201
|
+
def first_state_of_first_round?
|
202
|
+
@first_state_of_first_round ||= @betting_sequence_string.empty?
|
203
|
+
end
|
154
204
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
sum += sequence_per_round.length
|
159
|
-
end
|
160
|
-
end
|
205
|
+
def first_state_of_round?
|
206
|
+
@first_state_of_round ||= @betting_sequence_string[-1] == BETTING_SEQUENCE_SEPARATOR
|
207
|
+
end
|
161
208
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
def betting_sequence_string(betting_sequence=@betting_sequence)
|
167
|
-
(round + 1).times.inject('') do |string, i|
|
168
|
-
string << (betting_sequence[i].map { |action| action.to_s }).join('')
|
169
|
-
string << '/' unless i == round
|
170
|
-
string
|
171
|
-
end
|
172
|
-
end
|
209
|
+
# @return [Integer] The number of players in this match.
|
210
|
+
def number_of_players
|
211
|
+
@number_of_players ||= @hands_string.count(HAND_SEPARATOR) + 1
|
212
|
+
end
|
173
213
|
|
174
|
-
|
175
|
-
|
176
|
-
|
214
|
+
# @return [PokerAction] The last action taken.
|
215
|
+
def last_action
|
216
|
+
@last_action ||= if @betting_sequence_string.match(
|
217
|
+
/([^#{BETTING_SEQUENCE_SEPARATOR}])#{BETTING_SEQUENCE_SEPARATOR}*$/
|
218
|
+
)
|
219
|
+
PokerAction.new($1)
|
220
|
+
else
|
221
|
+
nil
|
177
222
|
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# @return [Integer] The number of actions in the current round.
|
226
|
+
def number_of_actions_this_round
|
227
|
+
@number_of_actions_this_round ||= betting_sequence[round].length
|
228
|
+
end
|
178
229
|
|
179
|
-
|
180
|
-
|
230
|
+
# @return [Integer] The number of actions in the current hand.
|
231
|
+
def number_of_actions_this_hand
|
232
|
+
@number_of_actions_this_hand ||= betting_sequence.inject(0) do |sum, sequence_per_round|
|
233
|
+
sum += sequence_per_round.length
|
181
234
|
end
|
235
|
+
end
|
182
236
|
|
183
|
-
|
184
|
-
|
185
|
-
|
237
|
+
def round_in_which_last_action_taken
|
238
|
+
@round_in_which_last_action_taken ||= if first_state_of_first_round?
|
239
|
+
nil
|
240
|
+
else
|
241
|
+
if @betting_sequence_string[-1] == BETTING_SEQUENCE_SEPARATOR
|
242
|
+
round - 1
|
186
243
|
else
|
187
|
-
|
188
|
-
round - 1
|
189
|
-
else
|
190
|
-
round
|
191
|
-
end
|
244
|
+
round
|
192
245
|
end
|
193
246
|
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# @param stacks [Array<#to_f>]
|
250
|
+
# @param blinds [Array<#to_f>]
|
251
|
+
# @return [PlayerGroup] The state of the players in this hand at the
|
252
|
+
# when the hand began.
|
253
|
+
def players_at_hand_start(stacks, blinds)
|
254
|
+
PlayerGroup.new all_hands, stacks, blinds
|
255
|
+
end
|
194
256
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
257
|
+
# @param game_def [GameDefinition]
|
258
|
+
# @return [PlayerGroup] The current state of the players.
|
259
|
+
def every_action(game_def)
|
260
|
+
@players = players_at_hand_start game_def.chip_stacks, game_def.blinds
|
261
|
+
|
262
|
+
last_round = -1
|
263
|
+
acting_player_position = nil
|
264
|
+
@player_acting_sequence = []
|
265
|
+
every_action_token do |action, round|
|
266
|
+
if round != last_round
|
267
|
+
acting_player_position = game_def.first_player_positions[round]
|
268
|
+
@player_acting_sequence << []
|
269
|
+
last_round = round
|
208
270
|
end
|
209
|
-
list_of_first_seats
|
210
|
-
end
|
211
271
|
|
212
|
-
|
213
|
-
|
214
|
-
|
272
|
+
acting_player_position = @players.position_of_first_active_player(
|
273
|
+
acting_player_position
|
274
|
+
)
|
215
275
|
|
216
|
-
|
217
|
-
!(@position_relative_to_dealer and @hand_number and
|
218
|
-
@list_of_hole_card_hands) || @list_of_hole_card_hands.empty?
|
219
|
-
end
|
276
|
+
@player_acting_sequence.last << acting_player_position
|
220
277
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
278
|
+
cost = @players.action_cost(
|
279
|
+
acting_player_position,
|
280
|
+
action,
|
281
|
+
game_def.min_wagers[round]
|
282
|
+
)
|
283
|
+
|
284
|
+
@players[acting_player_position].append_action!(
|
285
|
+
if cost > 0
|
286
|
+
action = PokerAction.new(action.to_s, cost: cost)
|
287
|
+
else
|
288
|
+
action
|
289
|
+
end,
|
290
|
+
round
|
291
|
+
)
|
292
|
+
|
293
|
+
yield action, round, acting_player_position, @players if block_given?
|
294
|
+
|
295
|
+
acting_player_position = @players.next_player_position(
|
296
|
+
acting_player_position
|
297
|
+
)
|
229
298
|
end
|
230
299
|
|
231
|
-
|
232
|
-
|
300
|
+
distribute_chips!(game_def) if hand_ended?(game_def)
|
301
|
+
|
302
|
+
@players
|
303
|
+
end
|
304
|
+
|
305
|
+
def players(game_def)
|
306
|
+
@players ||= every_action(game_def)
|
307
|
+
end
|
308
|
+
|
309
|
+
def player_acting_sequence(game_def)
|
310
|
+
every_action(game_def) unless @player_acting_sequence
|
311
|
+
|
312
|
+
@player_acting_sequence
|
313
|
+
end
|
233
314
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
315
|
+
# @return [Boolean] +true+ if the hand has ended, +false+ otherwise.
|
316
|
+
def hand_ended?(game_def)
|
317
|
+
@hand_ended ||= reached_showdown? || players(game_def).count { |player| player.inactive? } >= number_of_players - 1
|
318
|
+
end
|
319
|
+
|
320
|
+
def reached_showdown?
|
321
|
+
opponents_cards_visible?
|
322
|
+
end
|
323
|
+
|
324
|
+
def opponents_cards_visible?
|
325
|
+
@opponents_cards_visible ||= all_hands.count { |h| !h.empty? } > 1 # At least one opponent hand visible
|
326
|
+
end
|
327
|
+
|
328
|
+
def pot(game_def)
|
329
|
+
@pot ||= players(game_def).map { |player| player.contributions }.flatten.inject(:+)
|
330
|
+
end
|
331
|
+
|
332
|
+
private
|
333
|
+
|
334
|
+
# Distribute chips to all winning players
|
335
|
+
def distribute_chips!(game_def)
|
336
|
+
return self if pot(game_def) <= 0
|
337
|
+
|
338
|
+
# @todo This only works for Doyle's game where there are no side-pots.
|
339
|
+
if 1 == players(game_def).count { |player| !player.folded? }
|
340
|
+
players(game_def).select { |player| !player.folded? }.first.winnings = pot(game_def)
|
341
|
+
else
|
342
|
+
hand_strengths = players(game_def).map do |player|
|
343
|
+
if player.folded?
|
344
|
+
-1
|
345
|
+
else
|
346
|
+
PileOfCards.new(community_cards.flatten + player.hand).to_poker_hand_strength
|
239
347
|
end
|
240
348
|
end
|
349
|
+
winning_players = hand_strengths.indices(hand_strengths.max)
|
350
|
+
amount_each_player_wins = pot(game_def)/winning_players.length.to_r
|
241
351
|
|
242
|
-
|
243
|
-
|
244
|
-
|
352
|
+
winning_players.each do |player_index|
|
353
|
+
@players[player_index].winnings = amount_each_player_wins
|
354
|
+
end
|
245
355
|
end
|
246
356
|
|
247
|
-
|
248
|
-
|
249
|
-
every_set_of_cards(string_board_cards, '\/').map do |cards_in_round|
|
250
|
-
AcpcPokerTypes::Card.cards(cards_in_round)
|
251
|
-
end
|
252
|
-
)
|
253
|
-
board_cards.next_round! if board_cards.round < string_board_cards.count('/')
|
254
|
-
board_cards
|
255
|
-
end
|
357
|
+
self
|
358
|
+
end
|
256
359
|
|
257
|
-
|
258
|
-
|
360
|
+
def every_action_token
|
361
|
+
betting_sequence.each_with_index do |actions_per_round, round|
|
362
|
+
actions_per_round.each do |action|
|
363
|
+
yield action, round if block_given?
|
364
|
+
end
|
259
365
|
end
|
366
|
+
end
|
260
367
|
|
261
|
-
|
262
|
-
|
263
|
-
|
368
|
+
def all_string_hands(string_of_card_sets)
|
369
|
+
all_sets_of_cards(string_of_card_sets, HAND_SEPARATOR)
|
370
|
+
end
|
371
|
+
|
372
|
+
def all_sets_of_community_cards(string_of_card_sets)
|
373
|
+
all_sets_of_cards(string_of_card_sets, COMMUNITY_CARD_SEPARATOR)
|
374
|
+
end
|
375
|
+
|
376
|
+
def all_sets_of_cards(string_of_card_sets, divider)
|
377
|
+
string_of_card_sets.split(divider)
|
378
|
+
end
|
379
|
+
|
380
|
+
def actions_from_acpc_characters(action_sequence)
|
381
|
+
action_sequence.scan(/[^#{BETTING_SEQUENCE_SEPARATOR}\d]\d*/)
|
264
382
|
end
|
383
|
+
end
|
265
384
|
end
|