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