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.
@@ -1,11 +1,6 @@
1
1
  require 'socket'
2
2
  require 'json'
3
- require 'mongoid'
4
- require 'moped'
5
- require 'rusen'
6
- require 'contextual_exceptions'
7
- require 'acpc_dealer'
8
- require 'redis'
3
+ require 'fileutils'
9
4
 
10
5
  require_relative 'simple_logging'
11
6
  using AcpcTableManager::SimpleLogging::MessageFormatting
@@ -24,9 +19,21 @@ module AcpcTableManager
24
19
  THIS_MACHINE = Socket.gethostname
25
20
  DEALER_HOST = THIS_MACHINE
26
21
 
27
- attr_reader :file, :log_directory, :my_log_directory, :match_log_directory, :proxy_pids_file
22
+ attr_reader(
23
+ :file,
24
+ :log_directory,
25
+ :my_log_directory,
26
+ :match_log_directory,
27
+ :data_directory
28
+ )
28
29
 
29
- def initialize(file_path, log_directory_, match_log_directory_, proxy_pids_file_, interpolation_hash)
30
+ def initialize(
31
+ file_path,
32
+ log_directory_,
33
+ match_log_directory_,
34
+ data_directory_,
35
+ interpolation_hash
36
+ )
30
37
  @file = file_path
31
38
  JSON.parse(File.read(file_path)).each do |constant, val|
32
39
  define_singleton_method(constant.to_sym) do
@@ -36,8 +43,9 @@ module AcpcTableManager
36
43
  @log_directory = log_directory_
37
44
  @match_log_directory = match_log_directory_
38
45
  @my_log_directory = File.join(@log_directory, 'acpc_table_manager')
39
- @proxy_pids_file = proxy_pids_file_
40
- @logger = Logger.from_file_name(File.join(@my_log_directory, 'config.log'))
46
+ @logger = Logger.from_file_name(File.join(@my_log_directory, 'table_manager.log'))
47
+ @data_directory = data_directory_
48
+ FileUtils.mkdir_p @data_directory unless File.directory?(@data_directory)
41
49
  end
42
50
 
43
51
  def this_machine() THIS_MACHINE end
@@ -49,7 +57,11 @@ module AcpcTableManager
49
57
 
50
58
  attr_reader :file
51
59
 
52
- def initialize(file_path, interpolation_hash, logger = Logger.new(STDOUT))
60
+ def initialize(
61
+ file_path,
62
+ interpolation_hash,
63
+ logger = Logger.new(STDOUT)
64
+ )
53
65
  @logger = logger
54
66
  @file = file_path
55
67
  JSON.parse(File.read(file_path)).each do |constant, val|
@@ -81,141 +93,19 @@ module AcpcTableManager
81
93
  bot_map
82
94
  end
83
95
  else
84
- log(__method__, {warning: "Game '#{game_def_key}' has no opponents."}, Logger::Severity::WARN)
96
+ log(
97
+ __method__, {warning: "Game '#{game_def_key}' has no opponents."},
98
+ Logger::Severity::WARN
99
+ )
85
100
  {}
86
101
  end
87
102
  else
88
- log(__method__, {warning: "Unrecognized game, '#{game_def_key}'."}, Logger::Severity::WARN)
103
+ log(
104
+ __method__, {warning: "Unrecognized game, '#{game_def_key}'."},
105
+ Logger::Severity::WARN
106
+ )
89
107
  {}
90
108
  end
91
109
  end
92
110
  end
93
-
94
- class UninitializedError < StandardError
95
- include ContextualExceptions::ContextualError
96
- end
97
-
98
- def self.raise_uninitialized
99
- raise UninitializedError.new(
100
- "Unable to complete with AcpcTableManager uninitialized. Please initialize AcpcTableManager with configuration settings by calling AcpcTableManager.load! with a (YAML) configuration file name."
101
- )
102
- end
103
-
104
- @@config = nil
105
-
106
- def self.config
107
- if @@config
108
- @@config
109
- else
110
- raise_uninitialized
111
- end
112
- end
113
-
114
- @@exhibition_config = nil
115
- def self.exhibition_config
116
- if @@exhibition_config
117
- @@exhibition_config
118
- else
119
- raise_uninitialized
120
- end
121
- end
122
-
123
- @@is_initialized = false
124
-
125
- @@redis_config_file = nil
126
- def self.redis_config_file() @@redis_config_file end
127
-
128
- @@redis = nil
129
- def self.redis() @@redis end
130
-
131
- @@config_file = nil
132
- def self.config_file() @@config_file end
133
-
134
- @@notifier = nil
135
- def self.notifier() @@notifier end
136
-
137
- def self.load_config!(config_data, yaml_directory = File.pwd)
138
- interpolation_hash = {
139
- pwd: yaml_directory,
140
- home: Dir.home,
141
- :~ => Dir.home,
142
- dealer_directory: AcpcDealer::DEALER_DIRECTORY
143
- }
144
- config = interpolate_all_strings(config_data, interpolation_hash)
145
-
146
- @@config = Config.new(
147
- config['table_manager_constants'],
148
- config['log_directory'],
149
- config['match_log_directory'],
150
- config['proxy_pids_file'],
151
- interpolation_hash
152
- )
153
- @@exhibition_config = ExhibitionConfig.new(
154
- config['exhibition_constants'],
155
- interpolation_hash,
156
- Logger.from_file_name(File.join(@@config.my_log_directory, 'exhibition_config.log'))
157
- )
158
-
159
- # Moped.logger = Logger.from_file_name(File.join(@@config.log_directory, 'moped.log'))
160
- # Mongoid.logger = Logger.from_file_name(File.join(@@config.log_directory, 'mongoid.log'))
161
- # TODO: These should be set in configuration files
162
- Moped.logger.level = ::Logger::FATAL
163
- Mongoid.logger.level = ::Logger::FATAL
164
- Mongoid.load!(config['mongoid_config'], config['mongoid_env'].to_sym)
165
-
166
- if config['error_report']
167
- Rusen.settings.sender_address = config['error_report']['sender']
168
- Rusen.settings.exception_recipients = config['error_report']['recipients']
169
-
170
- Rusen.settings.outputs = config['error_report']['outputs'] || [:pony]
171
- Rusen.settings.sections = config['error_report']['sections'] || [:backtrace]
172
- Rusen.settings.email_prefix = config['error_report']['email_prefix'] || '[ERROR] '
173
- Rusen.settings.smtp_settings = config['error_report']['smtp']
174
-
175
- @@notifier = Rusen
176
- else
177
- @@config.log(__method__, {warning: "Email reporting disabled. Please set email configuration to enable this feature."}, Logger::Severity::WARN)
178
- end
179
-
180
- if config['redis_config_file']
181
- @@redis_config_file = config['redis_config_file']
182
- redis_config = YAML.load_file(@@redis_config_file).symbolize_keys
183
- dflt = redis_config[:default].symbolize_keys
184
- @@redis = Redis.new(
185
- if config['redis_environment_mode'] && redis_config[config['redis_environment_mode'].to_sym]
186
- dflt.merge(redis_config[config['redis_environment_mode'].to_sym].symbolize_keys)
187
- else
188
- dflt
189
- end
190
- )
191
- end
192
-
193
- @@is_initialized = true
194
- end
195
-
196
- def self.load!(config_file_path)
197
- @@config_file = config_file_path
198
- load_config! YAML.load_file(config_file_path), File.dirname(config_file_path)
199
- end
200
-
201
- def self.notify(exception)
202
- @@notifier.notify(exception) if @@notifier
203
- end
204
-
205
- def self.initialized?
206
- @@is_initialized
207
- end
208
-
209
- def self.raise_if_uninitialized
210
- raise_uninitialized unless initialized?
211
- end
212
-
213
- def self.new_log(log_file_name)
214
- raise_if_uninitialized
215
- Logger.from_file_name(File.join(@@config.my_log_directory, log_file_name)).with_metadata!
216
- end
217
-
218
- def self.unload!
219
- @@is_initialized = false
220
- end
221
111
  end
@@ -1,71 +1,45 @@
1
- require 'mongoid'
2
1
  require 'zaru'
3
2
 
4
3
  require 'acpc_poker_types/game_definition'
5
4
  require 'acpc_poker_types/match_state'
6
5
  require 'acpc_dealer'
7
-
8
- require_relative 'match_slice'
9
6
  require_relative 'config'
10
7
 
11
- module AcpcTableManager
12
- module TimeRefinement
13
- refine Time.class() do
14
- def now_as_string
15
- now.strftime('%b%-d_%Y-at-%-H_%-M_%-S')
16
- end
17
- end
18
- end
19
- end
20
- using AcpcTableManager::TimeRefinement
21
-
22
8
  module AcpcTableManager
23
9
  class Match
24
- include Mongoid::Document
25
- include Mongoid::Timestamps::Updated
26
-
27
- embeds_many :slices, class_name: "AcpcTableManager::MatchSlice"
28
-
29
10
  # Scopes
30
- scope :old, ->(lifespan) do
31
- where(:updated_at.lt => (Time.new - lifespan))
32
- end
33
- scope :inactive, ->(lifespan) do
34
- started.and.old(lifespan)
35
- end
36
- scope :active_between, ->(lifespan, reference_time=Time.now) do
37
- started.and.where(
38
- { 'slices.updated_at' => { '$gt' => (reference_time - lifespan)}}
39
- ).and.where(
40
- { 'slices.updated_at' => { '$lt' => reference_time}}
41
- )
42
- end
43
- scope :with_slices, ->(has_slices) do
44
- where({ 'slices.0' => { '$exists' => has_slices }})
45
- end
46
- scope :started, -> { with_slices(true) }
47
- scope :not_started, -> { with_slices(false) }
48
- scope :ready_to_start, -> { where(ready_to_start: true) }
49
- scope(
50
- :possibly_running,
51
- where(:proxy_pid.gt => 0).and.where(:dealer_pid.gt => 0)
52
- )
53
- # @return The matches to be started (have not been started and not
54
- # currently running) ordered from newest to oldest.
55
- scope :queue, not_started.and.ready_to_start.desc(:updated_at)
56
-
57
- index({ game_definition_key: 1 })
58
- index({ proxy_pid: 1, dealer_pid: 1 })
59
- index({ user_name: 1 })
11
+ # scope :old, ->(lifespan) do
12
+ # where(:updated_at.lt => (Time.new - lifespan))
13
+ # end
14
+ # scope :inactive, ->(lifespan) do
15
+ # started.and.old(lifespan)
16
+ # end
17
+ # scope :active_between, ->(lifespan, reference_time=Time.now) do
18
+ # started.and.where(
19
+ # { 'slices.updated_at' => { '$gt' => (reference_time - lifespan)}}
20
+ # ).and.where(
21
+ # { 'slices.updated_at' => { '$lt' => reference_time}}
22
+ # )
23
+ # end
24
+ # scope :with_slices, ->(has_slices) do
25
+ # where({ 'slices.0' => { '$exists' => has_slices }})
26
+ # end
27
+ # scope :started, -> { with_slices(true) }
28
+ # scope :not_started, -> { with_slices(false) }
29
+ # scope :ready_to_start, -> { where(ready_to_start: true) }
30
+ # scope(
31
+ # :possibly_running,
32
+ # where(:proxy_pid.gt => 0).and.where(:dealer_pid.gt => 0)
33
+ # )
34
+ # # @return The matches to be started (have not been started and not
35
+ # # currently running) ordered from newest to oldest.
36
+ # scope :queue, not_started.and.ready_to_start.desc(:updated_at)
37
+ #
38
+ # index({ game_definition_key: 1 })
39
+ # index({ proxy_pid: 1, dealer_pid: 1 })
40
+ # index({ user_name: 1 })
60
41
 
61
42
  class << self
62
- # @todo Move to AcpcDealer
63
- def safe_kill(pid)
64
- if pid && pid > 0
65
- AcpcDealer::kill_process pid
66
- sleep 1 # Give the process a chance to exit
67
- end
68
- end
69
43
  def kill_process_if_running(pid)
70
44
  if pid && pid > 0
71
45
  begin
@@ -229,29 +203,28 @@ class Match
229
203
  end
230
204
 
231
205
  # Schema
232
- field :port_numbers, type: Array
233
- field :random_seed, type: Integer, default: new_random_seed
234
- field :last_slice_viewed, type: Integer, default: -1
235
- field :dealer_pid, type: Integer, default: nil
236
- field :proxy_pid, type: Integer, default: nil
237
- field :ready_to_start, type: Boolean, default: false
238
- field :unable_to_start_dealer, type: Boolean, default: false
239
- field :dealer_options, type: String, default: (
240
- [
241
- '-a', # Append logs with the same name rather than overwrite
242
- "--t_response 80000", # 80 seconds per action
243
- '--t_hand -1',
244
- '--t_per_hand -1'
245
- ].join(' ')
246
- )
247
- include_name
248
- include_name_from_user
249
- include_user_name
250
- include_game_definition
251
- include_number_of_hands
252
- include_opponent_names
253
- include_seat
254
-
206
+ # field :port_numbers, type: Array
207
+ # field :random_seed, type: Integer, default: new_random_seed
208
+ # field :last_slice_viewed, type: Integer, default: -1
209
+ # field :dealer_pid, type: Integer, default: nil
210
+ # field :proxy_pid, type: Integer, default: nil
211
+ # field :ready_to_start, type: Boolean, default: false
212
+ # field :unable_to_start_dealer, type: Boolean, default: false
213
+ # field :dealer_options, type: String, default: (
214
+ # [
215
+ # '-a', # Append logs with the same name rather than overwrite
216
+ # "--t_response 80000", # 80 seconds per action
217
+ # '--t_hand -1',
218
+ # '--t_per_hand -1'
219
+ # ].join(' ')
220
+ # )
221
+ # include_name
222
+ # include_name_from_user
223
+ # include_user_name
224
+ # include_game_definition
225
+ # include_number_of_hands
226
+ # include_opponent_names
227
+ # include_seat
255
228
 
256
229
  def bots(dealer_host)
257
230
  bot_info_from_config_that_match_opponents = ::AcpcTableManager.exhibition_config.bots(game_definition_key, *opponent_names)
@@ -0,0 +1,290 @@
1
+ require 'json'
2
+ require 'acpc_poker_types'
3
+ include AcpcPokerTypes
4
+ require 'acpc_poker_player_proxy'
5
+ include AcpcPokerPlayerProxy
6
+ require 'acpc_dealer'
7
+ include AcpcDealer
8
+
9
+ module AcpcTableManager
10
+ module ProxyUtils
11
+ def exit_and_del_saved
12
+ @communicator.del_saved
13
+ exit
14
+ end
15
+
16
+ def start_proxy(
17
+ game_info,
18
+ seat,
19
+ port,
20
+ must_send_ready = false
21
+ )
22
+ game_definition = GameDefinition.parse_file(game_info['file'])
23
+
24
+ PlayerProxy.new(
25
+ ConnectionInformation.new(
26
+ port,
27
+ AcpcTableManager.config.dealer_host
28
+ ),
29
+ game_definition,
30
+ seat,
31
+ must_send_ready
32
+ ) do |patt|
33
+ if patt.match_state
34
+ log __method__, msg: 'Sending match state'
35
+ @communicator.publish(
36
+ ProxyUtils.players_at_the_table_to_json(
37
+ patt,
38
+ game_info['num_hands_per_match'],
39
+ @state_index
40
+ )
41
+ )
42
+ @state_index += 1
43
+ else
44
+ log __method__, msg: 'Before first match state'
45
+ end
46
+ end
47
+ end
48
+
49
+ def play_check_fold!(proxy)
50
+ log __method__
51
+ if proxy.users_turn_to_act?
52
+ action = if (
53
+ proxy.legal_actions.any? do |a|
54
+ a == AcpcPokerTypes::PokerAction::FOLD
55
+ end
56
+ )
57
+ AcpcPokerTypes::PokerAction::FOLD
58
+ else
59
+ AcpcPokerTypes::PokerAction::CALL
60
+ end
61
+ proxy.play!(action) do |patt|
62
+ log __method__, msg: 'Sending match state'
63
+ @communicator.publish to_json(patt)
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.chip_contributions_in_previous_rounds(player, round)
69
+ if round > 0
70
+ player['chip_contributions'][0..round-1].inject(:+)
71
+ else
72
+ 0
73
+ end
74
+ end
75
+
76
+ def self.opponents(players, users_seat)
77
+ opp = players.dup.rotate(users_seat)
78
+ opp.delete_at(users_seat)
79
+ opp
80
+ end
81
+
82
+ # Over round
83
+ def self.pot_fraction_wager_to_over_round(proxy, fraction=1)
84
+ return 0 if proxy.hand_ended?
85
+
86
+ [
87
+ [
88
+ (
89
+ (fraction * pot_after_call(proxy.match_state, proxy.game_def)) +
90
+ chip_contribution_after_calling(proxy.match_state, proxy.game_def)
91
+ ),
92
+ minimum_wager_to
93
+ ].max,
94
+ all_in
95
+ ].min.floor
96
+ end
97
+
98
+ def self.players_at_the_table_to_json(
99
+ patt,
100
+ max_num_hands_per_match,
101
+ state_index = 0
102
+ )
103
+ players_ = players(patt)
104
+ {
105
+ status: {
106
+ hand_has_ended: patt.hand_ended?,
107
+ match_has_ended: (
108
+ if patt.match_state.stack_sizes
109
+ patt.match_ended?
110
+ else
111
+ patt.match_ended?(max_num_hands_per_match)
112
+ end
113
+ ),
114
+ hand_index: patt.match_state.hand_number,
115
+ state_index: state_index
116
+ },
117
+ legal_actions: patt.legal_actions.map do |action|
118
+ {type: action.to_s, cost: action.cost}
119
+ end,
120
+ table: {
121
+ board_cards: (
122
+ patt.match_state.community_cards.flatten.map do |c|
123
+ {rank: c.rank.to_s, suit: c.suit.to_s}
124
+ end
125
+ ),
126
+ pot_chips: pot_at_start_of_round(patt.match_state, patt.game_def).to_i,
127
+ user: players_[patt.seat],
128
+ opponents: opponents(players_, patt.seat)
129
+ }
130
+ }.to_json
131
+ end
132
+
133
+ def self.betting_sequence(match_state, game_def)
134
+ sequence = ''
135
+ match_state.betting_sequence(game_def).each_with_index do |actions_per_round, round|
136
+ actions_per_round.each_with_index do |action, action_index|
137
+ adjusted_action = adjust_action_amount(
138
+ action,
139
+ round,
140
+ match_state,
141
+ game_def
142
+ )
143
+
144
+ sequence << if (
145
+ match_state.player_acting_sequence(game_def)[round][action_index].to_i ==
146
+ match_state.position_relative_to_dealer
147
+ )
148
+ adjusted_action.capitalize
149
+ else
150
+ adjusted_action
151
+ end
152
+ end
153
+ sequence << '/' unless round == match_state.betting_sequence(game_def).length - 1
154
+ end
155
+ sequence
156
+ end
157
+
158
+ def self.pot_at_start_of_round(match_state, game_def)
159
+ return 0 if match_state.round == 0
160
+
161
+ match_state.players(game_def).inject(0) do |sum, pl|
162
+ sum += pl.contributions[0..match_state.round - 1].inject(:+)
163
+ end
164
+ end
165
+
166
+ # @return [Array<Hash>] Player information ordered by seat.
167
+ # Each player hash should contain
168
+ # values for the following keys:
169
+ # 'seat'
170
+ # 'chip-stack-amount'
171
+ # 'contributions'
172
+ # 'chip-balance-amount'
173
+ # 'hole-cards'
174
+ # 'winnings'
175
+ # 'dealer'
176
+ # 'acting'
177
+ def self.players(patt)
178
+ patt.players.map do |player|
179
+ hole_cards = if player.folded?
180
+ []
181
+ elsif player.hand.empty?
182
+ [{}] * patt.game_def.number_of_hole_cards
183
+ else
184
+ player.hand.map do |c|
185
+ {rank: c.rank.to_acpc, suit: c.suit.to_acpc}
186
+ end
187
+ end
188
+
189
+ {
190
+ seat: player.seat.to_i,
191
+ chipStackAmount: player.stack.to_i,
192
+ contribution: (
193
+ if player.contributions.length <= patt.match_state.round
194
+ 0
195
+ else
196
+ player.contributions.last.to_i
197
+ end
198
+ ),
199
+ contributionFromPreviousRounds: (
200
+ patt.match_state.round.times.inject(0) do |s, i|
201
+ s += player.contributions[i].to_i
202
+ end
203
+ ),
204
+ chipBalanceAmount: player.balance,
205
+ holeCards: hole_cards,
206
+ winnings: player.winnings.to_f,
207
+ dealer: player.seat == patt.dealer_player.seat.to_i,
208
+ acting: (
209
+ patt.next_player_to_act && (
210
+ player.seat == patt.next_player_to_act.seat
211
+ )
212
+ )
213
+ }
214
+ end
215
+ end
216
+
217
+ # Over round
218
+ def self.minimum_wager_to(state, game_def)
219
+ return 0 unless state.next_to_act(game_def)
220
+
221
+ min_wager = [
222
+ (
223
+ state.min_wager_by(game_def) +
224
+ chip_contribution_after_calling(state, game_def)
225
+ ).ceil,
226
+ state.players(game_def)[state.next_to_act(game_def)].stack
227
+ ].min
228
+ if chip_contribution_after_calling(state, game_def) < state.players(game_def)[state.next_to_act(game_def)].stack
229
+ min_wager
230
+ else
231
+ nil
232
+ end
233
+ end
234
+
235
+ # Over round
236
+ def self.chip_contribution_after_calling(state, game_def)
237
+ return 0 unless state.next_to_act(game_def)
238
+
239
+ (
240
+ (
241
+ state.players(game_def)[
242
+ state.next_to_act(game_def)
243
+ ].contributions[state.round] || 0
244
+ ) + amount_to_call(state, game_def)
245
+ )
246
+ end
247
+
248
+ # Over round
249
+ def self.pot_after_call(state, game_def)
250
+ return state.pot(game_def) if state.hand_ended?(game_def)
251
+
252
+ state.pot(game_def) + state.players(game_def).amount_to_call(state.next_to_act(game_def))
253
+ end
254
+
255
+ # Over round
256
+ def self.all_in(state, game_def)
257
+ return 0 if state.hand_ended?(game_def)
258
+
259
+ (
260
+ state.players(game_def)[state.next_to_act(game_def)].stack +
261
+ (
262
+ state.players(game_def)[state.next_to_act(game_def)]
263
+ .contributions[state.round] || 0
264
+ )
265
+ ).floor
266
+ end
267
+
268
+ def self.amount_to_call(state, game_def)
269
+ return 0 if state.next_to_act(game_def).nil?
270
+
271
+ state.players(game_def).amount_to_call(state.next_to_act(game_def))
272
+ end
273
+
274
+ private
275
+
276
+ def self.adjust_action_amount(action, round, match_state, game_def)
277
+ amount_to_over_hand = action.modifier
278
+ if amount_to_over_hand.nil? || amount_to_over_hand.strip.empty?
279
+ action
280
+ else
281
+ amount_to_over_round = (
282
+ amount_to_over_hand.to_i - match_state.players(game_def)[
283
+ match_state.position_relative_to_dealer
284
+ ].contributions_before(round).to_i
285
+ )
286
+ "#{action[0]}#{amount_to_over_round}"
287
+ end
288
+ end
289
+ end
290
+ end