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.
@@ -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