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.
@@ -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
@@ -1,16 +1,638 @@
1
- require_relative "acpc_table_manager/version"
2
- require_relative "acpc_table_manager/config"
3
- require_relative "acpc_table_manager/match"
4
- require_relative "acpc_table_manager/match_slice"
5
- require_relative "acpc_table_manager/match_view"
6
- require_relative "acpc_table_manager/monkey_patches"
7
- require_relative "acpc_table_manager/opponents"
8
- require_relative "acpc_table_manager/dealer"
9
- require_relative "acpc_table_manager/proxy"
10
- require_relative "acpc_table_manager/simple_logging"
11
- require_relative "acpc_table_manager/maintainer"
12
- require_relative "acpc_table_manager/table_queue"
13
- require_relative "acpc_table_manager/utils"
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