module AcpcTableManager
Constants
- VERSION
Public Class Methods
allocate_ports(players, game, ports_in_use)
click to toggle source
# File lib/acpc_table_manager.rb, line 532 def self.allocate_ports(players, game, ports_in_use) num_special_ports_for_this_match = 0 max_num_special_ports = if exhibition_config.special_ports_to_dealer.nil? 0 else exhibition_config.special_ports_to_dealer.length end players.map do |player| bot_info = exhibition_config.games[game]['opponents'][player] if bot_info && bot_info['requires_special_port'] num_special_ports_for_this_match += 1 if num_special_ports_for_this_match > max_num_special_ports raise( RequiresTooManySpecialPorts, %Q{At least #{num_special_ports_for_this_match} special ports are required but only #{max_num_special_ports} ports were declared.} ) end special_port = next_special_port(ports_in_use) ports_in_use << special_port special_port else 0 end end end
available_special_ports(ports_in_use)
click to toggle source
# File lib/acpc_table_manager.rb, line 440 def self.available_special_ports(ports_in_use) if exhibition_config.special_ports_to_dealer exhibition_config.special_ports_to_dealer - ports_in_use else [] end end
config()
click to toggle source
# File lib/acpc_table_manager.rb, line 118 def self.config if @@config @@config else raise_uninitialized end end
config_file()
click to toggle source
# File lib/acpc_table_manager.rb, line 141 def self.config_file() @@config_file end
data_directory(game = nil)
click to toggle source
# File lib/acpc_table_manager.rb, line 254 def self.data_directory(game = nil) raise_if_uninitialized if game File.join(@@config.data_directory, shell_sanitize(game)) else @@config.data_directory end end
dealer_arguments(game, name, players, random_seed)
click to toggle source
# File lib/acpc_table_manager.rb, line 305 def self.dealer_arguments(game, name, players, random_seed) { match_name: shell_sanitize(name), game_def_file_name: Shellwords.escape( exhibition_config.games[game]['file'] ), hands: Shellwords.escape( exhibition_config.games[game]['num_hands_per_match'] ), random_seed: Shellwords.escape(random_seed.to_s), player_names: sanitized_player_names(players).join(' '), options: exhibition_config.dealer_options.join(' ') } end
each_key_value_pair(collection) { |k, v| ... }
click to toggle source
# File lib/acpc_table_manager/utils.rb, line 4 def self.each_key_value_pair(collection) # @todo I can't believe this is necessary... if collection.is_a?(Array) collection.each_with_index { |v, k| yield k, v } else collection.each { |k, v| yield k, v } end collection end
enqueue_match(game, players, seed)
click to toggle source
# File lib/acpc_table_manager.rb, line 410 def self.enqueue_match(game, players, seed) sanitized_name = match_name( game_def_key: game, players: players, time: true ) enqueued_matches_ = enqueued_matches game if enqueued_matches_.any? { |e| e[:name] == sanitized_name } raise( MatchAlreadyEnqueued, %Q{Match "#{sanitized_name}" already enqueued.} ) end enqueued_matches_ << ( { name: sanitized_name, game_def_key: game, players: sanitized_player_names(players), random_seed: seed } ) update_enqueued_matches game, enqueued_matches_ end
enqueued_matches(game)
click to toggle source
# File lib/acpc_table_manager.rb, line 271 def self.enqueued_matches(game) YAML.load_file(enqueued_matches_file(game)) || [] end
enqueued_matches_file(game)
click to toggle source
# File lib/acpc_table_manager.rb, line 263 def self.enqueued_matches_file(game) File.join(data_directory(game), 'enqueued_matches.yml') end
exhibition_config()
click to toggle source
# File lib/acpc_table_manager.rb, line 127 def self.exhibition_config if @@exhibition_config @@exhibition_config else raise_uninitialized end end
initialized?()
click to toggle source
# File lib/acpc_table_manager.rb, line 231 def self.initialized? @@is_initialized end
interpolate_all_strings(value, interpolation_hash)
click to toggle source
# File lib/acpc_table_manager/utils.rb, line 23 def self.interpolate_all_strings(value, interpolation_hash) if value.is_a?(String) # $VERBOSE and $DEBUG change '%''s behaviour _v = $VERBOSE $VERBOSE = false r = begin value % interpolation_hash rescue ArgumentError value end $VERBOSE = _v r elsif value.respond_to?(:each) each_key_value_pair(value) do |k, v| value[k] = interpolate_all_strings(v, interpolation_hash) end else value end end
load!(config_file_path)
click to toggle source
# File lib/acpc_table_manager.rb, line 222 def self.load!(config_file_path) @@config_file = config_file_path load_config! YAML.load_file(config_file_path), File.dirname(config_file_path) end
load_config!(config_data, yaml_directory = File.pwd)
click to toggle source
# File lib/acpc_table_manager.rb, line 146 def self.load_config!(config_data, yaml_directory = File.pwd) interpolation_hash = { pwd: yaml_directory, home: Dir.home, :~ => Dir.home, dealer_directory: AcpcDealer::DEALER_DIRECTORY } config = interpolate_all_strings(config_data, interpolation_hash) interpolation_hash[:pwd] = File.dirname(config['table_manager_constants']) @@config = Config.new( config['table_manager_constants'], config['log_directory'], config['match_log_directory'], config['data_directory'], interpolation_hash ) interpolation_hash[:pwd] = File.dirname(config['exhibition_constants']) @@exhibition_config = ExhibitionConfig.new( config['exhibition_constants'], interpolation_hash, Logger.from_file_name(File.join(@@config.my_log_directory, 'exhibition_config.log')) ) if config['error_report'] Rusen.settings.sender_address = config['error_report']['sender'] Rusen.settings.exception_recipients = config['error_report']['recipients'] Rusen.settings.outputs = config['error_report']['outputs'] || [:pony] Rusen.settings.sections = config['error_report']['sections'] || [:backtrace] Rusen.settings.email_prefix = config['error_report']['email_prefix'] || '[ERROR] ' Rusen.settings.smtp_settings = config['error_report']['smtp'] @@notifier = Rusen else @@config.log( __method__, { warning: "Email reporting disabled. Please set email configuration to enable this feature." }, Logger::Severity::WARN ) end @@redis_config_file = config['redis_config_file'] || 'default' FileUtils.mkdir(opponents_log_dir) unless File.directory?(opponents_log_dir) @@is_initialized = true @@exhibition_config.games.keys.each do |game| d = data_directory(game) FileUtils.mkdir_p d unless File.directory?(d) q = enqueued_matches_file(game) FileUtils.touch q unless File.exist?(q) r = running_matches_file(game) FileUtils.touch r unless File.exist?(r) end end
match_name(players: nil, game_def_key: nil, time: true)
click to toggle source
# File lib/acpc_table_manager.rb, line 295 def self.match_name(players: nil, game_def_key: nil, time: true) name = "match" name += ".#{sanitized_player_names(players).join('.')}" if players if game_def_key name += ".#{game_def_key}.#{exhibition_config.games[game_def_key]['num_hands_per_match']}h" end name += ".#{Time.now_as_string}" if time shell_sanitize name end
new_log(log_file_name, log_directory_ = nil)
click to toggle source
# File lib/acpc_table_manager.rb, line 239 def self.new_log(log_file_name, log_directory_ = nil) raise_if_uninitialized log_directory_ ||= @@config.my_log_directory FileUtils.mkdir_p(log_directory_) unless File.directory?(log_directory_) Logger.from_file_name(File.join(log_directory_, log_file_name)).with_metadata! end
new_redis_connection(options = {})
click to toggle source
# File lib/acpc_table_manager.rb, line 206 def self.new_redis_connection(options = {}) if @@redis_config_file && @@redis_config_file != 'default' redis_config = YAML.load_file(@@redis_config_file).symbolize_keys options.merge!(redis_config[:default].symbolize_keys) Redis.new( if config['redis_environment_mode'] && redis_config[config['redis_environment_mode'].to_sym] options.merge(redis_config[config['redis_environment_mode'].to_sym].symbolize_keys) else options end ) else Redis.new options end end
next_special_port(ports_in_use)
click to toggle source
# File lib/acpc_table_manager.rb, line 448 def self.next_special_port(ports_in_use) available_ports_ = available_special_ports(ports_in_use) port_ = available_ports_.pop until port_.nil? || AcpcDealer.port_available?(port_) port_ = available_ports_.pop end unless port_ raise NoPortForDealerAvailable, "None of the available special ports (#{available_special_ports(ports_in_use)}) are open." end port_ end
notifier()
click to toggle source
# File lib/acpc_table_manager.rb, line 144 def self.notifier() @@notifier end
notify(exception)
click to toggle source
# File lib/acpc_table_manager.rb, line 227 def self.notify(exception) @@notifier.notify(exception) if @@notifier end
opponents_log_dir()
click to toggle source
# File lib/acpc_table_manager.rb, line 250 def self.opponents_log_dir File.join(AcpcTableManager.config.log_directory, 'opponents') end
player_id(game, player_name, seat)
click to toggle source
# File lib/acpc_table_manager.rb, line 434 def self.player_id(game, player_name, seat) shell_sanitize( "#{match_name(game_def_key: game, players: [player_name], time: false)}.#{seat}" ) end
proxy_player?(player_name, game_def_key)
click to toggle source
# File lib/acpc_table_manager.rb, line 320 def self.proxy_player?(player_name, game_def_key) exhibition_config.games[game_def_key]['opponents'][player_name].nil? end
raise_if_uninitialized()
click to toggle source
# File lib/acpc_table_manager.rb, line 235 def self.raise_if_uninitialized raise_uninitialized unless initialized? end
raise_uninitialized()
click to toggle source
# File lib/acpc_table_manager.rb, line 110 def self.raise_uninitialized raise UninitializedError.new( "Unable to complete with AcpcTableManager uninitialized. Please initialize AcpcTableManager with configuration settings by calling AcpcTableManager.load! with a (YAML) configuration file name." ) end
redis_config_file()
click to toggle source
# File lib/acpc_table_manager.rb, line 138 def self.redis_config_file() @@redis_config_file end
resolve_path(path, root = __FILE__)
click to toggle source
# File lib/acpc_table_manager/utils.rb, line 14 def self.resolve_path(path, root = __FILE__) path = Pathname.new(path) if path.exist? path.realpath.to_s else File.expand_path(path, root) end end
running_matches(game)
click to toggle source
# File lib/acpc_table_manager.rb, line 275 def self.running_matches(game) saved_matches = YAML.load_file(running_matches_file(game)) return [] unless saved_matches checked_matches = [] saved_matches.each do |match| if AcpcDealer::process_exists?(match[:dealer][:pid]) checked_matches << match end end if checked_matches.length != saved_matches.length update_running_matches game, checked_matches end checked_matches end
running_matches_file(game)
click to toggle source
# File lib/acpc_table_manager.rb, line 267 def self.running_matches_file(game) File.join(data_directory(game), 'running_matches.yml') end
sanitized_player_names(names)
click to toggle source
# File lib/acpc_table_manager.rb, line 291 def self.sanitized_player_names(names) names.map { |name| Shellwords.escape(name.gsub(/\s+/, '_')) } end
shell_sanitize(string)
click to toggle source
# File lib/acpc_table_manager.rb, line 106 def self.shell_sanitize(string) Zaru.sanitize!(Shellwords.escape(string.gsub(/\s+/, '_'))) end
start_bot(id, bot_info, port)
click to toggle source
@return [Integer] PID of the bot started
# File lib/acpc_table_manager.rb, line 390 def self.start_bot(id, bot_info, port) runner = bot_info['runner'].to_s if runner.nil? || runner.strip.empty? raise NoBotRunner, %Q{Bot "#{id}" with info #{bot_info} has no runner.} end args = [runner, config.dealer_host.to_s, port.to_s] log_file = File.join(opponents_log_dir, "#{id}.log") command_to_run = args.join(' ') config.log( __method__, { starting_bot: id, args: args, log_file: log_file } ) start_process command_to_run, log_file end
start_dealer(game, name, players, random_seed, port_numbers)
click to toggle source
# File lib/acpc_table_manager.rb, line 324 def self.start_dealer(game, name, players, random_seed, port_numbers) config.log __method__, name: name args = dealer_arguments game, name, players, random_seed config.log __method__, { dealer_arguments: args, log_directory: ::AcpcTableManager.config.match_log_directory, port_numbers: port_numbers, command: AcpcDealer::DealerRunner.command( args, port_numbers ) } Timeout::timeout(3) do AcpcDealer::DealerRunner.start( args, config.match_log_directory, port_numbers ) end end
start_match( game, name, players, seed, port_numbers )
click to toggle source
# File lib/acpc_table_manager.rb, line 489 def self.start_match( game, name, players, seed, port_numbers ) dealer_info = start_dealer( game, name, players, seed, port_numbers ) port_numbers = dealer_info[:port_numbers] player_info = [] players.each_with_index do |player_name, i| player_info << ( { name: player_name, pid: ( if exhibition_config.games[game]['opponents'][player_name] start_bot( player_id(game, player_name, i), exhibition_config.games[game]['opponents'][player_name], port_numbers[i] ) else start_proxy( game, player_id(game, player_name, i), port_numbers[i], i ) end ) } ) end return dealer_info, player_info end
start_matches_if_allowed(game = nil)
click to toggle source
# File lib/acpc_table_manager.rb, line 460 def self.start_matches_if_allowed(game = nil) if game running_matches_ = running_matches(game) skipped_matches = [] enqueued_matches_ = enqueued_matches(game) start_matches_in_game_if_allowed( game, running_matches_, skipped_matches, enqueued_matches_ ) unless enqueued_matches_.empty? && skipped_matches.empty? update_enqueued_matches game, skipped_matches + enqueued_matches_ end else exhibition_config.games.keys.each do |game| start_matches_if_allowed game end end end
start_proxy(game, proxy_id, port, seat)
click to toggle source
# File lib/acpc_table_manager.rb, line 347 def self.start_proxy(game, proxy_id, port, seat) config.log __method__, msg: "Starting proxy" args = [ "-t #{config_file}", "-i #{proxy_id}", "-p #{port}", "-s #{seat}", "-g #{game}" ] command = "#{File.expand_path('../../exe/acpc_proxy', __FILE__)} #{args.join(' ')}" start_process command end
unload!()
click to toggle source
# File lib/acpc_table_manager.rb, line 246 def self.unload! @@is_initialized = false end
update_enqueued_matches(game, enqueued_matches_)
click to toggle source
# File lib/acpc_table_manager.rb, line 481 def self.update_enqueued_matches(game, enqueued_matches_) write_yml enqueued_matches_file(game), enqueued_matches_ end
update_running_matches(game, running_matches_)
click to toggle source
# File lib/acpc_table_manager.rb, line 485 def self.update_running_matches(game, running_matches_) write_yml running_matches_file(game), running_matches_ end
Private Class Methods
start_matches_in_game_if_allowed( game, running_matches_, skipped_matches, enqueued_matches_ )
click to toggle source
# File lib/acpc_table_manager.rb, line 564 def self.start_matches_in_game_if_allowed( game, running_matches_, skipped_matches, enqueued_matches_ ) while running_matches_.length < exhibition_config.games[game]['max_num_matches'] next_match = enqueued_matches_.shift break unless next_match ports_in_use = running_matches_.map do |m| m[:dealer][:port_numbers] end.flatten begin port_numbers = allocate_ports(next_match[:players], game, ports_in_use) dealer_info, player_info = start_match( game, next_match[:name], next_match[:players], next_match[:random_seed], port_numbers ) rescue NoPortForDealerAvailable => e config.log( __method__, { message: e.message, skipping_match: next_match[:name], backtrace: e.backtrace }, Logger::Severity::WARN ) skipped_matches << next_match rescue RequiresTooManySpecialPorts, Timeout::Error => e config.log( __method__, { message: e.message, deleting_match: next_match[:name], backtrace: e.backtrace }, Logger::Severity::ERROR ) else running_matches_.push( name: next_match[:name], dealer: dealer_info, players: player_info ) update_running_matches game, running_matches_ end update_enqueued_matches game, enqueued_matches_ end end
start_process(command, log_file = nil)
click to toggle source
# File lib/acpc_table_manager.rb, line 621 def self.start_process(command, log_file = nil) config.log __method__, running_command: command options = {chdir: AcpcDealer::DEALER_DIRECTORY} if log_file options[[:err, :out]] = [log_file, File::CREAT|File::WRONLY|File::APPEND] end pid = Timeout.timeout(3) do pid = Process.spawn(command, options) Process.detach(pid) pid end config.log __method__, ran_command: command, pid: pid pid end
write_yml(f, obj)
click to toggle source
# File lib/acpc_table_manager.rb, line 560 def self.write_yml(f, obj) File.open(f, 'w') { |f| f.write YAML.dump(obj) } end