acpc_table_manager 0.0.1
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 +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +58 -0
- data/Rakefile +10 -0
- data/acpc_table_manager.gemspec +55 -0
- data/bin/console +16 -0
- data/bin/setup +7 -0
- data/exe/acpc_table_manager +63 -0
- data/lib/acpc_table_manager.rb +17 -0
- data/lib/acpc_table_manager/config.rb +180 -0
- data/lib/acpc_table_manager/dealer.rb +57 -0
- data/lib/acpc_table_manager/match.rb +350 -0
- data/lib/acpc_table_manager/match_slice.rb +196 -0
- data/lib/acpc_table_manager/match_view.rb +203 -0
- data/lib/acpc_table_manager/monkey_patches.rb +19 -0
- data/lib/acpc_table_manager/opponents.rb +39 -0
- data/lib/acpc_table_manager/param_retrieval.rb +32 -0
- data/lib/acpc_table_manager/proxy.rb +276 -0
- data/lib/acpc_table_manager/simple_logging.rb +54 -0
- data/lib/acpc_table_manager/table_manager.rb +260 -0
- data/lib/acpc_table_manager/table_queue.rb +379 -0
- data/lib/acpc_table_manager/utils.rb +34 -0
- data/lib/acpc_table_manager/version.rb +3 -0
- metadata +311 -0
| @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            require 'acpc_dealer'
         | 
| 2 | 
            +
            require 'timeout'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require_relative 'config'
         | 
| 5 | 
            +
            require_relative 'match'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require_relative 'simple_logging'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module AcpcTableManager
         | 
| 10 | 
            +
            module Dealer
         | 
| 11 | 
            +
              extend SimpleLogging
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              @logger = nil
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              # @return [Hash<Symbol, Object>] The dealer information
         | 
| 16 | 
            +
              # @note Saves the actual port numbers used by the dealer instance in +match+
         | 
| 17 | 
            +
              def self.start(options, match, port_numbers: nil)
         | 
| 18 | 
            +
                @logger ||= ::AcpcTableManager.new_log 'dealer.log'
         | 
| 19 | 
            +
                log __method__, options: options, match: match
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                dealer_arguments = {
         | 
| 22 | 
            +
                  match_name: Shellwords.escape(match.name.gsub(/\s+/, '_')),
         | 
| 23 | 
            +
                  game_def_file_name: Shellwords.escape(match.game_definition_file_name),
         | 
| 24 | 
            +
                  hands: Shellwords.escape(match.number_of_hands),
         | 
| 25 | 
            +
                  random_seed: Shellwords.escape(match.random_seed.to_s),
         | 
| 26 | 
            +
                  player_names: match.player_names.map { |name| Shellwords.escape(name.gsub(/\s+/, '_')) }.join(' '),
         | 
| 27 | 
            +
                  options: (options.split(' ').map { |o| Shellwords.escape o }.join(' ') || '')
         | 
| 28 | 
            +
                }
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                log __method__, {
         | 
| 31 | 
            +
                  match_id: match.id,
         | 
| 32 | 
            +
                  dealer_arguments: dealer_arguments,
         | 
| 33 | 
            +
                  log_directory: ::AcpcTableManager.config.match_log_directory,
         | 
| 34 | 
            +
                  port_numbers: port_numbers,
         | 
| 35 | 
            +
                  command: AcpcDealer::DealerRunner.command(dealer_arguments, port_numbers)
         | 
| 36 | 
            +
                }
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                dealer_info = Timeout::timeout(3) do
         | 
| 39 | 
            +
                  AcpcDealer::DealerRunner.start(
         | 
| 40 | 
            +
                    dealer_arguments,
         | 
| 41 | 
            +
                    ::AcpcTableManager.config.match_log_directory,
         | 
| 42 | 
            +
                    port_numbers
         | 
| 43 | 
            +
                  )
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                match.port_numbers = dealer_info[:port_numbers]
         | 
| 47 | 
            +
                match.save!
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                log __method__, {
         | 
| 50 | 
            +
                  match_id: match.id,
         | 
| 51 | 
            +
                  saved_port_numbers: match.port_numbers
         | 
| 52 | 
            +
                }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                dealer_info
         | 
| 55 | 
            +
              end
         | 
| 56 | 
            +
            end
         | 
| 57 | 
            +
            end
         | 
| @@ -0,0 +1,350 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            require 'mongoid'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'acpc_poker_types/game_definition'
         | 
| 5 | 
            +
            require 'acpc_poker_types/match_state'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require_relative 'match_slice'
         | 
| 8 | 
            +
            require_relative 'config'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            module AcpcTableManager
         | 
| 11 | 
            +
            module TimeRefinement
         | 
| 12 | 
            +
              refine Time.class() do
         | 
| 13 | 
            +
                def now_as_string
         | 
| 14 | 
            +
                  now.strftime('%b%-d_%Y-at-%-H_%-M_%-S')
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| 18 | 
            +
            end
         | 
| 19 | 
            +
            using AcpcTableManager::TimeRefinement
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            module AcpcTableManager
         | 
| 22 | 
            +
            class Match
         | 
| 23 | 
            +
              include Mongoid::Document
         | 
| 24 | 
            +
              include Mongoid::Timestamps::Updated
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              embeds_many :slices, class_name: "AcpcTableManager::MatchSlice"
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              # Scopes
         | 
| 29 | 
            +
              scope :old, ->(lifespan) do
         | 
| 30 | 
            +
                where(:updated_at.lt => (Time.new - lifespan))
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
              scope :inactive, ->(lifespan) do
         | 
| 33 | 
            +
                started.and.old(lifespan)
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
              scope :with_slices, ->(has_slices) do
         | 
| 36 | 
            +
                where({ 'slices.0' => { '$exists' => has_slices }})
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
              scope :started, -> { with_slices(true) }
         | 
| 39 | 
            +
              scope :not_started, -> { with_slices(false) }
         | 
| 40 | 
            +
              scope :with_running_status, ->(is_running) do
         | 
| 41 | 
            +
                where(is_running: is_running)
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
              scope :running, -> { with_running_status(true) }
         | 
| 44 | 
            +
              scope :not_running, -> { with_running_status(false) }
         | 
| 45 | 
            +
              scope :running_or_started, -> { any_of([running.selector, started.selector]) }
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              class << self
         | 
| 48 | 
            +
                def id_exists?(match_id)
         | 
| 49 | 
            +
                  where(id: match_id).exists?
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                # Almost scopes
         | 
| 53 | 
            +
                def finished
         | 
| 54 | 
            +
                  all.select { |match| match.finished? }
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
                def unfinished(matches=all)
         | 
| 57 | 
            +
                  matches.select { |match| !match.finished? }
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
                def started_and_unfinished()
         | 
| 60 | 
            +
                  started.to_a.select { |match| !match.finished? }
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                # Schema
         | 
| 64 | 
            +
                def include_name
         | 
| 65 | 
            +
                  field :name
         | 
| 66 | 
            +
                  validates_presence_of :name
         | 
| 67 | 
            +
                  validates_format_of :name, without: /\A\s*\z/
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
                def include_name_from_user
         | 
| 70 | 
            +
                  field :name_from_user
         | 
| 71 | 
            +
                  validates_presence_of :name_from_user
         | 
| 72 | 
            +
                  validates_format_of :name_from_user, without: /\A\s*\z/
         | 
| 73 | 
            +
                  validates_uniqueness_of :name_from_user
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
                def include_game_definition
         | 
| 76 | 
            +
                  field :game_definition_key, type: Symbol
         | 
| 77 | 
            +
                  validates_presence_of :game_definition_key
         | 
| 78 | 
            +
                  field :game_definition_file_name
         | 
| 79 | 
            +
                  field :game_def_hash, type: Hash
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
                def include_number_of_hands
         | 
| 82 | 
            +
                  field :number_of_hands, type: Integer
         | 
| 83 | 
            +
                  validates_presence_of :number_of_hands
         | 
| 84 | 
            +
                  validates_numericality_of :number_of_hands, greater_than: 0, only_integer: true
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
                def include_opponent_names
         | 
| 87 | 
            +
                  field :opponent_names, type: Array
         | 
| 88 | 
            +
                  validates_presence_of :opponent_names
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
                def include_seat
         | 
| 91 | 
            +
                  field :seat, type: Integer
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
                def include_user_name
         | 
| 94 | 
            +
                  field :user_name
         | 
| 95 | 
            +
                  validates_presence_of :user_name
         | 
| 96 | 
            +
                  validates_format_of :user_name, without: /\A\s*\z/
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                # Generators
         | 
| 100 | 
            +
                def new_name(
         | 
| 101 | 
            +
                  user_name,
         | 
| 102 | 
            +
                  game_def_key: nil,
         | 
| 103 | 
            +
                  num_hands: nil,
         | 
| 104 | 
            +
                  seed: nil,
         | 
| 105 | 
            +
                  seat: nil,
         | 
| 106 | 
            +
                  time: true
         | 
| 107 | 
            +
                )
         | 
| 108 | 
            +
                  name = "match.#{user_name}"
         | 
| 109 | 
            +
                  name += ".#{game_def_key}" if game_def_key
         | 
| 110 | 
            +
                  name += ".#{num_hands}h" if num_hands
         | 
| 111 | 
            +
                  name += ".#{seat}s" if seat
         | 
| 112 | 
            +
                  name += ".#{seed}r" if seed
         | 
| 113 | 
            +
                  name += ".#{Time.now_as_string}" if time
         | 
| 114 | 
            +
                  name
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
                def new_random_seed
         | 
| 117 | 
            +
                  random_float = rand
         | 
| 118 | 
            +
                  random_int = (random_float * 10**random_float.to_s.length).to_i
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
                def new_random_seat(num_players)
         | 
| 121 | 
            +
                  rand(num_players) + 1
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
                def default_opponent_names(num_players)
         | 
| 124 | 
            +
                  (num_players - 1).times.map { |i| "Tester" }
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
                # @todo Port numbers don't need to be stored
         | 
| 127 | 
            +
                def create_with_defaults(
         | 
| 128 | 
            +
                  user_name: 'Guest',
         | 
| 129 | 
            +
                  game_definition_key: :two_player_limit,
         | 
| 130 | 
            +
                  port_numbers: []
         | 
| 131 | 
            +
                )
         | 
| 132 | 
            +
                  new(
         | 
| 133 | 
            +
                    name_from_user: new_name(user_name),
         | 
| 134 | 
            +
                    user_name: user_name,
         | 
| 135 | 
            +
                    port_numbers: port_numbers,
         | 
| 136 | 
            +
                    game_definition_key: game_definition_key
         | 
| 137 | 
            +
                  ).finish_starting!
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                # Deletion
         | 
| 141 | 
            +
                def delete_matches_older_than!(lifespan)
         | 
| 142 | 
            +
                  old(lifespan).delete_all
         | 
| 143 | 
            +
                  self
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
                def delete_finished_matches!
         | 
| 146 | 
            +
                  finished.each do |m|
         | 
| 147 | 
            +
                    m.delete if m.all_slices_viewed?
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
                  self
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
                def delete_match!(match_id)
         | 
| 152 | 
            +
                  begin
         | 
| 153 | 
            +
                    match = find match_id
         | 
| 154 | 
            +
                  rescue Mongoid::Errors::DocumentNotFound
         | 
| 155 | 
            +
                  else
         | 
| 156 | 
            +
                    match.delete
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
                  self
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
              end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
              # Schema
         | 
| 163 | 
            +
              field :port_numbers, type: Array
         | 
| 164 | 
            +
              field :random_seed, type: Integer, default: new_random_seed
         | 
| 165 | 
            +
              field :last_slice_viewed, type: Integer, default: -1
         | 
| 166 | 
            +
              field :is_running, type: Boolean, default: false
         | 
| 167 | 
            +
              field :dealer_options, type: String, default: (
         | 
| 168 | 
            +
                [
         | 
| 169 | 
            +
                  '-a', # Append logs with the same name rather than overwrite
         | 
| 170 | 
            +
                  "--t_response 80000", # 80 seconds per action
         | 
| 171 | 
            +
                  '--t_hand -1',
         | 
| 172 | 
            +
                  '--t_per_hand -1'
         | 
| 173 | 
            +
                ].join(' ')
         | 
| 174 | 
            +
              )
         | 
| 175 | 
            +
              include_name
         | 
| 176 | 
            +
              include_name_from_user
         | 
| 177 | 
            +
              include_user_name
         | 
| 178 | 
            +
              include_game_definition
         | 
| 179 | 
            +
              include_number_of_hands
         | 
| 180 | 
            +
              include_opponent_names
         | 
| 181 | 
            +
              include_seat
         | 
| 182 | 
            +
             | 
| 183 | 
            +
             | 
| 184 | 
            +
              def bots(dealer_host)
         | 
| 185 | 
            +
                bot_info_from_config_that_match_opponents = ::AcpcTableManager.exhibition_config.bots(game_definition_key, *opponent_names)
         | 
| 186 | 
            +
                bot_opponent_ports = opponent_ports_with_condition do |name|
         | 
| 187 | 
            +
                  bot_info_from_config_that_match_opponents.keys.include? name
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                raise unless (
         | 
| 191 | 
            +
                  port_numbers.length == player_names.length ||
         | 
| 192 | 
            +
                  bot_opponent_ports.length == bot_info_from_config_that_match_opponents.length
         | 
| 193 | 
            +
                )
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                bot_opponent_ports.zip(
         | 
| 196 | 
            +
                  bot_info_from_config_that_match_opponents.keys,
         | 
| 197 | 
            +
                  bot_info_from_config_that_match_opponents.values
         | 
| 198 | 
            +
                ).reduce({}) do |map, args|
         | 
| 199 | 
            +
                  port_num, name, info = args
         | 
| 200 | 
            +
                  map[name] = {
         | 
| 201 | 
            +
                    runner: (if info['runner'] then info['runner'] else info end),
         | 
| 202 | 
            +
                    host: dealer_host, port: port_num
         | 
| 203 | 
            +
                  }
         | 
| 204 | 
            +
                  map
         | 
| 205 | 
            +
                end
         | 
| 206 | 
            +
              end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
              # Initializers
         | 
| 209 | 
            +
              def set_name!(name_ = self.name_from_user)
         | 
| 210 | 
            +
                name_from_user_ = name_.strip
         | 
| 211 | 
            +
                self.name = name_from_user_
         | 
| 212 | 
            +
                self.name_from_user = name_from_user_
         | 
| 213 | 
            +
                self
         | 
| 214 | 
            +
              end
         | 
| 215 | 
            +
              def set_seat!(seat_ = self.seat)
         | 
| 216 | 
            +
                self.seat = seat_ || self.class().new_random_seat(game_info['num_players'])
         | 
| 217 | 
            +
                if self.seat > game_info['num_players']
         | 
| 218 | 
            +
                  self.seat = game_info['num_players']
         | 
| 219 | 
            +
                end
         | 
| 220 | 
            +
                self
         | 
| 221 | 
            +
              end
         | 
| 222 | 
            +
              def set_game_definition_file_name!(file_name = game_info['file'])
         | 
| 223 | 
            +
                self.game_definition_file_name = file_name
         | 
| 224 | 
            +
                self
         | 
| 225 | 
            +
              end
         | 
| 226 | 
            +
              def set_game_definition_hash!(hash = self.game_def_hash)
         | 
| 227 | 
            +
                self.game_def_hash = hash || game_def_hash_from_key
         | 
| 228 | 
            +
              end
         | 
| 229 | 
            +
              def finish_starting!
         | 
| 230 | 
            +
                set_name!.set_seat!.set_game_definition_file_name!.set_game_definition_hash!
         | 
| 231 | 
            +
                self.opponent_names ||= self.class().default_opponent_names(game_info['num_players'])
         | 
| 232 | 
            +
                self.number_of_hands ||= 1
         | 
| 233 | 
            +
                save!
         | 
| 234 | 
            +
                self
         | 
| 235 | 
            +
              end
         | 
| 236 | 
            +
             | 
| 237 | 
            +
              UNIQUENESS_GUARANTEE_CHARACTER = '_'
         | 
| 238 | 
            +
              def copy_for_next_human_player(next_user_name, next_seat)
         | 
| 239 | 
            +
                match = dup
         | 
| 240 | 
            +
                # This match was not given a name from the user,
         | 
| 241 | 
            +
                # so set this parameter to an arbitrary character
         | 
| 242 | 
            +
                match.name_from_user = UNIQUENESS_GUARANTEE_CHARACTER
         | 
| 243 | 
            +
                while !match.save do
         | 
| 244 | 
            +
                  match.name_from_user << UNIQUENESS_GUARANTEE_CHARACTER
         | 
| 245 | 
            +
                end
         | 
| 246 | 
            +
                match.user_name = next_user_name
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                # Swap seat
         | 
| 249 | 
            +
                match.seat = next_seat
         | 
| 250 | 
            +
                match.opponent_names.insert(seat - 1, user_name)
         | 
| 251 | 
            +
                match.opponent_names.delete_at(seat - 1)
         | 
| 252 | 
            +
                match.save!(validate: false)
         | 
| 253 | 
            +
                match
         | 
| 254 | 
            +
              end
         | 
| 255 | 
            +
              def copy?
         | 
| 256 | 
            +
                self.name_from_user.match(/^#{UNIQUENESS_GUARANTEE_CHARACTER}+$/)
         | 
| 257 | 
            +
              end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
              # Convenience accessors
         | 
| 260 | 
            +
              def game_info
         | 
| 261 | 
            +
                @game_info ||= AcpcTableManager.exhibition_config.games[self.game_definition_key.to_s]
         | 
| 262 | 
            +
              end
         | 
| 263 | 
            +
              # @todo Why am I storing the file name if I want to get it from the key anyway?
         | 
| 264 | 
            +
              def game_def_file_name_from_key() game_info['file'] end
         | 
| 265 | 
            +
              def game_def_hash_from_key()
         | 
| 266 | 
            +
                @game_def_hash_from_key ||= AcpcPokerTypes::GameDefinition.parse_file(game_def_file_name_from_key).to_h
         | 
| 267 | 
            +
              end
         | 
| 268 | 
            +
              def game_def
         | 
| 269 | 
            +
                @game_def ||= AcpcPokerTypes::GameDefinition.new(game_def_hash_from_key)
         | 
| 270 | 
            +
              end
         | 
| 271 | 
            +
              def hand_number
         | 
| 272 | 
            +
                return nil if slices.last.nil?
         | 
| 273 | 
            +
                state = AcpcPokerTypes::MatchState.parse(
         | 
| 274 | 
            +
                  slices.last.state_string
         | 
| 275 | 
            +
                )
         | 
| 276 | 
            +
                if state then state.hand_number else nil end
         | 
| 277 | 
            +
              end
         | 
| 278 | 
            +
              def no_limit?
         | 
| 279 | 
            +
                @is_no_limit ||= game_def.betting_type == AcpcPokerTypes::GameDefinition::BETTING_TYPES[:nolimit]
         | 
| 280 | 
            +
              end
         | 
| 281 | 
            +
              def started?
         | 
| 282 | 
            +
                !self.slices.empty?
         | 
| 283 | 
            +
              end
         | 
| 284 | 
            +
              def finished?
         | 
| 285 | 
            +
                started? && self.slices.last.match_ended?
         | 
| 286 | 
            +
              end
         | 
| 287 | 
            +
              def running?
         | 
| 288 | 
            +
                self.is_running
         | 
| 289 | 
            +
              end
         | 
| 290 | 
            +
              def all_slices_viewed?
         | 
| 291 | 
            +
                self.last_slice_viewed >= (self.slices.length - 1)
         | 
| 292 | 
            +
              end
         | 
| 293 | 
            +
              def all_slices_up_to_hand_end_viewed?
         | 
| 294 | 
            +
                (self.slices.length - 1).downto(0).each do |slice_index|
         | 
| 295 | 
            +
                  slice = self.slices[slice_index]
         | 
| 296 | 
            +
                  if slice.hand_has_ended
         | 
| 297 | 
            +
                    if self.last_slice_viewed >= slice_index
         | 
| 298 | 
            +
                      return true
         | 
| 299 | 
            +
                    else
         | 
| 300 | 
            +
                      return false
         | 
| 301 | 
            +
                    end
         | 
| 302 | 
            +
                  end
         | 
| 303 | 
            +
                end
         | 
| 304 | 
            +
                return all_slices_viewed?
         | 
| 305 | 
            +
              end
         | 
| 306 | 
            +
              def player_names
         | 
| 307 | 
            +
                opponent_names.dup.insert seat-1, self.user_name
         | 
| 308 | 
            +
              end
         | 
| 309 | 
            +
              def bot_special_port_requirements
         | 
| 310 | 
            +
                ::AcpcTableManager.exhibition_config.bots(game_definition_key, *opponent_names).values.map do |bot|
         | 
| 311 | 
            +
                  bot['requires_special_port']
         | 
| 312 | 
            +
                end
         | 
| 313 | 
            +
              end
         | 
| 314 | 
            +
              def users_port
         | 
| 315 | 
            +
                port_numbers[seat - 1]
         | 
| 316 | 
            +
              end
         | 
| 317 | 
            +
              def opponent_ports
         | 
| 318 | 
            +
                port_numbers_ = port_numbers.dup
         | 
| 319 | 
            +
                users_port_ = port_numbers_.delete_at(seat - 1)
         | 
| 320 | 
            +
                port_numbers_
         | 
| 321 | 
            +
              end
         | 
| 322 | 
            +
              def opponent_seats_with_condition
         | 
| 323 | 
            +
                player_names.each_index.select do |i|
         | 
| 324 | 
            +
                  yield player_names[i]
         | 
| 325 | 
            +
                end.map { |s| s + 1 } - [self.seat]
         | 
| 326 | 
            +
              end
         | 
| 327 | 
            +
              def opponent_seats(opponent_name)
         | 
| 328 | 
            +
                opponent_seats_with_condition { |player_name| player_name == opponent_name }
         | 
| 329 | 
            +
              end
         | 
| 330 | 
            +
              def opponent_ports_with_condition
         | 
| 331 | 
            +
                opponent_seats_with_condition { |player_name| yield player_name }.map do |opp_seat|
         | 
| 332 | 
            +
                  port_numbers[opp_seat - 1]
         | 
| 333 | 
            +
                end
         | 
| 334 | 
            +
              end
         | 
| 335 | 
            +
              def opponent_ports_without_condition
         | 
| 336 | 
            +
                local_opponent_ports = opponent_ports
         | 
| 337 | 
            +
                opponent_ports_with_condition { |player_name| yield player_name }.each do |port|
         | 
| 338 | 
            +
                  local_opponent_ports.delete port
         | 
| 339 | 
            +
                end
         | 
| 340 | 
            +
                local_opponent_ports
         | 
| 341 | 
            +
              end
         | 
| 342 | 
            +
              def rejoinable_seats(user_name)
         | 
| 343 | 
            +
                (
         | 
| 344 | 
            +
                  opponent_seats(user_name) -
         | 
| 345 | 
            +
                  # Remove seats already taken by players who have already joined this match
         | 
| 346 | 
            +
                  self.class().where(name: self.name).ne(name_from_user: self.name).map { |m| m.seat }
         | 
| 347 | 
            +
                )
         | 
| 348 | 
            +
              end
         | 
| 349 | 
            +
            end
         | 
| 350 | 
            +
            end
         | 
| @@ -0,0 +1,196 @@ | |
| 1 | 
            +
            require 'mongoid'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'acpc_poker_types/game_definition'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            require_relative 'config'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module AcpcTableManager
         | 
| 8 | 
            +
            class MatchSlice
         | 
| 9 | 
            +
              include Mongoid::Document
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              embedded_in :match, inverse_of: :slices
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              field :hand_has_ended, type: Boolean
         | 
| 14 | 
            +
              field :match_has_ended, type: Boolean
         | 
| 15 | 
            +
              field :seat_with_dealer_button, type: Integer
         | 
| 16 | 
            +
              field :seat_next_to_act, type: Integer
         | 
| 17 | 
            +
              field :state_string, type: String
         | 
| 18 | 
            +
              # Not necessary to be in the database, but more performant than processing on the
         | 
| 19 | 
            +
              # Rails server
         | 
| 20 | 
            +
              field :betting_sequence, type: String
         | 
| 21 | 
            +
              field :pot_at_start_of_round, type: Integer
         | 
| 22 | 
            +
              field :players, type: Array
         | 
| 23 | 
            +
              field :minimum_wager_to, type: Integer
         | 
| 24 | 
            +
              field :chip_contribution_after_calling, type: Integer
         | 
| 25 | 
            +
              field :pot_after_call, type: Integer
         | 
| 26 | 
            +
              field :is_users_turn_to_act, type: Boolean
         | 
| 27 | 
            +
              field :legal_actions, type: Array
         | 
| 28 | 
            +
              field :amount_to_call, type: Integer
         | 
| 29 | 
            +
              field :messages, type: Array
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def self.from_players_at_the_table!(patt, match_has_ended, match)
         | 
| 32 | 
            +
                match.slices.create!(
         | 
| 33 | 
            +
                  hand_has_ended: patt.hand_ended?,
         | 
| 34 | 
            +
                  match_has_ended: match_has_ended,
         | 
| 35 | 
            +
                  seat_with_dealer_button: patt.dealer_player.seat.to_i,
         | 
| 36 | 
            +
                  seat_next_to_act: if patt.next_player_to_act
         | 
| 37 | 
            +
                    patt.next_player_to_act.seat.to_i
         | 
| 38 | 
            +
                  end,
         | 
| 39 | 
            +
                  state_string: patt.match_state.to_s,
         | 
| 40 | 
            +
                  # Not necessary to be in the database, but more performant than processing on the
         | 
| 41 | 
            +
                  # Rails server
         | 
| 42 | 
            +
                  betting_sequence: betting_sequence(patt.match_state, patt.game_def),
         | 
| 43 | 
            +
                  pot_at_start_of_round: pot_at_start_of_round(patt.match_state, patt.game_def).to_i,
         | 
| 44 | 
            +
                  players: players(patt, match.player_names),
         | 
| 45 | 
            +
                  minimum_wager_to: minimum_wager_to(patt.match_state, patt.game_def).to_i,
         | 
| 46 | 
            +
                  chip_contribution_after_calling: chip_contribution_after_calling(patt.match_state, patt.game_def).to_i,
         | 
| 47 | 
            +
                  pot_after_call: pot_after_call(patt.match_state, patt.game_def).to_i,
         | 
| 48 | 
            +
                  all_in: all_in(patt.match_state, patt.game_def).to_i,
         | 
| 49 | 
            +
                  is_users_turn_to_act: patt.users_turn_to_act?,
         | 
| 50 | 
            +
                  legal_actions: patt.legal_actions.map { |action| action.to_s },
         | 
| 51 | 
            +
                  amount_to_call: amount_to_call(patt.match_state, patt.game_def).to_i
         | 
| 52 | 
            +
                )
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              def self.betting_sequence(match_state, game_def)
         | 
| 56 | 
            +
                sequence = ''
         | 
| 57 | 
            +
                match_state.betting_sequence(game_def).each_with_index do |actions_per_round, round|
         | 
| 58 | 
            +
                  actions_per_round.each_with_index do |action, action_index|
         | 
| 59 | 
            +
                    adjusted_action = adjust_action_amount(
         | 
| 60 | 
            +
                      action,
         | 
| 61 | 
            +
                      round,
         | 
| 62 | 
            +
                      match_state,
         | 
| 63 | 
            +
                      game_def
         | 
| 64 | 
            +
                    )
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                    sequence << if (
         | 
| 67 | 
            +
                      match_state.player_acting_sequence(game_def)[round][action_index].to_i ==
         | 
| 68 | 
            +
                      match_state.position_relative_to_dealer
         | 
| 69 | 
            +
                    )
         | 
| 70 | 
            +
                      adjusted_action.capitalize
         | 
| 71 | 
            +
                    else
         | 
| 72 | 
            +
                      adjusted_action
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
                  sequence << '/' unless round == match_state.betting_sequence(game_def).length - 1
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
                sequence
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
              def self.pot_at_start_of_round(match_state, game_def)
         | 
| 81 | 
            +
                return 0 if match_state.round == 0
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                match_state.players(game_def).inject(0) do |sum, pl|
         | 
| 84 | 
            +
                  sum += pl.contributions[0..match_state.round - 1].inject(:+)
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
              # @return [Array<Hash>] Player information ordered by seat.
         | 
| 89 | 
            +
              # Each player hash should contain
         | 
| 90 | 
            +
              # values for the following keys:
         | 
| 91 | 
            +
              # 'name',
         | 
| 92 | 
            +
              # 'seat'
         | 
| 93 | 
            +
              # 'chip_stack'
         | 
| 94 | 
            +
              # 'chip_contributions'
         | 
| 95 | 
            +
              # 'chip_balance'
         | 
| 96 | 
            +
              # 'hole_cards'
         | 
| 97 | 
            +
              # 'winnings'
         | 
| 98 | 
            +
              def self.players(patt, player_names)
         | 
| 99 | 
            +
                player_names_queue = player_names.dup
         | 
| 100 | 
            +
                patt.players.map do |player|
         | 
| 101 | 
            +
                  hole_cards = if !(player.hand.empty? || player.folded?)
         | 
| 102 | 
            +
                    player.hand.to_acpc
         | 
| 103 | 
            +
                  elsif player.folded?
         | 
| 104 | 
            +
                    ''
         | 
| 105 | 
            +
                  else
         | 
| 106 | 
            +
                    '_' * patt.game_def.number_of_hole_cards
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  {
         | 
| 110 | 
            +
                    'name' => player_names_queue.shift,
         | 
| 111 | 
            +
                    'seat' => player.seat,
         | 
| 112 | 
            +
                    'chip_stack' => player.stack.to_i,
         | 
| 113 | 
            +
                    'chip_contributions' => player.contributions.map { |contrib| contrib.to_i },
         | 
| 114 | 
            +
                    'chip_balance' => player.balance,
         | 
| 115 | 
            +
                    'hole_cards' => hole_cards,
         | 
| 116 | 
            +
                    'winnings' => player.winnings.to_f
         | 
| 117 | 
            +
                  }
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
              end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
              # Over round
         | 
| 122 | 
            +
              def self.minimum_wager_to(state, game_def)
         | 
| 123 | 
            +
                return 0 unless state.next_to_act(game_def)
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                (
         | 
| 126 | 
            +
                  state.min_wager_by(game_def) +
         | 
| 127 | 
            +
                  chip_contribution_after_calling(state, game_def)
         | 
| 128 | 
            +
                ).ceil
         | 
| 129 | 
            +
              end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
              # Over round
         | 
| 132 | 
            +
              def self.chip_contribution_after_calling(state, game_def)
         | 
| 133 | 
            +
                return 0 unless state.next_to_act(game_def)
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                (
         | 
| 136 | 
            +
                  (
         | 
| 137 | 
            +
                    state.players(game_def)[
         | 
| 138 | 
            +
                      state.next_to_act(game_def)
         | 
| 139 | 
            +
                    ].contributions[state.round] || 0
         | 
| 140 | 
            +
                  ) + amount_to_call(state, game_def)
         | 
| 141 | 
            +
                )
         | 
| 142 | 
            +
              end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
              # Over round
         | 
| 145 | 
            +
              def self.pot_after_call(state, game_def)
         | 
| 146 | 
            +
                return state.pot(game_def) if state.hand_ended?(game_def)
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                state.pot(game_def) + state.players(game_def).amount_to_call(state.next_to_act(game_def))
         | 
| 149 | 
            +
              end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
              # Over round
         | 
| 152 | 
            +
              def self.all_in(state, game_def)
         | 
| 153 | 
            +
                return 0 if state.hand_ended?(game_def)
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                (
         | 
| 156 | 
            +
                  state.players(game_def)[state.next_to_act(game_def)].stack +
         | 
| 157 | 
            +
                  (
         | 
| 158 | 
            +
                    state.players(game_def)[state.next_to_act(game_def)]
         | 
| 159 | 
            +
                      .contributions[state.round] || 0
         | 
| 160 | 
            +
                  )
         | 
| 161 | 
            +
                ).floor
         | 
| 162 | 
            +
              end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
              def self.amount_to_call(state, game_def)
         | 
| 165 | 
            +
                return 0 if state.next_to_act(game_def).nil?
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                state.players(game_def).amount_to_call(state.next_to_act(game_def))
         | 
| 168 | 
            +
              end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
              def users_turn_to_act?
         | 
| 171 | 
            +
                self.is_users_turn_to_act
         | 
| 172 | 
            +
              end
         | 
| 173 | 
            +
              def hand_ended?
         | 
| 174 | 
            +
                self.hand_has_ended
         | 
| 175 | 
            +
              end
         | 
| 176 | 
            +
              def match_ended?
         | 
| 177 | 
            +
                self.match_has_ended
         | 
| 178 | 
            +
              end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
              private
         | 
| 181 | 
            +
             | 
| 182 | 
            +
              def self.adjust_action_amount(action, round, match_state, game_def)
         | 
| 183 | 
            +
                amount_to_over_hand = action.modifier
         | 
| 184 | 
            +
                if amount_to_over_hand.blank?
         | 
| 185 | 
            +
                  action
         | 
| 186 | 
            +
                else
         | 
| 187 | 
            +
                  amount_to_over_round = (
         | 
| 188 | 
            +
                    amount_to_over_hand.to_i - match_state.players(game_def)[
         | 
| 189 | 
            +
                      match_state.position_relative_to_dealer
         | 
| 190 | 
            +
                    ].contributions_before(round).to_i
         | 
| 191 | 
            +
                  )
         | 
| 192 | 
            +
                  "#{action[0]}#{amount_to_over_round}"
         | 
| 193 | 
            +
                end
         | 
| 194 | 
            +
              end
         | 
| 195 | 
            +
            end
         | 
| 196 | 
            +
            end
         |