acpc_table_manager 0.0.1

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