acpc_table_manager 3.0.18 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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