acpc_table_manager 3.0.18 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|