acpc_dealer_data 0.3.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a1b82be7131e77192adfdf5cc580289a1f85facf
4
+ data.tar.gz: 97cd0e6a637586fd999c83e47d27c7516386fdde
5
+ SHA512:
6
+ metadata.gz: 74054e4d315506f413864776bdf18df98e27093c1e401ce536971a6977672ddaa0b9e4272b2de09ff9cdacd09ef53326de18266d60994fe65955e45bc6d48f55
7
+ data.tar.gz: 84fb2b82c348817ae76794e5864e1339af37dea0794bb25493de65fcfc29613c7a146ddc7f55dc287b05946a7ee27241f08f474fa2081fb3ec4757996b1b4aff
data/Rakefile CHANGED
@@ -7,6 +7,6 @@ require 'rake/testtask'
7
7
  Rake::TestTask.new do |t|
8
8
  t.libs << "lib" << 'spec/support'
9
9
  t.test_files = FileList['spec/**/*_spec.rb']
10
- t.verbose = true
11
- t.warning = true
10
+ t.verbose = false
11
+ t.warning = false # pry-rescue has a lot of warnings
12
12
  end
@@ -8,15 +8,17 @@ Gem::Specification.new do |gem|
8
8
  gem.summary = %q{Gem to parse, manipulate, and use data from the ACPC Dealer program.}
9
9
  gem.homepage = "https://github.com/dmorrill10/acpc_dealer_data"
10
10
 
11
- gem.add_dependency 'acpc_dealer'
12
- gem.add_dependency 'acpc_poker_types'
13
- gem.add_dependency 'celluloid'
14
- gem.add_dependency 'dmorrill10-utils', '>=1.0.0'
11
+ gem.add_dependency 'acpc_dealer', '~> 0.0'
12
+ gem.add_dependency 'acpc_poker_types', '~> 0.0'
13
+ gem.add_dependency 'celluloid', '~> 0.13'
14
+ gem.add_dependency 'dmorrill10-utils', '~> 1.0'
15
15
 
16
-
17
- gem.add_development_dependency 'mocha'
18
- gem.add_development_dependency 'simplecov'
19
- gem.add_development_dependency 'turn'
16
+ gem.add_development_dependency 'minitest', '~> 4.7'
17
+ gem.add_development_dependency 'mocha', '~> 0.13'
18
+ gem.add_development_dependency 'simplecov', '~> 0.7'
19
+ gem.add_development_dependency 'turn', '~> 0.9'
20
+ gem.add_development_dependency 'pry-rescue', '~> 1.1'
21
+ gem.add_development_dependency 'awesome_print', '~> 1.1'
20
22
 
21
23
  gem.files = `git ls-files`.split($\)
22
24
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -5,6 +5,7 @@ require 'acpc_dealer_data/hand_data'
5
5
  require 'acpc_dealer_data/hand_results'
6
6
  require 'acpc_dealer_data/poker_match_data'
7
7
  require 'acpc_dealer_data/match_definition'
8
+ require 'acpc_dealer_data/log_file'
8
9
 
9
10
  module AcpcDealerData
10
11
  end
@@ -2,14 +2,14 @@
2
2
  require 'acpc_poker_types/match_state'
3
3
  require 'acpc_poker_types/poker_action'
4
4
 
5
- require_relative 'match_definition'
6
-
7
- class ActionMessages
5
+ require 'acpc_dealer_data/match_definition'
6
+ require 'acpc_dealer_data/log_file'
8
7
 
8
+ class AcpcDealerData::ActionMessages
9
9
  attr_reader(
10
10
  :data, :final_score, :match_def
11
11
  )
12
-
12
+
13
13
  ToMessage = Struct.new(
14
14
  # @returns [Integer] Seat of the player receiving the message
15
15
  :seat,
@@ -40,14 +40,16 @@ class ActionMessages
40
40
  end
41
41
  end
42
42
 
43
+ CONCATONATED_ACTIONS = PokerAction::LEGAL_ACPC_CHARACTERS.to_a.join('')
44
+
43
45
  # @param [String] from_message FROM message (message from player)
44
46
  def self.parse_from_message(from_message)
45
47
  if from_message.strip.match(
46
- /^FROM\s*(\d+)\s*at\s*[\d\.]+\s*(#{MatchState::LABEL}\S+):([#{PokerAction::LEGAL_ACPC_CHARACTERS.to_a.join('')}]\s*\d*)$/
48
+ /^FROM\s*(\d+)\s*at\s*[\d\.]+\s*(#{MatchState::LABEL}\S+):([#{CONCATONATED_ACTIONS}]\s*\d*)$/
47
49
  )
48
50
  FromMessage.new(
49
- $1.to_i - 1,
50
- MatchState.parse($2),
51
+ $1.to_i - 1,
52
+ MatchState.parse($2),
51
53
  PokerAction.new($3)
52
54
  )
53
55
  else
@@ -59,10 +61,10 @@ class ActionMessages
59
61
  if score_string.strip.match(
60
62
  /^SCORE:([\d\-\.|]+):([\w|]+)$/
61
63
  )
62
-
64
+
63
65
  stack_changes = $1.split '|'
64
66
  players = $2.split '|'
65
-
67
+
66
68
  players.each_index.inject({}) do |player_results, j|
67
69
  player_results[players[j].to_sym] = stack_changes[j].to_r
68
70
  player_results
@@ -73,45 +75,53 @@ class ActionMessages
73
75
  end
74
76
 
75
77
  def self.parse_to_or_from_message(message)
76
- parsed_message = ActionMessages.parse_to_message(message)
78
+ parsed_message = AcpcDealerData::ActionMessages.parse_to_message(message)
77
79
  if parsed_message.nil?
78
- ActionMessages.parse_from_message(message)
80
+ AcpcDealerData::ActionMessages.parse_from_message(message)
79
81
  else
80
82
  parsed_message
81
83
  end
82
84
  end
83
85
 
84
- class LogFile < File
85
- end
86
-
87
- def self.parse_file(acpc_log_file_path, player_names, game_def_directory, num_hands=nil)
88
- LogFile.open(acpc_log_file_path, 'r') do |file|
89
- ActionMessages.parse file, player_names, game_def_directory, num_hands
86
+ def self.parse_file(
87
+ acpc_log_file_path,
88
+ player_names,
89
+ game_def_directory,
90
+ num_hands=nil
91
+ )
92
+ AcpcDealerData::LogFile.open(acpc_log_file_path, 'r') do |file|
93
+ AcpcDealerData::ActionMessages.parse file, player_names, game_def_directory, num_hands
90
94
  end
91
95
  end
92
96
 
93
97
  alias_new :parse
94
98
 
95
- def initialize(acpc_log_statements, player_names, game_def_directory, num_hands=nil)
99
+ def initialize(
100
+ acpc_log_statements,
101
+ player_names,
102
+ game_def_directory,
103
+ num_hands=nil
104
+ )
96
105
  @final_score = nil
97
106
  @match_def = nil
98
107
  @data = acpc_log_statements.inject([]) do |accumulating_data, log_line|
99
108
  if @match_def.nil?
100
- @match_def = MatchDefinition.parse(log_line, player_names, game_def_directory)
109
+ @match_def = AcpcDealerData::MatchDefinition.parse(log_line, player_names, game_def_directory)
101
110
  else
102
- parsed_message = ActionMessages.parse_to_or_from_message(log_line)
111
+ parsed_message = AcpcDealerData::ActionMessages.parse_to_or_from_message(log_line)
103
112
  if parsed_message
104
113
  if (
105
- accumulating_data.empty? ||
114
+ accumulating_data.empty? ||
106
115
  accumulating_data.last.first[:state].hand_number != parsed_message[:state].hand_number
107
116
  )
108
117
  break accumulating_data if accumulating_data.length == num_hands
118
+
109
119
  accumulating_data << []
110
120
  end
111
121
 
112
122
  accumulating_data.last << parsed_message
113
123
  else
114
- @final_score = ActionMessages.parse_score(log_line) unless @final_score
124
+ @final_score = AcpcDealerData::ActionMessages.parse_score(log_line) unless @final_score
115
125
  end
116
126
  end
117
127
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  require 'dmorrill10-utils/class'
3
3
 
4
- require_relative 'match_definition'
4
+ require 'acpc_dealer_data/match_definition'
5
5
 
6
6
  # Monkey patch for easy boundary checking
7
7
  class Array
@@ -10,15 +10,15 @@ class Array
10
10
  end
11
11
  end
12
12
 
13
- class HandData
13
+ class AcpcDealerData::HandData
14
14
 
15
15
  exceptions :match_definitions_do_not_match, :invalid_data
16
16
 
17
- attr_reader(
17
+ attr_accessor(
18
18
  # @returns [Array<Numeric>] Chip distribution at the end of the hand
19
19
  :chip_distribution,
20
20
  # @returns [MatchDefinition] Game definition and match parameters
21
- :match_def,
21
+ :match_def,
22
22
  # @returns [Integer] Zero-index turn number within the hand
23
23
  :turn_number,
24
24
  # @returns [Turn] Turn data
@@ -37,7 +37,7 @@ class HandData
37
37
 
38
38
  def initialize(match_def, action_data, result)
39
39
  @match_def = match_def
40
-
40
+
41
41
  set_chip_distribution! result
42
42
 
43
43
  set_data! action_data
@@ -134,7 +134,7 @@ class HandData
134
134
 
135
135
  message_number += number_of_state_messages
136
136
 
137
- action_message = if action_data.in_bounds?(message_number) &&
137
+ action_message = if action_data.in_bounds?(message_number) &&
138
138
  action_data[message_number].respond_to?(:action)
139
139
 
140
140
  message_number += 1
@@ -160,7 +160,7 @@ class HandData
160
160
 
161
161
  def assert_messages_have_no_actions(state_messages)
162
162
  if state_messages.any? { |message| message.respond_to?(:action) }
163
- raise InvalidData, state_messages.find do |message|
163
+ raise InvalidData, state_messages.find do |message|
164
164
  !message.action.nil?
165
165
  end.inspect
166
166
  end
@@ -173,7 +173,7 @@ class HandData
173
173
  end
174
174
 
175
175
  def assert_message_is_from_final_turn(action_data, message_number, state_messages)
176
- if action_data.in_bounds?(message_number+1) &&
176
+ if action_data.in_bounds?(message_number+1) &&
177
177
  state_messages.last.round == action_data[message_number+1].state.round
178
178
  raise InvalidData, action_data[message_number].inspect
179
179
  end
@@ -1,20 +1,20 @@
1
1
 
2
2
  require 'dmorrill10-utils/class'
3
3
 
4
- require_relative 'match_definition'
5
-
6
- class HandResults
4
+ require 'acpc_dealer_data/match_definition'
5
+ require 'acpc_dealer_data/log_file'
7
6
 
7
+ class AcpcDealerData::HandResults
8
8
  attr_reader :data, :final_score, :match_def
9
9
 
10
10
  def self.parse_state(state_string)
11
11
  if state_string.strip.match(
12
12
  /^STATE:\d+:[cfr\d\/]+:[^:]+:([\d\-\.|]+):([\w|]+)$/
13
13
  )
14
-
14
+
15
15
  stack_changes = $1.split '|'
16
16
  players = $2.split '|'
17
-
17
+
18
18
  players.each_index.inject({}) do |player_results, j|
19
19
  player_results[players[j].to_sym] = stack_changes[j].to_r
20
20
  player_results
@@ -28,10 +28,10 @@ class HandResults
28
28
  if score_string.strip.match(
29
29
  /^SCORE:([\d\-\.|]+):([\w|]+)$/
30
30
  )
31
-
31
+
32
32
  stack_changes = $1.split '|'
33
33
  players = $2.split '|'
34
-
34
+
35
35
  players.each_index.inject({}) do |player_results, j|
36
36
  player_results[players[j].to_sym] = stack_changes[j].to_r
37
37
  player_results
@@ -41,12 +41,14 @@ class HandResults
41
41
  end
42
42
  end
43
43
 
44
- class LogFile < File
45
- end
46
-
47
- def self.parse_file(acpc_log_file_path, player_names, game_def_directory, num_hands=nil)
48
- LogFile.open(acpc_log_file_path, 'r') do |file|
49
- HandResults.parse file, player_names, game_def_directory, num_hands
44
+ def self.parse_file(
45
+ acpc_log_file_path,
46
+ player_names,
47
+ game_def_directory,
48
+ num_hands=nil
49
+ )
50
+ AcpcDealerData::LogFile.open(acpc_log_file_path, 'r') do |file|
51
+ AcpcDealerData::HandResults.parse file, player_names, game_def_directory, num_hands
50
52
  end
51
53
  end
52
54
 
@@ -55,20 +57,20 @@ class HandResults
55
57
  def initialize(acpc_log_statements, player_names, game_def_directory, num_hands=nil)
56
58
  @final_score = nil
57
59
  @match_def = nil
58
-
60
+
59
61
  @data = acpc_log_statements.inject([]) do |accumulating_data, log_line|
60
62
  if @match_def.nil?
61
- @match_def = MatchDefinition.parse(log_line, player_names, game_def_directory)
63
+ @match_def = AcpcDealerData::MatchDefinition.parse(log_line, player_names, game_def_directory)
62
64
  else
63
- parsed_message = HandResults.parse_state(log_line)
65
+ parsed_message = AcpcDealerData::HandResults.parse_state(log_line)
64
66
  if parsed_message
65
67
  accumulating_data << parsed_message
66
68
  break accumulating_data if accumulating_data.length == num_hands
67
69
  else
68
- @final_score = HandResults.parse_score(log_line) unless @final_score
70
+ @final_score = AcpcDealerData::HandResults.parse_score(log_line) unless @final_score
69
71
  end
70
72
  end
71
-
73
+
72
74
  accumulating_data
73
75
  end
74
76
  end
@@ -0,0 +1,3 @@
1
+ # Wrapper class to enable mocking log files in tests
2
+ class AcpcDealerData::LogFile < File
3
+ end
@@ -5,7 +5,7 @@ require 'acpc_poker_types/game_definition'
5
5
 
6
6
  require 'dmorrill10-utils/class'
7
7
 
8
- class MatchDefinition
8
+ class AcpcDealerData::MatchDefinition
9
9
 
10
10
  exceptions :unable_to_parse, :incorrect_number_of_player_names
11
11
 
@@ -14,19 +14,15 @@ class MatchDefinition
14
14
  def self.parse(acpc_log_string, player_names, game_def_directory)
15
15
  if acpc_log_string.strip.match(
16
16
  '^\s*#\s*name/game/hands/seed\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s*$'
17
- )
17
+ )
18
18
  name = $1
19
- game_def = GameDefinition.parse_file(File.join(game_def_directory, File.basename($2)))
19
+ game_def = GameDefinition.parse_file(
20
+ File.join(game_def_directory, File.basename($2))
21
+ )
20
22
  number_of_hands = $3
21
23
  random_seed = $4
22
24
 
23
- MatchDefinition.new(
24
- name,
25
- game_def,
26
- number_of_hands,
27
- random_seed,
28
- player_names
29
- )
25
+ new(name, game_def, number_of_hands, random_seed, player_names)
30
26
  else
31
27
  nil
32
28
  end
@@ -1,67 +1,77 @@
1
1
 
2
2
  require 'acpc_poker_types/player'
3
3
 
4
- require 'celluloid'
4
+ require 'celluloid/autostart'
5
5
 
6
6
  require 'dmorrill10-utils/class'
7
7
 
8
- require_relative 'action_messages'
9
- require_relative 'hand_data'
10
- require_relative 'hand_results'
11
- require_relative 'match_definition'
8
+ require 'acpc_dealer_data/action_messages'
9
+ require 'acpc_dealer_data/hand_data'
10
+ require 'acpc_dealer_data/hand_results'
11
+ require 'acpc_dealer_data/match_definition'
12
12
 
13
- class PokerMatchData
13
+ class AcpcDealerData::PokerMatchData
14
14
 
15
15
  exceptions :match_definitions_do_not_match, :final_scores_do_not_match, :player_data_inconsistent
16
16
 
17
- attr_reader(
17
+ attr_accessor(
18
18
  # @returns [Array<Numeric>] Chip distribution at the end of the match
19
19
  :chip_distribution,
20
20
  # @returns [MatchDefinition] Game definition and match parameters
21
21
  :match_def,
22
22
  # @returns [Integer] Zero-index turn number within the hand
23
23
  :hand_number,
24
- # @returns [HandData] Data from each hand
24
+ # @returns [AcpcDealerData::HandData] Data from each hand
25
25
  :data,
26
26
  # @returns [Array<Player>] Player information
27
- :players
28
- )
29
- attr_accessor(
27
+ :players,
30
28
  # @returns [Integer] Seat of the active player
31
29
  :seat
32
30
  )
33
31
 
34
- # @returns [PokerMatchData]
35
- def self.parse_files(action_messages_file, result_messages_file, player_names, dealer_directory, num_hands=nil)
32
+ # @returns [AcpcDealerData::PokerMatchData]
33
+ def self.parse_files(
34
+ action_messages_file,
35
+ result_messages_file,
36
+ player_names,
37
+ dealer_directory,
38
+ num_hands=nil
39
+ )
36
40
  parsed_action_messages = Celluloid::Future.new do
37
- ActionMessages.parse_file(
38
- action_messages_file,
39
- player_names,
41
+ AcpcDealerData::ActionMessages.parse_file(
42
+ action_messages_file,
43
+ player_names,
40
44
  dealer_directory,
41
45
  num_hands
42
46
  )
43
47
  end
44
- parsed_hand_results = Celluloid::Future.new do
45
- HandResults.parse_file(
46
- result_messages_file,
47
- player_names,
48
+ parsed_hand_results = Celluloid::Future.new do
49
+ AcpcDealerData::HandResults.parse_file(
50
+ result_messages_file,
51
+ player_names,
48
52
  dealer_directory,
49
53
  num_hands
50
54
  )
51
55
  end
52
56
 
53
- PokerMatchData.new(
54
- parsed_action_messages.value,
55
- parsed_hand_results.value,
56
- player_names,
57
+ new(
58
+ parsed_action_messages.value,
59
+ parsed_hand_results.value,
60
+ player_names,
57
61
  dealer_directory
58
62
  )
59
63
  end
60
64
 
61
- # @returns [PokerMatchData]
62
- def self.parse(action_messages, result_messages, player_names, dealer_directory, num_hands=nil)
65
+ # @returns [AcpcDealerData::PokerMatchData]
66
+ def self.parse(
67
+ action_messages,
68
+ result_messages,
69
+ player_names,
70
+ dealer_directory,
71
+ num_hands=nil
72
+ )
63
73
  parsed_action_messages = Celluloid::Future.new do
64
- ActionMessages.parse(
74
+ AcpcDealerData::ActionMessages.parse(
65
75
  action_messages,
66
76
  player_names,
67
77
  dealer_directory,
@@ -69,7 +79,7 @@ class PokerMatchData
69
79
  )
70
80
  end
71
81
  parsed_hand_results = Celluloid::Future.new do
72
- HandResults.parse(
82
+ AcpcDealerData::HandResults.parse(
73
83
  result_messages,
74
84
  player_names,
75
85
  dealer_directory,
@@ -77,7 +87,7 @@ class PokerMatchData
77
87
  )
78
88
  end
79
89
 
80
- PokerMatchData.new(
90
+ new(
81
91
  parsed_action_messages.value,
82
92
  parsed_hand_results.value,
83
93
  player_names,
@@ -85,7 +95,12 @@ class PokerMatchData
85
95
  )
86
96
  end
87
97
 
88
- def initialize(parsed_action_messages, parsed_hand_results, player_names, dealer_directory)
98
+ def initialize(
99
+ parsed_action_messages,
100
+ parsed_hand_results,
101
+ player_names,
102
+ dealer_directory
103
+ )
89
104
  if (
90
105
  parsed_action_messages.match_def.nil? ||
91
106
  parsed_hand_results.match_def.nil? ||
@@ -127,7 +142,7 @@ class PokerMatchData
127
142
  end
128
143
 
129
144
  def player(seat=@seat) @players[seat] end
130
-
145
+
131
146
  def for_every_hand!
132
147
  initialize_players!
133
148
 
@@ -166,7 +181,7 @@ class PokerMatchData
166
181
 
167
182
  if (
168
183
  player.active? &&
169
- !match_state.first_state_of_first_round? &&
184
+ !match_state.first_state_of_first_round? &&
170
185
  match_state.round > last_match_state.round
171
186
  )
172
187
  player.start_new_round!
@@ -185,27 +200,28 @@ class PokerMatchData
185
200
  self
186
201
  end
187
202
 
203
+ def match_has_another_round?(current_round, turn_index, turns_taken)
204
+ new_round?(current_round, turn_index) ||
205
+ players_all_in?(current_round, turn_index, turns_taken)
206
+ end
207
+
208
+ def hand_started?
209
+ @hand_number &&
210
+ current_hand.turn_number &&
211
+ current_hand.turn_number > 0
212
+ end
213
+
188
214
  def player_acting_sequence
189
215
  sequence = [[]]
190
-
191
- if (
192
- @hand_number.nil? ||
193
- current_hand.turn_number.nil? ||
194
- current_hand.turn_number < 1
195
- )
196
- return sequence
197
- end
198
-
216
+
217
+ return sequence unless hand_started?
218
+
199
219
  turns_taken = current_hand.data[0..current_hand.turn_number-1]
200
220
  turns_taken.each_with_index do |turn, turn_index|
201
221
  next unless turn.action_message
202
222
 
203
223
  sequence[turn.action_message.state.round] << turn.action_message.seat
204
-
205
- if (
206
- new_round?(sequence.length - 1 , turn_index) ||
207
- players_all_in?(sequence.length - 1, turn_index, turns_taken)
208
- )
224
+ if match_has_another_round?(sequence.length - 1, turn_index, turns_taken)
209
225
  sequence << []
210
226
  end
211
227
  end
@@ -247,7 +263,7 @@ class PokerMatchData
247
263
  end
248
264
  def opponents_cards_visible?
249
265
  return false unless current_hand
250
-
266
+
251
267
  current_hand.current_match_state.list_of_hole_card_hands.reject_empty_elements.length > 1
252
268
  end
253
269
  def player_with_dealer_button
@@ -278,15 +294,15 @@ class PokerMatchData
278
294
  end
279
295
  def betting_sequence
280
296
  sequence = [[]]
281
-
297
+
282
298
  if (
283
- @hand_number.nil? ||
284
- current_hand.turn_number.nil? ||
299
+ @hand_number.nil? ||
300
+ current_hand.turn_number.nil? ||
285
301
  current_hand.turn_number < 1
286
302
  )
287
303
  return sequence
288
304
  end
289
-
305
+
290
306
  turns_taken = current_hand.data[0..current_hand.turn_number-1]
291
307
  turns_taken.each_with_index do |turn, turn_index|
292
308
  next unless turn.action_message
@@ -315,13 +331,13 @@ class PokerMatchData
315
331
  # @match_def.game_def.min_wagers[current_hand.next_state.round]
316
332
  # ChipStack.new [@min_wager.to_i, action_with_context.amount_to_put_in_pot.to_i].max
317
333
  # end
318
-
334
+
319
335
  protected
320
336
 
321
337
  def initialize_players!
322
338
  @players = @match_def.player_names.length.times.map do |seat|
323
339
  Player.join_match(
324
- @match_def.player_names[seat],
340
+ @match_def.player_names[seat],
325
341
  seat,
326
342
  @match_def.game_def.chip_stacks[seat]
327
343
  )
@@ -346,7 +362,7 @@ class PokerMatchData
346
362
  def set_data!(parsed_action_messages, parsed_hand_results)
347
363
  @data = []
348
364
  parsed_action_messages.data.zip(parsed_hand_results.data).each do |action_messages_by_hand, hand_result|
349
- @data << HandData.new(
365
+ @data << AcpcDealerData::HandData.new(
350
366
  @match_def,
351
367
  action_messages_by_hand,
352
368
  hand_result
@@ -361,8 +377,9 @@ class PokerMatchData
361
377
  def players_all_in?(current_round, turn_index, turns_taken)
362
378
  current_hand.data.length == turn_index + 2 &&
363
379
  current_round < (@match_def.game_def.number_of_rounds - 1) &&
364
- (turns_taken[0..turn_index].count do |t|
365
- t.action_message.action.to_sym == :fold
380
+ (turns_taken[0..turn_index].count do |t|
381
+ # @todo Use FOLD constant instead once poker action has one
382
+ t.action_message.action.to_acpc_character == 'f'
366
383
  end) != @players.length - 1
367
384
  end
368
385