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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +58 -0
- data/Rakefile +10 -0
- data/acpc_table_manager.gemspec +55 -0
- data/bin/console +16 -0
- data/bin/setup +7 -0
- data/exe/acpc_table_manager +63 -0
- data/lib/acpc_table_manager.rb +17 -0
- data/lib/acpc_table_manager/config.rb +180 -0
- data/lib/acpc_table_manager/dealer.rb +57 -0
- data/lib/acpc_table_manager/match.rb +350 -0
- data/lib/acpc_table_manager/match_slice.rb +196 -0
- data/lib/acpc_table_manager/match_view.rb +203 -0
- data/lib/acpc_table_manager/monkey_patches.rb +19 -0
- data/lib/acpc_table_manager/opponents.rb +39 -0
- data/lib/acpc_table_manager/param_retrieval.rb +32 -0
- data/lib/acpc_table_manager/proxy.rb +276 -0
- data/lib/acpc_table_manager/simple_logging.rb +54 -0
- data/lib/acpc_table_manager/table_manager.rb +260 -0
- data/lib/acpc_table_manager/table_queue.rb +379 -0
- data/lib/acpc_table_manager/utils.rb +34 -0
- data/lib/acpc_table_manager/version.rb +3 -0
- metadata +311 -0
@@ -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
|