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
@@ -0,0 +1,115 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'acpc_poker_types'
|
4
|
+
require 'acpc_poker_basic_proxy'
|
5
|
+
require 'acpc_dealer'
|
6
|
+
require 'awesome_print'
|
7
|
+
|
8
|
+
class AcpcTestingBot
|
9
|
+
def initialize(port_number, server_host_name='localhost', random=false)
|
10
|
+
log(
|
11
|
+
__method__,
|
12
|
+
port_number: port_number,
|
13
|
+
server_host_name: server_host_name,
|
14
|
+
random: random
|
15
|
+
)
|
16
|
+
dealer_info = AcpcDealer::ConnectionInformation.new port_number, server_host_name
|
17
|
+
@proxy_bot = AcpcPokerBasicProxy::BasicProxy.new dealer_info
|
18
|
+
|
19
|
+
log __method__, msg: 'Connected to dealer'
|
20
|
+
|
21
|
+
match_state = @proxy_bot.receive_match_state!
|
22
|
+
|
23
|
+
log __method__, msg: 'Got first match state'
|
24
|
+
|
25
|
+
@counter = 0
|
26
|
+
while true do
|
27
|
+
begin
|
28
|
+
if random
|
29
|
+
send_random_action
|
30
|
+
else
|
31
|
+
send_deterministic_action
|
32
|
+
end
|
33
|
+
match_state = @proxy_bot.receive_match_state!
|
34
|
+
|
35
|
+
log __method__, match_state: match_state
|
36
|
+
|
37
|
+
if match_state.last_action && match_state.last_action.action == 'r'
|
38
|
+
@fold_allowed = true
|
39
|
+
else
|
40
|
+
@fold_allowed = false
|
41
|
+
end
|
42
|
+
rescue AcpcPokerBasicProxy::DealerStream::UnableToWriteToDealer
|
43
|
+
exit
|
44
|
+
rescue AcpcPokerBasicProxy::DealerStream::UnableToGetFromDealer
|
45
|
+
# Ignore this these since they will always occur at the end of the match
|
46
|
+
# since this bot doesn't know anything about the match or turns.
|
47
|
+
exit
|
48
|
+
rescue => e
|
49
|
+
puts "Error in main loop: #{e.message}, backtrace: #{e.backtrace.join("\n")}"
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def send_deterministic_action
|
56
|
+
log __method__, counter: @counter
|
57
|
+
|
58
|
+
case (@counter % 3)
|
59
|
+
when 0
|
60
|
+
@proxy_bot.send_action AcpcPokerTypes::PokerAction::CALL
|
61
|
+
when 1
|
62
|
+
if @fold_allowed
|
63
|
+
@proxy_bot.send_action AcpcPokerTypes::PokerAction::FOLD
|
64
|
+
else
|
65
|
+
@proxy_bot.send_action AcpcPokerTypes::PokerAction::CALL
|
66
|
+
end
|
67
|
+
when 2
|
68
|
+
@proxy_bot.send_action AcpcPokerTypes::PokerAction.new('r1')
|
69
|
+
end
|
70
|
+
@counter += 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def send_random_action
|
74
|
+
random_number = rand
|
75
|
+
@counter = (random_number * 10 ** (random_number.to_s.length-2)).to_i
|
76
|
+
|
77
|
+
log __method__, counter: @counter
|
78
|
+
|
79
|
+
send_deterministic_action
|
80
|
+
end
|
81
|
+
|
82
|
+
def log(method, data)
|
83
|
+
ap({"#{self.class}##{method}" => data})
|
84
|
+
STDOUT.flush
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def print_usage
|
89
|
+
puts "Usage: #{$0} <port number> [server host name] [millisecond timeout] [random]"
|
90
|
+
end
|
91
|
+
|
92
|
+
def proper_usage?
|
93
|
+
ARGV.length > 0
|
94
|
+
end
|
95
|
+
|
96
|
+
def run_script
|
97
|
+
if ARGV.length > 1
|
98
|
+
server_host_name, port = ARGV[0].strip, ARGV[1].strip
|
99
|
+
else
|
100
|
+
server_host_name, port = 'localhost', ARGV[0].strip
|
101
|
+
end
|
102
|
+
random = if ARGV.length > 3
|
103
|
+
ARGV[3].strip == 'true'
|
104
|
+
else
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
AcpcTestingBot.new port, server_host_name, random
|
109
|
+
end
|
110
|
+
|
111
|
+
if proper_usage?
|
112
|
+
run_script
|
113
|
+
else
|
114
|
+
print_usage
|
115
|
+
end
|
data/lib/acpc_table_manager.rb
CHANGED
@@ -1,16 +1,638 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
require_relative
|
12
|
-
require_relative
|
13
|
-
require_relative
|
1
|
+
require 'contextual_exceptions'
|
2
|
+
require 'rusen'
|
3
|
+
require 'acpc_dealer'
|
4
|
+
require 'acpc_poker_types'
|
5
|
+
require 'redis'
|
6
|
+
require 'timeout'
|
7
|
+
require 'zaru'
|
8
|
+
require 'shellwords'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
require_relative 'acpc_table_manager/version'
|
12
|
+
require_relative 'acpc_table_manager/config'
|
13
|
+
require_relative 'acpc_table_manager/monkey_patches'
|
14
|
+
require_relative 'acpc_table_manager/simple_logging'
|
15
|
+
require_relative 'acpc_table_manager/utils'
|
16
|
+
require_relative 'acpc_table_manager/proxy_utils'
|
17
|
+
|
18
|
+
using AcpcTableManager::SimpleLogging::MessageFormatting
|
14
19
|
|
15
20
|
module AcpcTableManager
|
21
|
+
class UninitializedError < StandardError
|
22
|
+
include ContextualExceptions::ContextualError
|
23
|
+
end
|
24
|
+
class NoPortForDealerAvailable < StandardError
|
25
|
+
include ContextualExceptions::ContextualError
|
26
|
+
end
|
27
|
+
class MatchAlreadyEnqueued < StandardError
|
28
|
+
include ContextualExceptions::ContextualError
|
29
|
+
end
|
30
|
+
class NoBotRunner < StandardError
|
31
|
+
include ContextualExceptions::ContextualError
|
32
|
+
end
|
33
|
+
class RequiresTooManySpecialPorts < StandardError
|
34
|
+
include ContextualExceptions::ContextualError
|
35
|
+
end
|
36
|
+
class SubscribeTimeout < StandardError
|
37
|
+
include ContextualExceptions::ContextualError
|
38
|
+
end
|
39
|
+
|
40
|
+
class CommunicatorComponent
|
41
|
+
attr_reader :channel
|
42
|
+
def initialize(id)
|
43
|
+
@channel = self.class.channel_from_id(id)
|
44
|
+
@redis = AcpcTableManager.new_redis_connection()
|
45
|
+
end
|
46
|
+
def del() @redis.del @channel end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Receiver < CommunicatorComponent
|
50
|
+
def subscribe_with_timeout
|
51
|
+
list, message = @redis.blpop(
|
52
|
+
@channel,
|
53
|
+
timeout: AcpcTableManager.config.maintenance_interval_s
|
54
|
+
)
|
55
|
+
if message
|
56
|
+
yield JSON.parse(message)
|
57
|
+
else
|
58
|
+
raise SubscribeTimeout
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class TableManagerReceiver < Receiver
|
64
|
+
def self.channel_from_id(id) id end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Sender < CommunicatorComponent
|
68
|
+
def self.channel_from_id(id) "#{id}-from-proxy" end
|
69
|
+
def publish(data)
|
70
|
+
@redis.rpush @channel, data
|
71
|
+
@redis.publish @channel, data
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class ProxyReceiver < Receiver
|
76
|
+
def self.channel_from_id(id) "#{id}-to-proxy" end
|
77
|
+
end
|
78
|
+
|
79
|
+
class ProxyCommunicator
|
80
|
+
def initialize(id)
|
81
|
+
@sender = Sender.new(id)
|
82
|
+
@receiver = ProxyReceiver.new(id)
|
83
|
+
end
|
84
|
+
def publish(data) @sender.publish(data) end
|
85
|
+
def subscribe_with_timeout
|
86
|
+
@receiver.subscribe_with_timeout { |on| yield on }
|
87
|
+
end
|
88
|
+
def send_channel() @sender.channel end
|
89
|
+
def receive_channel() @receiver.channel end
|
90
|
+
def del_saved
|
91
|
+
@receiver.del
|
92
|
+
@sender.del
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module TimeRefinement
|
97
|
+
refine Time.class() do
|
98
|
+
def now_as_string
|
99
|
+
now.strftime('%b%-d_%Y-at-%-H_%-M_%-S')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
using AcpcTableManager::TimeRefinement
|
104
|
+
|
105
|
+
def self.shell_sanitize(string)
|
106
|
+
Zaru.sanitize!(Shellwords.escape(string.gsub(/\s+/, '_')))
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.raise_uninitialized
|
110
|
+
raise UninitializedError.new(
|
111
|
+
"Unable to complete with AcpcTableManager uninitialized. Please initialize AcpcTableManager with configuration settings by calling AcpcTableManager.load! with a (YAML) configuration file name."
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
@@config = nil
|
116
|
+
|
117
|
+
def self.config
|
118
|
+
if @@config
|
119
|
+
@@config
|
120
|
+
else
|
121
|
+
raise_uninitialized
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
@@exhibition_config = nil
|
126
|
+
def self.exhibition_config
|
127
|
+
if @@exhibition_config
|
128
|
+
@@exhibition_config
|
129
|
+
else
|
130
|
+
raise_uninitialized
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
@@is_initialized = false
|
135
|
+
|
136
|
+
@@redis_config_file = nil
|
137
|
+
def self.redis_config_file() @@redis_config_file end
|
138
|
+
|
139
|
+
@@config_file = nil
|
140
|
+
def self.config_file() @@config_file end
|
141
|
+
|
142
|
+
@@notifier = nil
|
143
|
+
def self.notifier() @@notifier end
|
144
|
+
|
145
|
+
def self.load_config!(config_data, yaml_directory = File.pwd)
|
146
|
+
interpolation_hash = {
|
147
|
+
pwd: yaml_directory,
|
148
|
+
home: Dir.home,
|
149
|
+
:~ => Dir.home,
|
150
|
+
dealer_directory: AcpcDealer::DEALER_DIRECTORY
|
151
|
+
}
|
152
|
+
config = interpolate_all_strings(config_data, interpolation_hash)
|
153
|
+
interpolation_hash[:pwd] = File.dirname(config['table_manager_constants'])
|
154
|
+
|
155
|
+
@@config = Config.new(
|
156
|
+
config['table_manager_constants'],
|
157
|
+
config['log_directory'],
|
158
|
+
config['match_log_directory'],
|
159
|
+
config['data_directory'],
|
160
|
+
interpolation_hash
|
161
|
+
)
|
162
|
+
|
163
|
+
interpolation_hash[:pwd] = File.dirname(config['exhibition_constants'])
|
164
|
+
@@exhibition_config = ExhibitionConfig.new(
|
165
|
+
config['exhibition_constants'],
|
166
|
+
interpolation_hash,
|
167
|
+
Logger.from_file_name(File.join(@@config.my_log_directory, 'exhibition_config.log'))
|
168
|
+
)
|
169
|
+
|
170
|
+
if config['error_report']
|
171
|
+
Rusen.settings.sender_address = config['error_report']['sender']
|
172
|
+
Rusen.settings.exception_recipients = config['error_report']['recipients']
|
173
|
+
|
174
|
+
Rusen.settings.outputs = config['error_report']['outputs'] || [:pony]
|
175
|
+
Rusen.settings.sections = config['error_report']['sections'] || [:backtrace]
|
176
|
+
Rusen.settings.email_prefix = config['error_report']['email_prefix'] || '[ERROR] '
|
177
|
+
Rusen.settings.smtp_settings = config['error_report']['smtp']
|
178
|
+
|
179
|
+
@@notifier = Rusen
|
180
|
+
else
|
181
|
+
@@config.log(
|
182
|
+
__method__,
|
183
|
+
{
|
184
|
+
warning: "Email reporting disabled. Please set email configuration to enable this feature."
|
185
|
+
},
|
186
|
+
Logger::Severity::WARN
|
187
|
+
)
|
188
|
+
end
|
189
|
+
@@redis_config_file = config['redis_config_file'] || 'default'
|
190
|
+
|
191
|
+
FileUtils.mkdir(opponents_log_dir) unless File.directory?(opponents_log_dir)
|
192
|
+
|
193
|
+
@@is_initialized = true
|
194
|
+
|
195
|
+
@@exhibition_config.games.keys.each do |game|
|
196
|
+
d = data_directory(game)
|
197
|
+
FileUtils.mkdir_p d unless File.directory?(d)
|
198
|
+
q = enqueued_matches_file(game)
|
199
|
+
FileUtils.touch q unless File.exist?(q)
|
200
|
+
r = running_matches_file(game)
|
201
|
+
FileUtils.touch r unless File.exist?(r)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.new_redis_connection(options = {})
|
206
|
+
if @@redis_config_file && @@redis_config_file != 'default'
|
207
|
+
redis_config = YAML.load_file(@@redis_config_file).symbolize_keys
|
208
|
+
options.merge!(redis_config[:default].symbolize_keys)
|
209
|
+
Redis.new(
|
210
|
+
if config['redis_environment_mode'] && redis_config[config['redis_environment_mode'].to_sym]
|
211
|
+
options.merge(redis_config[config['redis_environment_mode'].to_sym].symbolize_keys)
|
212
|
+
else
|
213
|
+
options
|
214
|
+
end
|
215
|
+
)
|
216
|
+
else
|
217
|
+
Redis.new options
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.load!(config_file_path)
|
222
|
+
@@config_file = config_file_path
|
223
|
+
load_config! YAML.load_file(config_file_path), File.dirname(config_file_path)
|
224
|
+
end
|
225
|
+
|
226
|
+
def self.notify(exception)
|
227
|
+
@@notifier.notify(exception) if @@notifier
|
228
|
+
end
|
229
|
+
|
230
|
+
def self.initialized?
|
231
|
+
@@is_initialized
|
232
|
+
end
|
233
|
+
|
234
|
+
def self.raise_if_uninitialized
|
235
|
+
raise_uninitialized unless initialized?
|
236
|
+
end
|
237
|
+
|
238
|
+
def self.new_log(log_file_name, log_directory_ = nil)
|
239
|
+
raise_if_uninitialized
|
240
|
+
log_directory_ ||= @@config.my_log_directory
|
241
|
+
FileUtils.mkdir_p(log_directory_) unless File.directory?(log_directory_)
|
242
|
+
Logger.from_file_name(File.join(log_directory_, log_file_name)).with_metadata!
|
243
|
+
end
|
244
|
+
|
245
|
+
def self.unload!
|
246
|
+
@@is_initialized = false
|
247
|
+
end
|
248
|
+
|
249
|
+
def self.opponents_log_dir
|
250
|
+
File.join(AcpcTableManager.config.log_directory, 'opponents')
|
251
|
+
end
|
252
|
+
|
253
|
+
def self.data_directory(game = nil)
|
254
|
+
raise_if_uninitialized
|
255
|
+
if game
|
256
|
+
File.join(@@config.data_directory, shell_sanitize(game))
|
257
|
+
else
|
258
|
+
@@config.data_directory
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.enqueued_matches_file(game)
|
263
|
+
File.join(data_directory(game), 'enqueued_matches.yml')
|
264
|
+
end
|
265
|
+
|
266
|
+
def self.running_matches_file(game)
|
267
|
+
File.join(data_directory(game), 'running_matches.yml')
|
268
|
+
end
|
269
|
+
|
270
|
+
def self.enqueued_matches(game)
|
271
|
+
YAML.load_file(enqueued_matches_file(game)) || []
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.running_matches(game)
|
275
|
+
saved_matches = YAML.load_file(running_matches_file(game))
|
276
|
+
return [] unless saved_matches
|
277
|
+
|
278
|
+
checked_matches = []
|
279
|
+
saved_matches.each do |match|
|
280
|
+
if AcpcDealer::process_exists?(match[:dealer][:pid])
|
281
|
+
checked_matches << match
|
282
|
+
end
|
283
|
+
end
|
284
|
+
if checked_matches.length != saved_matches.length
|
285
|
+
update_running_matches game, checked_matches
|
286
|
+
end
|
287
|
+
checked_matches
|
288
|
+
end
|
289
|
+
|
290
|
+
def self.sanitized_player_names(names)
|
291
|
+
names.map { |name| Shellwords.escape(name.gsub(/\s+/, '_')) }
|
292
|
+
end
|
293
|
+
|
294
|
+
def self.match_name(players: nil, game_def_key: nil, time: true)
|
295
|
+
name = "match"
|
296
|
+
name += ".#{sanitized_player_names(players).join('.')}" if players
|
297
|
+
if game_def_key
|
298
|
+
name += ".#{game_def_key}.#{exhibition_config.games[game_def_key]['num_hands_per_match']}h"
|
299
|
+
end
|
300
|
+
name += ".#{Time.now_as_string}" if time
|
301
|
+
shell_sanitize name
|
302
|
+
end
|
303
|
+
|
304
|
+
def self.dealer_arguments(game, name, players, random_seed)
|
305
|
+
{
|
306
|
+
match_name: shell_sanitize(name),
|
307
|
+
game_def_file_name: Shellwords.escape(
|
308
|
+
exhibition_config.games[game]['file']
|
309
|
+
),
|
310
|
+
hands: Shellwords.escape(
|
311
|
+
exhibition_config.games[game]['num_hands_per_match']
|
312
|
+
),
|
313
|
+
random_seed: Shellwords.escape(random_seed.to_s),
|
314
|
+
player_names: sanitized_player_names(players).join(' '),
|
315
|
+
options: exhibition_config.dealer_options.join(' ')
|
316
|
+
}
|
317
|
+
end
|
318
|
+
|
319
|
+
def self.proxy_player?(player_name, game_def_key)
|
320
|
+
exhibition_config.games[game_def_key]['opponents'][player_name].nil?
|
321
|
+
end
|
322
|
+
|
323
|
+
def self.start_dealer(game, name, players, random_seed, port_numbers)
|
324
|
+
config.log __method__, name: name
|
325
|
+
args = dealer_arguments game, name, players, random_seed
|
326
|
+
|
327
|
+
config.log __method__, {
|
328
|
+
dealer_arguments: args,
|
329
|
+
log_directory: ::AcpcTableManager.config.match_log_directory,
|
330
|
+
port_numbers: port_numbers,
|
331
|
+
command: AcpcDealer::DealerRunner.command(
|
332
|
+
args,
|
333
|
+
port_numbers
|
334
|
+
)
|
335
|
+
}
|
336
|
+
|
337
|
+
Timeout::timeout(3) do
|
338
|
+
AcpcDealer::DealerRunner.start(
|
339
|
+
args,
|
340
|
+
config.match_log_directory,
|
341
|
+
port_numbers
|
342
|
+
)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def self.start_proxy(game, proxy_id, port, seat)
|
347
|
+
config.log __method__, msg: "Starting proxy"
|
348
|
+
|
349
|
+
args = [
|
350
|
+
"-t #{config_file}",
|
351
|
+
"-i #{proxy_id}",
|
352
|
+
"-p #{port}",
|
353
|
+
"-s #{seat}",
|
354
|
+
"-g #{game}"
|
355
|
+
]
|
356
|
+
command = "#{File.expand_path('../../exe/acpc_proxy', __FILE__)} #{args.join(' ')}"
|
357
|
+
start_process command
|
358
|
+
end
|
359
|
+
|
360
|
+
# @todo This method looks broken
|
361
|
+
# def self.bots(game_def_key, player_names, dealer_host)
|
362
|
+
# bot_info_from_config_that_match_opponents = exhibition_config.bots(
|
363
|
+
# game_def_key,
|
364
|
+
# *opponent_names(player_names)
|
365
|
+
# )
|
366
|
+
# bot_opponent_ports = opponent_ports_with_condition do |name|
|
367
|
+
# bot_info_from_config_that_match_opponents.keys.include? name
|
368
|
+
# end
|
369
|
+
#
|
370
|
+
# raise unless (
|
371
|
+
# port_numbers.length == player_names.length ||
|
372
|
+
# bot_opponent_ports.length == bot_info_from_config_that_match_opponents.length
|
373
|
+
# )
|
374
|
+
#
|
375
|
+
# bot_opponent_ports.zip(
|
376
|
+
# bot_info_from_config_that_match_opponents.keys,
|
377
|
+
# bot_info_from_config_that_match_opponents.values
|
378
|
+
# ).reduce({}) do |map, args|
|
379
|
+
# port_num, name, info = args
|
380
|
+
# map[name] = {
|
381
|
+
# runner: (if info['runner'] then info['runner'] else info end),
|
382
|
+
# host: dealer_host, port: port_num
|
383
|
+
# }
|
384
|
+
# map
|
385
|
+
# end
|
386
|
+
# end
|
387
|
+
|
388
|
+
# @return [Integer] PID of the bot started
|
389
|
+
def self.start_bot(id, bot_info, port)
|
390
|
+
runner = bot_info['runner'].to_s
|
391
|
+
if runner.nil? || runner.strip.empty?
|
392
|
+
raise NoBotRunner, %Q{Bot "#{id}" with info #{bot_info} has no runner.}
|
393
|
+
end
|
394
|
+
args = [runner, config.dealer_host.to_s, port.to_s]
|
395
|
+
log_file = File.join(opponents_log_dir, "#{id}.log")
|
396
|
+
command_to_run = args.join(' ')
|
397
|
+
|
398
|
+
config.log(
|
399
|
+
__method__,
|
400
|
+
{
|
401
|
+
starting_bot: id,
|
402
|
+
args: args,
|
403
|
+
log_file: log_file
|
404
|
+
}
|
405
|
+
)
|
406
|
+
start_process command_to_run, log_file
|
407
|
+
end
|
408
|
+
|
409
|
+
def self.enqueue_match(game, players, seed)
|
410
|
+
sanitized_name = match_name(
|
411
|
+
game_def_key: game,
|
412
|
+
players: players,
|
413
|
+
time: true
|
414
|
+
)
|
415
|
+
enqueued_matches_ = enqueued_matches game
|
416
|
+
if enqueued_matches_.any? { |e| e[:name] == sanitized_name }
|
417
|
+
raise(
|
418
|
+
MatchAlreadyEnqueued,
|
419
|
+
%Q{Match "#{sanitized_name}" already enqueued.}
|
420
|
+
)
|
421
|
+
end
|
422
|
+
enqueued_matches_ << (
|
423
|
+
{
|
424
|
+
name: sanitized_name,
|
425
|
+
game_def_key: game,
|
426
|
+
players: sanitized_player_names(players),
|
427
|
+
random_seed: seed
|
428
|
+
}
|
429
|
+
)
|
430
|
+
update_enqueued_matches game, enqueued_matches_
|
431
|
+
end
|
432
|
+
|
433
|
+
def self.player_id(game, player_name, seat)
|
434
|
+
shell_sanitize(
|
435
|
+
"#{match_name(game_def_key: game, players: [player_name], time: false)}.#{seat}"
|
436
|
+
)
|
437
|
+
end
|
438
|
+
|
439
|
+
def self.available_special_ports(ports_in_use)
|
440
|
+
if exhibition_config.special_ports_to_dealer
|
441
|
+
exhibition_config.special_ports_to_dealer - ports_in_use
|
442
|
+
else
|
443
|
+
[]
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
def self.next_special_port(ports_in_use)
|
448
|
+
available_ports_ = available_special_ports(ports_in_use)
|
449
|
+
port_ = available_ports_.pop
|
450
|
+
until port_.nil? || AcpcDealer.port_available?(port_)
|
451
|
+
port_ = available_ports_.pop
|
452
|
+
end
|
453
|
+
unless port_
|
454
|
+
raise NoPortForDealerAvailable, "None of the available special ports (#{available_special_ports(ports_in_use)}) are open."
|
455
|
+
end
|
456
|
+
port_
|
457
|
+
end
|
458
|
+
|
459
|
+
def self.start_matches_if_allowed(game = nil)
|
460
|
+
if game
|
461
|
+
running_matches_ = running_matches(game)
|
462
|
+
skipped_matches = []
|
463
|
+
enqueued_matches_ = enqueued_matches(game)
|
464
|
+
start_matches_in_game_if_allowed(
|
465
|
+
game,
|
466
|
+
running_matches_,
|
467
|
+
skipped_matches,
|
468
|
+
enqueued_matches_
|
469
|
+
)
|
470
|
+
unless enqueued_matches_.empty? && skipped_matches.empty?
|
471
|
+
update_enqueued_matches game, skipped_matches + enqueued_matches_
|
472
|
+
end
|
473
|
+
else
|
474
|
+
exhibition_config.games.keys.each do |game|
|
475
|
+
start_matches_if_allowed game
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def self.update_enqueued_matches(game, enqueued_matches_)
|
481
|
+
write_yml enqueued_matches_file(game), enqueued_matches_
|
482
|
+
end
|
483
|
+
|
484
|
+
def self.update_running_matches(game, running_matches_)
|
485
|
+
write_yml running_matches_file(game), running_matches_
|
486
|
+
end
|
487
|
+
|
488
|
+
def self.start_match(
|
489
|
+
game,
|
490
|
+
name,
|
491
|
+
players,
|
492
|
+
seed,
|
493
|
+
port_numbers
|
494
|
+
)
|
495
|
+
dealer_info = start_dealer(
|
496
|
+
game,
|
497
|
+
name,
|
498
|
+
players,
|
499
|
+
seed,
|
500
|
+
port_numbers
|
501
|
+
)
|
502
|
+
port_numbers = dealer_info[:port_numbers]
|
503
|
+
|
504
|
+
player_info = []
|
505
|
+
players.each_with_index do |player_name, i|
|
506
|
+
player_info << (
|
507
|
+
{
|
508
|
+
name: player_name,
|
509
|
+
pid: (
|
510
|
+
if exhibition_config.games[game]['opponents'][player_name]
|
511
|
+
start_bot(
|
512
|
+
player_id(game, player_name, i),
|
513
|
+
exhibition_config.games[game]['opponents'][player_name],
|
514
|
+
port_numbers[i]
|
515
|
+
)
|
516
|
+
else
|
517
|
+
start_proxy(
|
518
|
+
game,
|
519
|
+
player_id(game, player_name, i),
|
520
|
+
port_numbers[i],
|
521
|
+
i
|
522
|
+
)
|
523
|
+
end
|
524
|
+
)
|
525
|
+
}
|
526
|
+
)
|
527
|
+
end
|
528
|
+
return dealer_info, player_info
|
529
|
+
end
|
530
|
+
|
531
|
+
def self.allocate_ports(players, game, ports_in_use)
|
532
|
+
num_special_ports_for_this_match = 0
|
533
|
+
max_num_special_ports = if exhibition_config.special_ports_to_dealer.nil?
|
534
|
+
0
|
535
|
+
else
|
536
|
+
exhibition_config.special_ports_to_dealer.length
|
537
|
+
end
|
538
|
+
players.map do |player|
|
539
|
+
bot_info = exhibition_config.games[game]['opponents'][player]
|
540
|
+
if bot_info && bot_info['requires_special_port']
|
541
|
+
num_special_ports_for_this_match += 1
|
542
|
+
if num_special_ports_for_this_match > max_num_special_ports
|
543
|
+
raise(
|
544
|
+
RequiresTooManySpecialPorts,
|
545
|
+
%Q{At least #{num_special_ports_for_this_match} special ports are required but only #{max_num_special_ports} ports were declared.}
|
546
|
+
)
|
547
|
+
end
|
548
|
+
special_port = next_special_port(ports_in_use)
|
549
|
+
ports_in_use << special_port
|
550
|
+
special_port
|
551
|
+
else
|
552
|
+
0
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
private
|
558
|
+
|
559
|
+
def self.write_yml(f, obj)
|
560
|
+
File.open(f, 'w') { |f| f.write YAML.dump(obj) }
|
561
|
+
end
|
562
|
+
|
563
|
+
def self.start_matches_in_game_if_allowed(
|
564
|
+
game,
|
565
|
+
running_matches_,
|
566
|
+
skipped_matches,
|
567
|
+
enqueued_matches_
|
568
|
+
)
|
569
|
+
while running_matches_.length < exhibition_config.games[game]['max_num_matches']
|
570
|
+
next_match = enqueued_matches_.shift
|
571
|
+
break unless next_match
|
572
|
+
|
573
|
+
ports_in_use = running_matches_.map do |m|
|
574
|
+
m[:dealer][:port_numbers]
|
575
|
+
end.flatten
|
576
|
+
|
577
|
+
begin
|
578
|
+
port_numbers = allocate_ports(next_match[:players], game, ports_in_use)
|
579
|
+
|
580
|
+
dealer_info, player_info = start_match(
|
581
|
+
game,
|
582
|
+
next_match[:name],
|
583
|
+
next_match[:players],
|
584
|
+
next_match[:random_seed],
|
585
|
+
port_numbers
|
586
|
+
)
|
587
|
+
rescue NoPortForDealerAvailable => e
|
588
|
+
config.log(
|
589
|
+
__method__,
|
590
|
+
{
|
591
|
+
message: e.message,
|
592
|
+
skipping_match: next_match[:name],
|
593
|
+
backtrace: e.backtrace
|
594
|
+
},
|
595
|
+
Logger::Severity::WARN
|
596
|
+
)
|
597
|
+
skipped_matches << next_match
|
598
|
+
rescue RequiresTooManySpecialPorts, Timeout::Error => e
|
599
|
+
config.log(
|
600
|
+
__method__,
|
601
|
+
{
|
602
|
+
message: e.message,
|
603
|
+
deleting_match: next_match[:name],
|
604
|
+
backtrace: e.backtrace
|
605
|
+
},
|
606
|
+
Logger::Severity::ERROR
|
607
|
+
)
|
608
|
+
else
|
609
|
+
running_matches_.push(
|
610
|
+
name: next_match[:name],
|
611
|
+
dealer: dealer_info,
|
612
|
+
players: player_info
|
613
|
+
)
|
614
|
+
update_running_matches game, running_matches_
|
615
|
+
end
|
616
|
+
update_enqueued_matches game, enqueued_matches_
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
def self.start_process(command, log_file = nil)
|
621
|
+
config.log __method__, running_command: command
|
622
|
+
|
623
|
+
options = {chdir: AcpcDealer::DEALER_DIRECTORY}
|
624
|
+
if log_file
|
625
|
+
options[[:err, :out]] = [log_file, File::CREAT|File::WRONLY|File::APPEND]
|
626
|
+
end
|
627
|
+
|
628
|
+
pid = Timeout.timeout(3) do
|
629
|
+
pid = Process.spawn(command, options)
|
630
|
+
Process.detach(pid)
|
631
|
+
pid
|
632
|
+
end
|
633
|
+
|
634
|
+
config.log __method__, ran_command: command, pid: pid
|
635
|
+
|
636
|
+
pid
|
637
|
+
end
|
16
638
|
end
|