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,203 @@
1
+ require 'delegate'
2
+ require 'timeout'
3
+
4
+ require 'acpc_poker_types/match_state'
5
+ require 'acpc_poker_types/game_definition'
6
+ require 'acpc_poker_types/hand'
7
+
8
+ require 'contextual_exceptions'
9
+ using ContextualExceptions::ClassRefinement
10
+
11
+ require_relative 'match'
12
+
13
+ module AcpcTableManager
14
+ class MatchView < SimpleDelegator
15
+ include AcpcPokerTypes
16
+
17
+ exceptions :unable_to_find_next_slice
18
+
19
+ attr_reader :match, :slice_index, :messages_to_display
20
+ attr_writer :messages_to_display
21
+
22
+ def self.chip_contributions_in_previous_rounds(player, round)
23
+ if round > 0
24
+ player['chip_contributions'][0..round-1].inject(:+)
25
+ else
26
+ 0
27
+ end
28
+ end
29
+
30
+ DEFAULT_WAIT_FOR_SLICE_TIMEOUT = 0 # seconds
31
+
32
+ def initialize(match_id, slice_index = nil, load_previous_messages: false, timeout: DEFAULT_WAIT_FOR_SLICE_TIMEOUT)
33
+ @match = Match.find(match_id)
34
+ super @match
35
+
36
+ @messages_to_display = []
37
+
38
+ @slice_index = slice_index || @match.last_slice_viewed
39
+
40
+ raise StandardError.new("Illegal slice index: #{@slice_index}") unless @slice_index >= 0
41
+
42
+ unless @slice_index < @match.slices.length
43
+ if timeout > 0
44
+ Timeout.timeout(timeout, UnableToFindNextSlice) do
45
+ while @slice_index >= @match.slices.length do
46
+ sleep 0.5
47
+ @match = Match.find(match_id)
48
+ end
49
+ end
50
+ super @match
51
+ else
52
+ raise UnableToFindNextSlice
53
+ end
54
+ end
55
+
56
+ @messages_to_display = slice.messages
57
+
58
+ @loaded_previous_messages_ = false
59
+
60
+ load_previous_messages! if load_previous_messages
61
+ end
62
+ def user_contributions_in_previous_rounds
63
+ self.class.chip_contributions_in_previous_rounds(user, state.round)
64
+ end
65
+ def state() @state ||= MatchState.parse slice.state_string end
66
+ def slice() slices[@slice_index] end
67
+
68
+ # zero indexed
69
+ def users_seat() @users_seat ||= @match.seat - 1 end
70
+ def betting_sequence() slice.betting_sequence end
71
+ def pot_at_start_of_round() slice.pot_at_start_of_round end
72
+ def hand_ended?() slice.hand_ended? end
73
+ def match_ended?() slice.match_ended? end
74
+ def users_turn_to_act?() slice.users_turn_to_act? end
75
+ def legal_actions
76
+ slice.legal_actions.map do |action|
77
+ AcpcPokerTypes::PokerAction.new(action)
78
+ end
79
+ end
80
+
81
+ # @return [Array<Hash>] Player information ordered by seat.
82
+ # Each player hash should contain
83
+ # values for the following keys:
84
+ # 'name',
85
+ # 'seat'
86
+ # 'chip_stack'
87
+ # 'chip_contributions'
88
+ # 'chip_balance'
89
+ # 'hole_cards'
90
+ def players
91
+ return @players if @players
92
+
93
+ @players = slice.players
94
+ end
95
+ def user
96
+ @user ||= players[users_seat]
97
+ end
98
+ def opponents
99
+ @opponents ||= compute_opponents
100
+ end
101
+ def opponents_sorted_by_position_from_user
102
+ @opponents_sorted_by_position_from_user ||= opponents.sort_by do |opp|
103
+ Seat.new(
104
+ opp['seat'],
105
+ players.length
106
+ ).seats_from(
107
+ users_seat
108
+ )
109
+ end
110
+ end
111
+ def amount_for_next_player_to_call
112
+ @amount_for_next_player_to_call ||= slice.amount_to_call
113
+ end
114
+
115
+ # Over round
116
+ def chip_contribution_for_next_player_after_calling
117
+ @chip_contribution_for_next_player_after_calling ||= slice.chip_contribution_after_calling
118
+ end
119
+
120
+ # Over round
121
+ def minimum_wager_to
122
+ @minimum_wager_to ||= slice.minimum_wager_to
123
+ end
124
+
125
+ # Over round
126
+ def pot_after_call
127
+ @pot_after_call ||= slice.pot_after_call
128
+ end
129
+
130
+ # Over round
131
+ def pot_fraction_wager_to(fraction=1)
132
+ return 0 if hand_ended?
133
+
134
+ [
135
+ [
136
+ (
137
+ fraction * pot_after_call +
138
+ chip_contribution_for_next_player_after_calling
139
+ ),
140
+ minimum_wager_to
141
+ ].max,
142
+ all_in
143
+ ].min.floor
144
+ end
145
+
146
+ # Over round
147
+ def all_in
148
+ @all_in ||= slice.all_in
149
+ end
150
+
151
+ def betting_type_label
152
+ if @betting_type_label.nil?
153
+ @betting_type_label = if no_limit?
154
+ 'nolimit'
155
+ else
156
+ 'limit'
157
+ end
158
+ end
159
+
160
+ @betting_type_label
161
+ end
162
+
163
+ def load_previous_messages!(index = @slice_index)
164
+ @messages_to_display = slices[0...index].inject([]) do |messages, s|
165
+ messages += s.messages
166
+ end + @messages_to_display
167
+ @loaded_previous_messages_ = true
168
+ self
169
+ end
170
+
171
+ def loaded_previous_messages?
172
+ @loaded_previous_messages_
173
+ end
174
+
175
+ private
176
+
177
+ def compute_opponents
178
+ opp = players.dup
179
+ opp.delete_at(users_seat)
180
+ opp
181
+ end
182
+
183
+ def next_slice_without_updating_messages!(max_retries = 0)
184
+ @slice_index += 1
185
+ retries = 0
186
+ if @slice_index >= slices.length && max_retries < 1
187
+ @slice_index -= 1
188
+ raise UnableToFindNextSlice.new("Unable to find next match slice after #{retries} retries")
189
+ end
190
+ while @slice_index >= slices.length do
191
+ sleep(0.1)
192
+ @match = Match.find(@match.id)
193
+ __setobj__ @match
194
+ if retries >= max_retries
195
+ @slice_index -= 1
196
+ raise UnableToFindNextSlice.new("Unable to find next match slice after #{retries} retries")
197
+ end
198
+ retries += 1
199
+ end
200
+ self
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,19 @@
1
+ module AcpcTableManager
2
+ module MonkeyPatches
3
+ module ConversionToEnglish
4
+ def to_english
5
+ gsub '_', ' '
6
+ end
7
+ end
8
+ module StringToEnglishExtension
9
+ refine String do
10
+ include ConversionToEnglish
11
+ end
12
+ end
13
+ module SymbolToEnglishExtension
14
+ refine Symbol do
15
+ include ConversionToEnglish
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ require 'timeout'
2
+ require 'process_runner'
3
+ require_relative 'config'
4
+ require_relative 'simple_logging'
5
+
6
+ module AcpcTableManager
7
+ module Opponents
8
+ extend SimpleLogging
9
+
10
+ @logger = nil
11
+
12
+ # @return [Array<Integer>] PIDs of the opponents started
13
+ def self.start(*bot_start_commands)
14
+ @logger ||= ::AcpcTableManager.new_log 'opponents.log'
15
+ log __method__, num_opponents: bot_start_commands.length
16
+
17
+ bot_start_commands.map do |bot_start_command|
18
+ log(
19
+ __method__,
20
+ {
21
+ bot_start_command_parameters: bot_start_command,
22
+ command_to_be_run: bot_start_command.join(' ')
23
+ }
24
+ )
25
+ pid = Timeout::timeout(3) do
26
+ ProcessRunner.go(bot_start_command)
27
+ end
28
+ log(
29
+ __method__,
30
+ {
31
+ bot_started?: true,
32
+ pid: pid
33
+ }
34
+ )
35
+ pid
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'monkey_patches'
2
+ using AcpcTableManager::MonkeyPatches::StringToEnglishExtension
3
+
4
+ require_relative 'config'
5
+
6
+ module AcpcTableManager
7
+ module ParamRetrieval
8
+ protected
9
+
10
+ # @param [Hash<String, Object>] params Parameter hash
11
+ # @param parameter_key The key of the parameter to be retrieved.
12
+ # @raise
13
+ def retrieve_parameter_or_raise_exception(
14
+ params,
15
+ parameter_key
16
+ )
17
+ raise StandardError.new("nil params hash given") unless params
18
+ retrieved_param = params[parameter_key]
19
+ unless retrieved_param
20
+ raise StandardError.new("No #{parameter_key.to_english} provided")
21
+ end
22
+ retrieved_param
23
+ end
24
+
25
+ # @param [Hash<String, Object>] params Parameter hash
26
+ # @raise (see #param)
27
+ def retrieve_match_id_or_raise_exception(params)
28
+ AcpcTableManager.raise_if_uninitialized
29
+ retrieve_parameter_or_raise_exception params, AcpcTableManager.config.match_id_key
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,276 @@
1
+ require_relative 'config'
2
+ require_relative 'match'
3
+ require_relative 'match_slice'
4
+
5
+ require 'acpc_poker_player_proxy'
6
+ require 'acpc_poker_types'
7
+
8
+ require_relative 'simple_logging'
9
+ using SimpleLogging::MessageFormatting
10
+
11
+ require 'contextual_exceptions'
12
+ using ContextualExceptions::ClassRefinement
13
+
14
+ module AcpcTableManager
15
+
16
+ class Proxy
17
+ include SimpleLogging
18
+ include AcpcPokerTypes
19
+
20
+ exceptions :unable_to_create_match_slice
21
+
22
+ def self.start(match)
23
+ game_definition = GameDefinition.parse_file(match.game_definition_file_name)
24
+ match.game_def_hash = game_definition.to_h
25
+ match.save!
26
+
27
+ proxy = new(
28
+ match.id,
29
+ AcpcDealer::ConnectionInformation.new(
30
+ match.port_numbers[match.seat - 1],
31
+ ::AcpcTableManager.config.dealer_host
32
+ ),
33
+ match.seat - 1,
34
+ game_definition,
35
+ match.player_names.join(' '),
36
+ match.number_of_hands
37
+ ) do |players_at_the_table|
38
+ yield players_at_the_table if block_given?
39
+ end
40
+ end
41
+
42
+ # @todo Reduce the # of params
43
+ #
44
+ # @param [String] match_id The ID of the match in which this player is participating.
45
+ # @param [DealerInformation] dealer_information Information about the dealer to which this bot should connect.
46
+ # @param [GameDefinition, #to_s] game_definition A game definition; either a +GameDefinition+ or the name of the file containing a game definition.
47
+ # @param [String] player_names The names of the players in this match.
48
+ # @param [Integer] number_of_hands The number of hands in this match.
49
+ def initialize(
50
+ match_id,
51
+ dealer_information,
52
+ users_seat,
53
+ game_definition,
54
+ player_names='user p2',
55
+ number_of_hands=1
56
+ )
57
+ @logger = AcpcTableManager.new_log File.join('proxies', "#{match_id}.#{users_seat}.log")
58
+
59
+ log __method__, {
60
+ dealer_information: dealer_information,
61
+ users_seat: users_seat,
62
+ game_definition: game_definition,
63
+ player_names: player_names,
64
+ number_of_hands: number_of_hands
65
+ }
66
+
67
+ @match_id = match_id
68
+ @player_proxy = AcpcPokerPlayerProxy::PlayerProxy.new(
69
+ dealer_information,
70
+ game_definition,
71
+ users_seat
72
+ ) do |players_at_the_table|
73
+
74
+ if players_at_the_table.match_state
75
+ update_database! players_at_the_table
76
+
77
+ yield players_at_the_table if block_given?
78
+ else
79
+ log __method__, {before_first_match_state: true}
80
+ end
81
+ end
82
+ end
83
+
84
+ # Player action interface
85
+ # @see PlayerProxy#play!
86
+ def play!(action)
87
+ log __method__, action: action
88
+
89
+ action = PokerAction.new(action) unless action.is_a?(PokerAction)
90
+
91
+ @player_proxy.play! action do |players_at_the_table|
92
+ update_database! players_at_the_table
93
+
94
+ yield players_at_the_table if block_given?
95
+ end
96
+
97
+ log(
98
+ __method__,
99
+ {
100
+ users_turn_to_act?: @player_proxy.users_turn_to_act?,
101
+ match_ended?: @player_proxy.match_ended?
102
+ }
103
+ )
104
+
105
+ self
106
+ end
107
+
108
+ # @see PlayerProxy#match_ended?
109
+ def match_ended?
110
+ return false if @player_proxy.nil?
111
+
112
+ @match ||= Match.find(@match_id)
113
+
114
+ @player_proxy.match_ended? ||
115
+ (
116
+ @player_proxy.hand_ended? &&
117
+ @player_proxy.match_state.hand_number >= @match.number_of_hands - 1
118
+ )
119
+ end
120
+
121
+ private
122
+
123
+ def update_database!(players_at_the_table)
124
+ @match = Match.find(@match_id)
125
+
126
+ begin
127
+ MatchSlice.from_players_at_the_table!(
128
+ players_at_the_table,
129
+ match_ended?,
130
+ @match
131
+ )
132
+
133
+ new_slice = @match.slices.last
134
+ new_slice.messages = []
135
+
136
+ ms = players_at_the_table.match_state
137
+
138
+ log(
139
+ __method__,
140
+ {
141
+ first_state_of_first_round?: ms.first_state_of_first_round?
142
+ }
143
+ )
144
+
145
+ if ms.first_state_of_first_round?
146
+ new_slice.messages << hand_dealt_description(
147
+ @match.player_names,
148
+ ms.hand_number + 1,
149
+ players_at_the_table.game_def,
150
+ @match.number_of_hands
151
+ )
152
+ end
153
+
154
+ last_action = ms.betting_sequence(
155
+ players_at_the_table.game_def
156
+ ).flatten.last
157
+
158
+ log(
159
+ __method__,
160
+ {
161
+ last_action: last_action
162
+ }
163
+ )
164
+
165
+ if last_action
166
+ last_actor = @match.player_names[
167
+ @match.slices[-2].seat_next_to_act
168
+ ]
169
+
170
+ log(
171
+ __method__,
172
+ {
173
+ last_actor: last_actor
174
+ }
175
+ )
176
+
177
+ case last_action.to_acpc_character
178
+ when PokerAction::CHECK
179
+ new_slice.messages << check_description(
180
+ last_actor
181
+ )
182
+ when PokerAction::CALL
183
+ new_slice.messages << call_description(
184
+ last_actor,
185
+ last_action
186
+ )
187
+ when PokerAction::BET
188
+ new_slice.messages << bet_description(
189
+ last_actor,
190
+ last_action
191
+ )
192
+ when PokerAction::RAISE
193
+ new_slice.messages << if @match.no_limit?
194
+ no_limit_raise_description(
195
+ last_actor,
196
+ last_action,
197
+ @match.slices[-2].amount_to_call
198
+ )
199
+ else
200
+ limit_raise_description(
201
+ last_actor,
202
+ last_action,
203
+ ms.players(players_at_the_table.game_def).num_wagers(ms.round) - 1,
204
+ players_at_the_table.game_def.max_number_of_wagers[ms.round]
205
+ )
206
+ end
207
+ when PokerAction::FOLD
208
+ new_slice.messages << fold_description(
209
+ last_actor
210
+ )
211
+ end
212
+ end
213
+
214
+ log(
215
+ __method__,
216
+ {
217
+ hand_ended?: players_at_the_table.hand_ended?
218
+ }
219
+ )
220
+
221
+ if players_at_the_table.hand_ended?
222
+ log(
223
+ __method__,
224
+ {
225
+ reached_showdown?: ms.reached_showdown?
226
+ }
227
+ )
228
+
229
+ if ms.reached_showdown?
230
+ players_at_the_table.players.each_with_index do |player, i|
231
+ hd = PileOfCards.new(
232
+ player.hand +
233
+ ms.community_cards.flatten
234
+ ).to_poker_hand_description
235
+ new_slice.messages << "#{@match.player_names[i]} shows #{hd}"
236
+ end
237
+ end
238
+ winning_players = new_slice.players.select do |player|
239
+ player['winnings'] > 0
240
+ end
241
+ if winning_players.length > 1
242
+ new_slice.messages << split_pot_description(
243
+ winning_players.map { |player| player['name'] },
244
+ ms.pot(players_at_the_table.game_def)
245
+ )
246
+ else
247
+ winnings = winning_players.first['winnings']
248
+ if winnings.to_i == winnings
249
+ winnings = winnings.to_i
250
+ end
251
+ chip_balance = winning_players.first['chip_balance']
252
+ if chip_balance.to_i == chip_balance
253
+ chip_balance = chip_balance.to_i
254
+ end
255
+
256
+ new_slice.messages << hand_win_description(
257
+ winning_players.first['name'],
258
+ winnings,
259
+ chip_balance - winnings
260
+ )
261
+ end
262
+ end
263
+
264
+ new_slice.save!
265
+
266
+ # Since creating a new slice doesn't "update" the match for some reason
267
+ @match.update_attribute(:updated_at, Time.now)
268
+ @match.save!
269
+ rescue => e
270
+ raise UnableToCreateMatchSlice.with_context('Unable to create match slice', e)
271
+ end
272
+
273
+ self
274
+ end
275
+ end
276
+ end