acpc_table_manager 3.0.18 → 4.0.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.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.travis.yml +2 -0
- data/README.md +8 -21
- data/Rakefile +1 -0
- data/acpc_table_manager.gemspec +11 -23
- data/exe/acpc_proxy +146 -58
- data/exe/acpc_table_manager +45 -26
- data/exe/acpc_testing_bot +115 -0
- data/lib/acpc_table_manager.rb +635 -13
- data/lib/acpc_table_manager/config.rb +31 -141
- data/lib/acpc_table_manager/match.rb +52 -79
- data/lib/acpc_table_manager/proxy_utils.rb +290 -0
- data/lib/acpc_table_manager/version.rb +1 -1
- metadata +39 -127
- data/lib/acpc_table_manager/dealer.rb +0 -59
- data/lib/acpc_table_manager/maintainer.rb +0 -31
- data/lib/acpc_table_manager/match_slice.rb +0 -194
- data/lib/acpc_table_manager/match_view.rb +0 -203
- data/lib/acpc_table_manager/opponents.rb +0 -62
- data/lib/acpc_table_manager/proxy.rb +0 -346
- data/lib/acpc_table_manager/table_queue.rb +0 -240
@@ -1,11 +1,6 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'json'
|
3
|
-
require '
|
4
|
-
require 'moped'
|
5
|
-
require 'rusen'
|
6
|
-
require 'contextual_exceptions'
|
7
|
-
require 'acpc_dealer'
|
8
|
-
require 'redis'
|
3
|
+
require 'fileutils'
|
9
4
|
|
10
5
|
require_relative 'simple_logging'
|
11
6
|
using AcpcTableManager::SimpleLogging::MessageFormatting
|
@@ -24,9 +19,21 @@ module AcpcTableManager
|
|
24
19
|
THIS_MACHINE = Socket.gethostname
|
25
20
|
DEALER_HOST = THIS_MACHINE
|
26
21
|
|
27
|
-
attr_reader
|
22
|
+
attr_reader(
|
23
|
+
:file,
|
24
|
+
:log_directory,
|
25
|
+
:my_log_directory,
|
26
|
+
:match_log_directory,
|
27
|
+
:data_directory
|
28
|
+
)
|
28
29
|
|
29
|
-
def initialize(
|
30
|
+
def initialize(
|
31
|
+
file_path,
|
32
|
+
log_directory_,
|
33
|
+
match_log_directory_,
|
34
|
+
data_directory_,
|
35
|
+
interpolation_hash
|
36
|
+
)
|
30
37
|
@file = file_path
|
31
38
|
JSON.parse(File.read(file_path)).each do |constant, val|
|
32
39
|
define_singleton_method(constant.to_sym) do
|
@@ -36,8 +43,9 @@ module AcpcTableManager
|
|
36
43
|
@log_directory = log_directory_
|
37
44
|
@match_log_directory = match_log_directory_
|
38
45
|
@my_log_directory = File.join(@log_directory, 'acpc_table_manager')
|
39
|
-
@
|
40
|
-
@
|
46
|
+
@logger = Logger.from_file_name(File.join(@my_log_directory, 'table_manager.log'))
|
47
|
+
@data_directory = data_directory_
|
48
|
+
FileUtils.mkdir_p @data_directory unless File.directory?(@data_directory)
|
41
49
|
end
|
42
50
|
|
43
51
|
def this_machine() THIS_MACHINE end
|
@@ -49,7 +57,11 @@ module AcpcTableManager
|
|
49
57
|
|
50
58
|
attr_reader :file
|
51
59
|
|
52
|
-
def initialize(
|
60
|
+
def initialize(
|
61
|
+
file_path,
|
62
|
+
interpolation_hash,
|
63
|
+
logger = Logger.new(STDOUT)
|
64
|
+
)
|
53
65
|
@logger = logger
|
54
66
|
@file = file_path
|
55
67
|
JSON.parse(File.read(file_path)).each do |constant, val|
|
@@ -81,141 +93,19 @@ module AcpcTableManager
|
|
81
93
|
bot_map
|
82
94
|
end
|
83
95
|
else
|
84
|
-
log(
|
96
|
+
log(
|
97
|
+
__method__, {warning: "Game '#{game_def_key}' has no opponents."},
|
98
|
+
Logger::Severity::WARN
|
99
|
+
)
|
85
100
|
{}
|
86
101
|
end
|
87
102
|
else
|
88
|
-
log(
|
103
|
+
log(
|
104
|
+
__method__, {warning: "Unrecognized game, '#{game_def_key}'."},
|
105
|
+
Logger::Severity::WARN
|
106
|
+
)
|
89
107
|
{}
|
90
108
|
end
|
91
109
|
end
|
92
110
|
end
|
93
|
-
|
94
|
-
class UninitializedError < StandardError
|
95
|
-
include ContextualExceptions::ContextualError
|
96
|
-
end
|
97
|
-
|
98
|
-
def self.raise_uninitialized
|
99
|
-
raise UninitializedError.new(
|
100
|
-
"Unable to complete with AcpcTableManager uninitialized. Please initialize AcpcTableManager with configuration settings by calling AcpcTableManager.load! with a (YAML) configuration file name."
|
101
|
-
)
|
102
|
-
end
|
103
|
-
|
104
|
-
@@config = nil
|
105
|
-
|
106
|
-
def self.config
|
107
|
-
if @@config
|
108
|
-
@@config
|
109
|
-
else
|
110
|
-
raise_uninitialized
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
@@exhibition_config = nil
|
115
|
-
def self.exhibition_config
|
116
|
-
if @@exhibition_config
|
117
|
-
@@exhibition_config
|
118
|
-
else
|
119
|
-
raise_uninitialized
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
@@is_initialized = false
|
124
|
-
|
125
|
-
@@redis_config_file = nil
|
126
|
-
def self.redis_config_file() @@redis_config_file end
|
127
|
-
|
128
|
-
@@redis = nil
|
129
|
-
def self.redis() @@redis end
|
130
|
-
|
131
|
-
@@config_file = nil
|
132
|
-
def self.config_file() @@config_file end
|
133
|
-
|
134
|
-
@@notifier = nil
|
135
|
-
def self.notifier() @@notifier end
|
136
|
-
|
137
|
-
def self.load_config!(config_data, yaml_directory = File.pwd)
|
138
|
-
interpolation_hash = {
|
139
|
-
pwd: yaml_directory,
|
140
|
-
home: Dir.home,
|
141
|
-
:~ => Dir.home,
|
142
|
-
dealer_directory: AcpcDealer::DEALER_DIRECTORY
|
143
|
-
}
|
144
|
-
config = interpolate_all_strings(config_data, interpolation_hash)
|
145
|
-
|
146
|
-
@@config = Config.new(
|
147
|
-
config['table_manager_constants'],
|
148
|
-
config['log_directory'],
|
149
|
-
config['match_log_directory'],
|
150
|
-
config['proxy_pids_file'],
|
151
|
-
interpolation_hash
|
152
|
-
)
|
153
|
-
@@exhibition_config = ExhibitionConfig.new(
|
154
|
-
config['exhibition_constants'],
|
155
|
-
interpolation_hash,
|
156
|
-
Logger.from_file_name(File.join(@@config.my_log_directory, 'exhibition_config.log'))
|
157
|
-
)
|
158
|
-
|
159
|
-
# Moped.logger = Logger.from_file_name(File.join(@@config.log_directory, 'moped.log'))
|
160
|
-
# Mongoid.logger = Logger.from_file_name(File.join(@@config.log_directory, 'mongoid.log'))
|
161
|
-
# TODO: These should be set in configuration files
|
162
|
-
Moped.logger.level = ::Logger::FATAL
|
163
|
-
Mongoid.logger.level = ::Logger::FATAL
|
164
|
-
Mongoid.load!(config['mongoid_config'], config['mongoid_env'].to_sym)
|
165
|
-
|
166
|
-
if config['error_report']
|
167
|
-
Rusen.settings.sender_address = config['error_report']['sender']
|
168
|
-
Rusen.settings.exception_recipients = config['error_report']['recipients']
|
169
|
-
|
170
|
-
Rusen.settings.outputs = config['error_report']['outputs'] || [:pony]
|
171
|
-
Rusen.settings.sections = config['error_report']['sections'] || [:backtrace]
|
172
|
-
Rusen.settings.email_prefix = config['error_report']['email_prefix'] || '[ERROR] '
|
173
|
-
Rusen.settings.smtp_settings = config['error_report']['smtp']
|
174
|
-
|
175
|
-
@@notifier = Rusen
|
176
|
-
else
|
177
|
-
@@config.log(__method__, {warning: "Email reporting disabled. Please set email configuration to enable this feature."}, Logger::Severity::WARN)
|
178
|
-
end
|
179
|
-
|
180
|
-
if config['redis_config_file']
|
181
|
-
@@redis_config_file = config['redis_config_file']
|
182
|
-
redis_config = YAML.load_file(@@redis_config_file).symbolize_keys
|
183
|
-
dflt = redis_config[:default].symbolize_keys
|
184
|
-
@@redis = Redis.new(
|
185
|
-
if config['redis_environment_mode'] && redis_config[config['redis_environment_mode'].to_sym]
|
186
|
-
dflt.merge(redis_config[config['redis_environment_mode'].to_sym].symbolize_keys)
|
187
|
-
else
|
188
|
-
dflt
|
189
|
-
end
|
190
|
-
)
|
191
|
-
end
|
192
|
-
|
193
|
-
@@is_initialized = true
|
194
|
-
end
|
195
|
-
|
196
|
-
def self.load!(config_file_path)
|
197
|
-
@@config_file = config_file_path
|
198
|
-
load_config! YAML.load_file(config_file_path), File.dirname(config_file_path)
|
199
|
-
end
|
200
|
-
|
201
|
-
def self.notify(exception)
|
202
|
-
@@notifier.notify(exception) if @@notifier
|
203
|
-
end
|
204
|
-
|
205
|
-
def self.initialized?
|
206
|
-
@@is_initialized
|
207
|
-
end
|
208
|
-
|
209
|
-
def self.raise_if_uninitialized
|
210
|
-
raise_uninitialized unless initialized?
|
211
|
-
end
|
212
|
-
|
213
|
-
def self.new_log(log_file_name)
|
214
|
-
raise_if_uninitialized
|
215
|
-
Logger.from_file_name(File.join(@@config.my_log_directory, log_file_name)).with_metadata!
|
216
|
-
end
|
217
|
-
|
218
|
-
def self.unload!
|
219
|
-
@@is_initialized = false
|
220
|
-
end
|
221
111
|
end
|
@@ -1,71 +1,45 @@
|
|
1
|
-
require 'mongoid'
|
2
1
|
require 'zaru'
|
3
2
|
|
4
3
|
require 'acpc_poker_types/game_definition'
|
5
4
|
require 'acpc_poker_types/match_state'
|
6
5
|
require 'acpc_dealer'
|
7
|
-
|
8
|
-
require_relative 'match_slice'
|
9
6
|
require_relative 'config'
|
10
7
|
|
11
|
-
module AcpcTableManager
|
12
|
-
module TimeRefinement
|
13
|
-
refine Time.class() do
|
14
|
-
def now_as_string
|
15
|
-
now.strftime('%b%-d_%Y-at-%-H_%-M_%-S')
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
using AcpcTableManager::TimeRefinement
|
21
|
-
|
22
8
|
module AcpcTableManager
|
23
9
|
class Match
|
24
|
-
include Mongoid::Document
|
25
|
-
include Mongoid::Timestamps::Updated
|
26
|
-
|
27
|
-
embeds_many :slices, class_name: "AcpcTableManager::MatchSlice"
|
28
|
-
|
29
10
|
# Scopes
|
30
|
-
scope :old, ->(lifespan) do
|
31
|
-
|
32
|
-
end
|
33
|
-
scope :inactive, ->(lifespan) do
|
34
|
-
|
35
|
-
end
|
36
|
-
scope :active_between, ->(lifespan, reference_time=Time.now) do
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
scope :with_slices, ->(has_slices) do
|
44
|
-
|
45
|
-
end
|
46
|
-
scope :started, -> { with_slices(true) }
|
47
|
-
scope :not_started, -> { with_slices(false) }
|
48
|
-
scope :ready_to_start, -> { where(ready_to_start: true) }
|
49
|
-
scope(
|
50
|
-
|
51
|
-
|
52
|
-
)
|
53
|
-
# @return The matches to be started (have not been started and not
|
54
|
-
# currently running) ordered from newest to oldest.
|
55
|
-
scope :queue, not_started.and.ready_to_start.desc(:updated_at)
|
56
|
-
|
57
|
-
index({ game_definition_key: 1 })
|
58
|
-
index({ proxy_pid: 1, dealer_pid: 1 })
|
59
|
-
index({ user_name: 1 })
|
11
|
+
# scope :old, ->(lifespan) do
|
12
|
+
# where(:updated_at.lt => (Time.new - lifespan))
|
13
|
+
# end
|
14
|
+
# scope :inactive, ->(lifespan) do
|
15
|
+
# started.and.old(lifespan)
|
16
|
+
# end
|
17
|
+
# scope :active_between, ->(lifespan, reference_time=Time.now) do
|
18
|
+
# started.and.where(
|
19
|
+
# { 'slices.updated_at' => { '$gt' => (reference_time - lifespan)}}
|
20
|
+
# ).and.where(
|
21
|
+
# { 'slices.updated_at' => { '$lt' => reference_time}}
|
22
|
+
# )
|
23
|
+
# end
|
24
|
+
# scope :with_slices, ->(has_slices) do
|
25
|
+
# where({ 'slices.0' => { '$exists' => has_slices }})
|
26
|
+
# end
|
27
|
+
# scope :started, -> { with_slices(true) }
|
28
|
+
# scope :not_started, -> { with_slices(false) }
|
29
|
+
# scope :ready_to_start, -> { where(ready_to_start: true) }
|
30
|
+
# scope(
|
31
|
+
# :possibly_running,
|
32
|
+
# where(:proxy_pid.gt => 0).and.where(:dealer_pid.gt => 0)
|
33
|
+
# )
|
34
|
+
# # @return The matches to be started (have not been started and not
|
35
|
+
# # currently running) ordered from newest to oldest.
|
36
|
+
# scope :queue, not_started.and.ready_to_start.desc(:updated_at)
|
37
|
+
#
|
38
|
+
# index({ game_definition_key: 1 })
|
39
|
+
# index({ proxy_pid: 1, dealer_pid: 1 })
|
40
|
+
# index({ user_name: 1 })
|
60
41
|
|
61
42
|
class << self
|
62
|
-
# @todo Move to AcpcDealer
|
63
|
-
def safe_kill(pid)
|
64
|
-
if pid && pid > 0
|
65
|
-
AcpcDealer::kill_process pid
|
66
|
-
sleep 1 # Give the process a chance to exit
|
67
|
-
end
|
68
|
-
end
|
69
43
|
def kill_process_if_running(pid)
|
70
44
|
if pid && pid > 0
|
71
45
|
begin
|
@@ -229,29 +203,28 @@ class Match
|
|
229
203
|
end
|
230
204
|
|
231
205
|
# Schema
|
232
|
-
field :port_numbers, type: Array
|
233
|
-
field :random_seed, type: Integer, default: new_random_seed
|
234
|
-
field :last_slice_viewed, type: Integer, default: -1
|
235
|
-
field :dealer_pid, type: Integer, default: nil
|
236
|
-
field :proxy_pid, type: Integer, default: nil
|
237
|
-
field :ready_to_start, type: Boolean, default: false
|
238
|
-
field :unable_to_start_dealer, type: Boolean, default: false
|
239
|
-
field :dealer_options, type: String, default: (
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
)
|
247
|
-
include_name
|
248
|
-
include_name_from_user
|
249
|
-
include_user_name
|
250
|
-
include_game_definition
|
251
|
-
include_number_of_hands
|
252
|
-
include_opponent_names
|
253
|
-
include_seat
|
254
|
-
|
206
|
+
# field :port_numbers, type: Array
|
207
|
+
# field :random_seed, type: Integer, default: new_random_seed
|
208
|
+
# field :last_slice_viewed, type: Integer, default: -1
|
209
|
+
# field :dealer_pid, type: Integer, default: nil
|
210
|
+
# field :proxy_pid, type: Integer, default: nil
|
211
|
+
# field :ready_to_start, type: Boolean, default: false
|
212
|
+
# field :unable_to_start_dealer, type: Boolean, default: false
|
213
|
+
# field :dealer_options, type: String, default: (
|
214
|
+
# [
|
215
|
+
# '-a', # Append logs with the same name rather than overwrite
|
216
|
+
# "--t_response 80000", # 80 seconds per action
|
217
|
+
# '--t_hand -1',
|
218
|
+
# '--t_per_hand -1'
|
219
|
+
# ].join(' ')
|
220
|
+
# )
|
221
|
+
# include_name
|
222
|
+
# include_name_from_user
|
223
|
+
# include_user_name
|
224
|
+
# include_game_definition
|
225
|
+
# include_number_of_hands
|
226
|
+
# include_opponent_names
|
227
|
+
# include_seat
|
255
228
|
|
256
229
|
def bots(dealer_host)
|
257
230
|
bot_info_from_config_that_match_opponents = ::AcpcTableManager.exhibition_config.bots(game_definition_key, *opponent_names)
|
@@ -0,0 +1,290 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'acpc_poker_types'
|
3
|
+
include AcpcPokerTypes
|
4
|
+
require 'acpc_poker_player_proxy'
|
5
|
+
include AcpcPokerPlayerProxy
|
6
|
+
require 'acpc_dealer'
|
7
|
+
include AcpcDealer
|
8
|
+
|
9
|
+
module AcpcTableManager
|
10
|
+
module ProxyUtils
|
11
|
+
def exit_and_del_saved
|
12
|
+
@communicator.del_saved
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_proxy(
|
17
|
+
game_info,
|
18
|
+
seat,
|
19
|
+
port,
|
20
|
+
must_send_ready = false
|
21
|
+
)
|
22
|
+
game_definition = GameDefinition.parse_file(game_info['file'])
|
23
|
+
|
24
|
+
PlayerProxy.new(
|
25
|
+
ConnectionInformation.new(
|
26
|
+
port,
|
27
|
+
AcpcTableManager.config.dealer_host
|
28
|
+
),
|
29
|
+
game_definition,
|
30
|
+
seat,
|
31
|
+
must_send_ready
|
32
|
+
) do |patt|
|
33
|
+
if patt.match_state
|
34
|
+
log __method__, msg: 'Sending match state'
|
35
|
+
@communicator.publish(
|
36
|
+
ProxyUtils.players_at_the_table_to_json(
|
37
|
+
patt,
|
38
|
+
game_info['num_hands_per_match'],
|
39
|
+
@state_index
|
40
|
+
)
|
41
|
+
)
|
42
|
+
@state_index += 1
|
43
|
+
else
|
44
|
+
log __method__, msg: 'Before first match state'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def play_check_fold!(proxy)
|
50
|
+
log __method__
|
51
|
+
if proxy.users_turn_to_act?
|
52
|
+
action = if (
|
53
|
+
proxy.legal_actions.any? do |a|
|
54
|
+
a == AcpcPokerTypes::PokerAction::FOLD
|
55
|
+
end
|
56
|
+
)
|
57
|
+
AcpcPokerTypes::PokerAction::FOLD
|
58
|
+
else
|
59
|
+
AcpcPokerTypes::PokerAction::CALL
|
60
|
+
end
|
61
|
+
proxy.play!(action) do |patt|
|
62
|
+
log __method__, msg: 'Sending match state'
|
63
|
+
@communicator.publish to_json(patt)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.chip_contributions_in_previous_rounds(player, round)
|
69
|
+
if round > 0
|
70
|
+
player['chip_contributions'][0..round-1].inject(:+)
|
71
|
+
else
|
72
|
+
0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.opponents(players, users_seat)
|
77
|
+
opp = players.dup.rotate(users_seat)
|
78
|
+
opp.delete_at(users_seat)
|
79
|
+
opp
|
80
|
+
end
|
81
|
+
|
82
|
+
# Over round
|
83
|
+
def self.pot_fraction_wager_to_over_round(proxy, fraction=1)
|
84
|
+
return 0 if proxy.hand_ended?
|
85
|
+
|
86
|
+
[
|
87
|
+
[
|
88
|
+
(
|
89
|
+
(fraction * pot_after_call(proxy.match_state, proxy.game_def)) +
|
90
|
+
chip_contribution_after_calling(proxy.match_state, proxy.game_def)
|
91
|
+
),
|
92
|
+
minimum_wager_to
|
93
|
+
].max,
|
94
|
+
all_in
|
95
|
+
].min.floor
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.players_at_the_table_to_json(
|
99
|
+
patt,
|
100
|
+
max_num_hands_per_match,
|
101
|
+
state_index = 0
|
102
|
+
)
|
103
|
+
players_ = players(patt)
|
104
|
+
{
|
105
|
+
status: {
|
106
|
+
hand_has_ended: patt.hand_ended?,
|
107
|
+
match_has_ended: (
|
108
|
+
if patt.match_state.stack_sizes
|
109
|
+
patt.match_ended?
|
110
|
+
else
|
111
|
+
patt.match_ended?(max_num_hands_per_match)
|
112
|
+
end
|
113
|
+
),
|
114
|
+
hand_index: patt.match_state.hand_number,
|
115
|
+
state_index: state_index
|
116
|
+
},
|
117
|
+
legal_actions: patt.legal_actions.map do |action|
|
118
|
+
{type: action.to_s, cost: action.cost}
|
119
|
+
end,
|
120
|
+
table: {
|
121
|
+
board_cards: (
|
122
|
+
patt.match_state.community_cards.flatten.map do |c|
|
123
|
+
{rank: c.rank.to_s, suit: c.suit.to_s}
|
124
|
+
end
|
125
|
+
),
|
126
|
+
pot_chips: pot_at_start_of_round(patt.match_state, patt.game_def).to_i,
|
127
|
+
user: players_[patt.seat],
|
128
|
+
opponents: opponents(players_, patt.seat)
|
129
|
+
}
|
130
|
+
}.to_json
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.betting_sequence(match_state, game_def)
|
134
|
+
sequence = ''
|
135
|
+
match_state.betting_sequence(game_def).each_with_index do |actions_per_round, round|
|
136
|
+
actions_per_round.each_with_index do |action, action_index|
|
137
|
+
adjusted_action = adjust_action_amount(
|
138
|
+
action,
|
139
|
+
round,
|
140
|
+
match_state,
|
141
|
+
game_def
|
142
|
+
)
|
143
|
+
|
144
|
+
sequence << if (
|
145
|
+
match_state.player_acting_sequence(game_def)[round][action_index].to_i ==
|
146
|
+
match_state.position_relative_to_dealer
|
147
|
+
)
|
148
|
+
adjusted_action.capitalize
|
149
|
+
else
|
150
|
+
adjusted_action
|
151
|
+
end
|
152
|
+
end
|
153
|
+
sequence << '/' unless round == match_state.betting_sequence(game_def).length - 1
|
154
|
+
end
|
155
|
+
sequence
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.pot_at_start_of_round(match_state, game_def)
|
159
|
+
return 0 if match_state.round == 0
|
160
|
+
|
161
|
+
match_state.players(game_def).inject(0) do |sum, pl|
|
162
|
+
sum += pl.contributions[0..match_state.round - 1].inject(:+)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# @return [Array<Hash>] Player information ordered by seat.
|
167
|
+
# Each player hash should contain
|
168
|
+
# values for the following keys:
|
169
|
+
# 'seat'
|
170
|
+
# 'chip-stack-amount'
|
171
|
+
# 'contributions'
|
172
|
+
# 'chip-balance-amount'
|
173
|
+
# 'hole-cards'
|
174
|
+
# 'winnings'
|
175
|
+
# 'dealer'
|
176
|
+
# 'acting'
|
177
|
+
def self.players(patt)
|
178
|
+
patt.players.map do |player|
|
179
|
+
hole_cards = if player.folded?
|
180
|
+
[]
|
181
|
+
elsif player.hand.empty?
|
182
|
+
[{}] * patt.game_def.number_of_hole_cards
|
183
|
+
else
|
184
|
+
player.hand.map do |c|
|
185
|
+
{rank: c.rank.to_acpc, suit: c.suit.to_acpc}
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
{
|
190
|
+
seat: player.seat.to_i,
|
191
|
+
chipStackAmount: player.stack.to_i,
|
192
|
+
contribution: (
|
193
|
+
if player.contributions.length <= patt.match_state.round
|
194
|
+
0
|
195
|
+
else
|
196
|
+
player.contributions.last.to_i
|
197
|
+
end
|
198
|
+
),
|
199
|
+
contributionFromPreviousRounds: (
|
200
|
+
patt.match_state.round.times.inject(0) do |s, i|
|
201
|
+
s += player.contributions[i].to_i
|
202
|
+
end
|
203
|
+
),
|
204
|
+
chipBalanceAmount: player.balance,
|
205
|
+
holeCards: hole_cards,
|
206
|
+
winnings: player.winnings.to_f,
|
207
|
+
dealer: player.seat == patt.dealer_player.seat.to_i,
|
208
|
+
acting: (
|
209
|
+
patt.next_player_to_act && (
|
210
|
+
player.seat == patt.next_player_to_act.seat
|
211
|
+
)
|
212
|
+
)
|
213
|
+
}
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Over round
|
218
|
+
def self.minimum_wager_to(state, game_def)
|
219
|
+
return 0 unless state.next_to_act(game_def)
|
220
|
+
|
221
|
+
min_wager = [
|
222
|
+
(
|
223
|
+
state.min_wager_by(game_def) +
|
224
|
+
chip_contribution_after_calling(state, game_def)
|
225
|
+
).ceil,
|
226
|
+
state.players(game_def)[state.next_to_act(game_def)].stack
|
227
|
+
].min
|
228
|
+
if chip_contribution_after_calling(state, game_def) < state.players(game_def)[state.next_to_act(game_def)].stack
|
229
|
+
min_wager
|
230
|
+
else
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Over round
|
236
|
+
def self.chip_contribution_after_calling(state, game_def)
|
237
|
+
return 0 unless state.next_to_act(game_def)
|
238
|
+
|
239
|
+
(
|
240
|
+
(
|
241
|
+
state.players(game_def)[
|
242
|
+
state.next_to_act(game_def)
|
243
|
+
].contributions[state.round] || 0
|
244
|
+
) + amount_to_call(state, game_def)
|
245
|
+
)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Over round
|
249
|
+
def self.pot_after_call(state, game_def)
|
250
|
+
return state.pot(game_def) if state.hand_ended?(game_def)
|
251
|
+
|
252
|
+
state.pot(game_def) + state.players(game_def).amount_to_call(state.next_to_act(game_def))
|
253
|
+
end
|
254
|
+
|
255
|
+
# Over round
|
256
|
+
def self.all_in(state, game_def)
|
257
|
+
return 0 if state.hand_ended?(game_def)
|
258
|
+
|
259
|
+
(
|
260
|
+
state.players(game_def)[state.next_to_act(game_def)].stack +
|
261
|
+
(
|
262
|
+
state.players(game_def)[state.next_to_act(game_def)]
|
263
|
+
.contributions[state.round] || 0
|
264
|
+
)
|
265
|
+
).floor
|
266
|
+
end
|
267
|
+
|
268
|
+
def self.amount_to_call(state, game_def)
|
269
|
+
return 0 if state.next_to_act(game_def).nil?
|
270
|
+
|
271
|
+
state.players(game_def).amount_to_call(state.next_to_act(game_def))
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
def self.adjust_action_amount(action, round, match_state, game_def)
|
277
|
+
amount_to_over_hand = action.modifier
|
278
|
+
if amount_to_over_hand.nil? || amount_to_over_hand.strip.empty?
|
279
|
+
action
|
280
|
+
else
|
281
|
+
amount_to_over_round = (
|
282
|
+
amount_to_over_hand.to_i - match_state.players(game_def)[
|
283
|
+
match_state.position_relative_to_dealer
|
284
|
+
].contributions_before(round).to_i
|
285
|
+
)
|
286
|
+
"#{action[0]}#{amount_to_over_round}"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|