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