Class: AcpcTableManager::Match

Inherits:
Object
  • Object
show all
Includes:
Mongoid::Document, Mongoid::Timestamps::Updated
Defined in:
lib/acpc_table_manager/match.rb

Constant Summary collapse

UNIQUENESS_GUARANTEE_CHARACTER =
'_'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.create_with_defaults(user_name: 'Guest', game_definition_key: :two_player_limit, port_numbers: []) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/acpc_table_manager/match.rb', line 196

def create_with_defaults(
  user_name: 'Guest',
  game_definition_key: :two_player_limit,
  port_numbers: []
)
  new(
    name_from_user: new_name(user_name),
    user_name: user_name,
    port_numbers: port_numbers,
    game_definition_key: game_definition_key
  ).finish_starting!
end

.default_opponent_names(num_players) ⇒ Object



192
193
194
# File 'lib/acpc_table_manager/match.rb', line 192

def default_opponent_names(num_players)
  (num_players - 1).times.map { |i| "Tester" }
end

.delete_finished_matches!Object



214
215
216
217
218
219
# File 'lib/acpc_table_manager/match.rb', line 214

def delete_finished_matches!
  finished.each do |m|
    m.delete if m.all_slices_viewed?
  end
  self
end

.delete_match!(match_id) ⇒ Object



220
221
222
223
224
225
226
227
228
# File 'lib/acpc_table_manager/match.rb', line 220

def delete_match!(match_id)
  begin
    match = find match_id
  rescue Mongoid::Errors::DocumentNotFound
  else
    match.delete
  end
  self
end

.delete_matches_older_than!(lifespan) ⇒ Object

Deletion



210
211
212
213
# File 'lib/acpc_table_manager/match.rb', line 210

def delete_matches_older_than!(lifespan)
  old(lifespan).delete_all
  self
end

.finished(matches = all) ⇒ Object



105
106
107
# File 'lib/acpc_table_manager/match.rb', line 105

def finished(matches=all)
  matches.select { |match| match.finished? }
end

.id_exists?(match_id, matches = all) ⇒ Boolean

Returns:

  • (Boolean)


86
87
88
# File 'lib/acpc_table_manager/match.rb', line 86

def id_exists?(match_id, matches=all)
  matches.where(id: match_id).exists?
end

.include_game_definitionObject



143
144
145
146
147
148
# File 'lib/acpc_table_manager/match.rb', line 143

def include_game_definition
  field :game_definition_key, type: Symbol
  validates_presence_of :game_definition_key
  field :game_definition_file_name
  field :game_def_hash, type: Hash
end

.include_nameObject

Schema



132
133
134
135
136
# File 'lib/acpc_table_manager/match.rb', line 132

def include_name
  field :name
  validates_presence_of :name
  validates_format_of :name, without: /\A\s*\z/
end

.include_name_from_userObject



137
138
139
140
141
142
# File 'lib/acpc_table_manager/match.rb', line 137

def include_name_from_user
  field :name_from_user
  validates_presence_of :name_from_user
  validates_format_of :name_from_user, without: /\A\s*\z/
  validates_uniqueness_of :name_from_user
end

.include_number_of_handsObject



149
150
151
152
153
# File 'lib/acpc_table_manager/match.rb', line 149

def include_number_of_hands
  field :number_of_hands, type: Integer
  validates_presence_of :number_of_hands
  validates_numericality_of :number_of_hands, greater_than: 0, only_integer: true
end

.include_opponent_namesObject



154
155
156
157
# File 'lib/acpc_table_manager/match.rb', line 154

def include_opponent_names
  field :opponent_names, type: Array
  validates_presence_of :opponent_names
end

.include_seatObject



158
159
160
# File 'lib/acpc_table_manager/match.rb', line 158

def include_seat
  field :seat, type: Integer
end

.include_user_nameObject



161
162
163
164
165
# File 'lib/acpc_table_manager/match.rb', line 161

def include_user_name
  field :user_name
  validates_presence_of :user_name
  validates_format_of :user_name, without: /\A\s*\z/
end

.kill_all_orphan_processes!(matches = all) ⇒ Object



123
124
125
# File 'lib/acpc_table_manager/match.rb', line 123

def kill_all_orphan_processes!(matches=all)
  matches.each { |m| m.kill_orphan_processes! }
end

.kill_all_orphan_proxies!(matches = all) ⇒ Object



127
128
129
# File 'lib/acpc_table_manager/match.rb', line 127

def kill_all_orphan_proxies!(matches=all)
  matches.each { |m| m.kill_orphan_proxy! }
end

.kill_process_if_running(pid) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/acpc_table_manager/match.rb', line 69

def kill_process_if_running(pid)
  if pid && pid > 0
    begin
      safe_kill pid
      if AcpcDealer::process_exists?(pid)
        AcpcDealer::force_kill_process pid
        sleep 1 # Give the process a chance to exit

        if AcpcDealer::process_exists?(pid)
          yield if block_given?
        end
      end
    rescue Errno::ESRCH
    end
  end
end

.new_name(user_name, game_def_key: nil, num_hands: nil, seed: nil, seat: nil, time: true) ⇒ Object

Generators



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/acpc_table_manager/match.rb', line 168

def new_name(
  user_name,
  game_def_key: nil,
  num_hands: nil,
  seed: nil,
  seat: nil,
  time: true
)
  name = "match.#{user_name}"
  name += ".#{game_def_key}" if game_def_key
  name += ".#{num_hands}h" if num_hands
  name += ".#{seat}s" if seat
  name += ".#{seed}r" if seed
  name += ".#{Time.now_as_string}" if time
  name
end

.new_random_seat(num_players) ⇒ Object



189
190
191
# File 'lib/acpc_table_manager/match.rb', line 189

def new_random_seat(num_players)
  rand(num_players) + 1
end

.new_random_seedObject



184
185
186
187
188
# File 'lib/acpc_table_manager/match.rb', line 184

def new_random_seed
  # The ACPC dealer requires 32 bit random seeds
  # TODO The bound to rand is exclusive so this should be 2**33
  rand(2**33 - 1)
end

.not_running(matches = all) ⇒ Object



102
103
104
# File 'lib/acpc_table_manager/match.rb', line 102

def not_running(matches=all)
  matches.select { |match| !match.running? }
end

.ports_in_use(matches = all) ⇒ Object



115
116
117
118
119
120
121
# File 'lib/acpc_table_manager/match.rb', line 115

def ports_in_use(matches=all)
  ports = []
  matches.possibly_running.each do |m|
    ports += m.port_numbers if m.running?
  end
  ports
end

.quiet_find(match_id) ⇒ Object



90
91
92
93
94
95
96
# File 'lib/acpc_table_manager/match.rb', line 90

def quiet_find(match_id)
  begin
    match = Match.find match_id
  rescue Mongoid::Errors::DocumentNotFound
    nil
  end
end

.running(matches = all) ⇒ Object

Almost scopes



99
100
101
# File 'lib/acpc_table_manager/match.rb', line 99

def running(matches=all)
  matches.possibly_running.select { |match| match.running? }
end

.safe_kill(pid) ⇒ Object



63
64
65
66
67
68
# File 'lib/acpc_table_manager/match.rb', line 63

def safe_kill(pid)
  if pid && pid > 0
    AcpcDealer::kill_process pid
    sleep 1 # Give the process a chance to exit
  end
end

.started_and_unfinishedObject



111
112
113
# File 'lib/acpc_table_manager/match.rb', line 111

def started_and_unfinished
  started.to_a.select { |match| !match.finished? }
end

.unfinished(matches = all) ⇒ Object



108
109
110
# File 'lib/acpc_table_manager/match.rb', line 108

def unfinished(matches=all)
  matches.select { |match| !match.finished? }
end

Instance Method Details

#all_slices_up_to_hand_end_viewed?Boolean

Returns:

  • (Boolean)


370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/acpc_table_manager/match.rb', line 370

def all_slices_up_to_hand_end_viewed?
  (self.slices.length - 1).downto(0).each do |slice_index|
    slice = self.slices[slice_index]
    if slice.hand_has_ended
      if self.last_slice_viewed >= slice_index
        return true
      else
        return false
      end
    end
  end
  return all_slices_viewed?
end

#all_slices_viewed?Boolean

Returns:

  • (Boolean)


367
368
369
# File 'lib/acpc_table_manager/match.rb', line 367

def all_slices_viewed?
  self.last_slice_viewed >= (self.slices.length - 1)
end

#bot_special_port_requirementsObject



386
387
388
389
390
# File 'lib/acpc_table_manager/match.rb', line 386

def bot_special_port_requirements
  ::AcpcTableManager.exhibition_config.bots(game_definition_key, *opponent_names).values.map do |bot|
    bot['requires_special_port']
  end
end

#bots(dealer_host) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/acpc_table_manager/match.rb', line 256

def bots(dealer_host)
  bot_info_from_config_that_match_opponents = ::AcpcTableManager.exhibition_config.bots(game_definition_key, *opponent_names)
  bot_opponent_ports = opponent_ports_with_condition do |name|
    bot_info_from_config_that_match_opponents.keys.include? name
  end

  raise unless (
    port_numbers.length == player_names.length ||
    bot_opponent_ports.length == bot_info_from_config_that_match_opponents.length
  )

  bot_opponent_ports.zip(
    bot_info_from_config_that_match_opponents.keys,
    bot_info_from_config_that_match_opponents.values
  ).reduce({}) do |map, args|
    port_num, name, info = args
    map[name] = {
      runner: (if info['runner'] then info['runner'] else info end),
      host: dealer_host, port: port_num
    }
    map
  end
end

#copy?Boolean

Returns:

  • (Boolean)


332
333
334
# File 'lib/acpc_table_manager/match.rb', line 332

def copy?
  self.name_from_user.match(/^#{UNIQUENESS_GUARANTEE_CHARACTER}+$/)
end

#copy_for_next_human_player(next_user_name, next_seat) ⇒ Object



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/acpc_table_manager/match.rb', line 315

def copy_for_next_human_player(next_user_name, next_seat)
  match = dup
  # This match was not given a name from the user,
  # so set this parameter to an arbitrary character
  match.name_from_user = UNIQUENESS_GUARANTEE_CHARACTER
  while !match.save do
    match.name_from_user << UNIQUENESS_GUARANTEE_CHARACTER
  end
  match.user_name = next_user_name

  # Swap seat
  match.seat = next_seat
  match.opponent_names.insert(seat - 1, user_name)
  match.opponent_names.delete_at(seat - 1)
  match.save!(validate: false)
  match
end

#dealer_running?Boolean

Returns:

  • (Boolean)


361
362
363
# File 'lib/acpc_table_manager/match.rb', line 361

def dealer_running?
  self.dealer_pid && self.dealer_pid > 0 && AcpcDealer::process_exists?(self.dealer_pid)
end

#defunkt?Boolean

Returns:

  • (Boolean)


442
443
444
# File 'lib/acpc_table_manager/match.rb', line 442

def defunkt?()
  (started? and !running? and !finished?) || self.unable_to_start_dealer
end

#finish_starting!Object



305
306
307
308
309
310
311
312
# File 'lib/acpc_table_manager/match.rb', line 305

def finish_starting!
  set_name!.set_seat!.set_game_definition_file_name!.set_game_definition_hash!
  self.opponent_names ||= self.class().default_opponent_names(game_info['num_players'])
  self.number_of_hands ||= 1
  self.ready_to_start = true
  save!
  self
end

#finished?Boolean

Returns:

  • (Boolean)


359
# File 'lib/acpc_table_manager/match.rb', line 359

def finished?() started? && self.slices.last.match_ended? end

#game_defObject



345
346
347
# File 'lib/acpc_table_manager/match.rb', line 345

def game_def
  @game_def ||= AcpcPokerTypes::GameDefinition.new(game_def_hash_from_key)
end

#game_def_file_name_from_keyObject



341
# File 'lib/acpc_table_manager/match.rb', line 341

def game_def_file_name_from_key() game_info['file'] end

#game_def_hash_from_keyObject



342
343
344
# File 'lib/acpc_table_manager/match.rb', line 342

def game_def_hash_from_key()
  @game_def_hash_from_key ||= AcpcPokerTypes::GameDefinition.parse_file(game_def_file_name_from_key).to_h
end

#game_infoObject

Convenience accessors



337
338
339
# File 'lib/acpc_table_manager/match.rb', line 337

def game_info
  @game_info ||= AcpcTableManager.exhibition_config.games[self.game_definition_key.to_s]
end

#hand_numberObject



348
349
350
351
352
353
354
# File 'lib/acpc_table_manager/match.rb', line 348

def hand_number
  return nil if slices.last.nil?
  state = AcpcPokerTypes::MatchState.parse(
    slices.last.state_string
  )
  if state then state.hand_number else nil end
end

#kill_dealer!Object



429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/acpc_table_manager/match.rb', line 429

def kill_dealer!
  self.class().kill_process_if_running(self.dealer_pid) do
    raise(
      StandardError.new(
        "Dealer process #{self.dealer_pid} couldn't be killed!"
      )
    )
  end
  self.dealer_pid = nil
  save
  self
end

#kill_orphan_processes!Object



463
464
465
466
467
468
469
# File 'lib/acpc_table_manager/match.rb', line 463

def kill_orphan_processes!
  if dealer_running? && !proxy_running?
    kill_dealer!
  elsif proxy_running && !dealer_running?
    kill_proxy!
  end
end

#kill_orphan_proxy!Object



459
460
461
# File 'lib/acpc_table_manager/match.rb', line 459

def kill_orphan_proxy!
  kill_proxy! if proxy_running? && !dealer_running?
end

#kill_proxy!Object



446
447
448
449
450
451
452
453
454
455
456
457
# File 'lib/acpc_table_manager/match.rb', line 446

def kill_proxy!
  self.class().kill_process_if_running(self.proxy_pid) do
    raise(
      StandardError.new(
        "Proxy process #{self.proxy_pid} couldn't be killed!"
      )
    )
  end
  self.proxy_pid = nil
  save
  self
end

#no_limit?Boolean

Returns:

  • (Boolean)


355
356
357
# File 'lib/acpc_table_manager/match.rb', line 355

def no_limit?
  @is_no_limit ||= game_def.betting_type == AcpcPokerTypes::GameDefinition::BETTING_TYPES[:nolimit]
end

#opponent_portsObject



394
395
396
397
398
# File 'lib/acpc_table_manager/match.rb', line 394

def opponent_ports
  port_numbers_ = port_numbers.dup
  users_port_ = port_numbers_.delete_at(seat - 1)
  port_numbers_
end

#opponent_ports_with_conditionObject



407
408
409
410
411
# File 'lib/acpc_table_manager/match.rb', line 407

def opponent_ports_with_condition
  opponent_seats_with_condition { |player_name| yield player_name }.map do |opp_seat|
    port_numbers[opp_seat - 1]
  end
end

#opponent_ports_without_conditionObject



412
413
414
415
416
417
418
# File 'lib/acpc_table_manager/match.rb', line 412

def opponent_ports_without_condition
  local_opponent_ports = opponent_ports
  opponent_ports_with_condition { |player_name| yield player_name }.each do |port|
    local_opponent_ports.delete port
  end
  local_opponent_ports
end

#opponent_seats(opponent_name) ⇒ Object



404
405
406
# File 'lib/acpc_table_manager/match.rb', line 404

def opponent_seats(opponent_name)
  opponent_seats_with_condition { |player_name| player_name == opponent_name }
end

#opponent_seats_with_conditionObject



399
400
401
402
403
# File 'lib/acpc_table_manager/match.rb', line 399

def opponent_seats_with_condition
  player_names.each_index.select do |i|
    yield player_names[i]
  end.map { |s| s + 1 } - [self.seat]
end

#player_namesObject



383
384
385
# File 'lib/acpc_table_manager/match.rb', line 383

def player_names
  opponent_names.dup.insert seat-1, self.user_name
end

#proxy_running?Boolean

Returns:

  • (Boolean)


364
365
366
# File 'lib/acpc_table_manager/match.rb', line 364

def proxy_running?
  self.proxy_pid && self.proxy_pid > 0 && AcpcDealer::process_exists?(self.proxy_pid)
end

#queueObject

Returns The matches to be started (have not been started and not currently running) ordered from newest to oldest.

Returns:

  • The matches to be started (have not been started and not currently running) ordered from newest to oldest.



55
# File 'lib/acpc_table_manager/match.rb', line 55

scope :queue, not_started.and.ready_to_start.desc(:updated_at)

#rejoinable_seats(user_name) ⇒ Object



419
420
421
422
423
424
425
# File 'lib/acpc_table_manager/match.rb', line 419

def rejoinable_seats(user_name)
  (
    opponent_seats(user_name) -
    # Remove seats already taken by players who have already joined this match
    self.class().where(name: self.name).ne(name_from_user: self.name).map { |m| m.seat }
  )
end

#running?Boolean

Returns:

  • (Boolean)


360
# File 'lib/acpc_table_manager/match.rb', line 360

def running?() dealer_running? && proxy_running? end

#sanitized_nameObject



426
427
428
# File 'lib/acpc_table_manager/match.rb', line 426

def sanitized_name
  Zaru.sanitize!(Shellwords.escape(self.name.gsub(/\s+/, '_')))
end

#set_dealer_options!(options) ⇒ Object

Initializers



281
282
283
284
# File 'lib/acpc_table_manager/match.rb', line 281

def set_dealer_options!(options)
  self.dealer_options = (options.split(' ').map { |o| Shellwords.escape o }.join(' ') || '')
  self
end

#set_game_definition_file_name!(file_name = ) ⇒ Object



298
299
300
301
# File 'lib/acpc_table_manager/match.rb', line 298

def set_game_definition_file_name!(file_name = game_info['file'])
  self.game_definition_file_name = file_name
  self
end

#set_game_definition_hash!(hash = self.game_def_hash) ⇒ Object



302
303
304
# File 'lib/acpc_table_manager/match.rb', line 302

def set_game_definition_hash!(hash = self.game_def_hash)
  self.game_def_hash = hash || game_def_hash_from_key
end

#set_name!(name_ = self.name_from_user) ⇒ Object



285
286
287
288
289
290
# File 'lib/acpc_table_manager/match.rb', line 285

def set_name!(name_ = self.name_from_user)
  name_from_user_ = name_.strip
  self.name = name_from_user_
  self.name_from_user = name_from_user_
  self
end

#set_seat!(seat_ = self.seat) ⇒ Object



291
292
293
294
295
296
297
# File 'lib/acpc_table_manager/match.rb', line 291

def set_seat!(seat_ = self.seat)
  self.seat = seat_ || self.class().new_random_seat(game_info['num_players'])
  if self.seat > game_info['num_players']
    self.seat = game_info['num_players']
  end
  self
end

#started?Boolean

Returns:

  • (Boolean)


358
# File 'lib/acpc_table_manager/match.rb', line 358

def started?() !self.slices.empty? end

#users_portObject



391
392
393
# File 'lib/acpc_table_manager/match.rb', line 391

def users_port
  port_numbers[seat - 1]
end