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