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,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