git_game_show 0.1.9 → 0.2.0

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,392 @@
1
+ module GitGameShow
2
+ # Enable this for testing/debugging
3
+ $BLAME_GAME_DEBUG = false
4
+
5
+ class BlameGame < MiniGame
6
+ self.name = "Blame Game"
7
+ self.description = "Identify who committed the highlighted line of code! (Can be slow to load)"
8
+ self.example = <<~EXAMPLE
9
+ Who committed the highlighted line of code?
10
+
11
+ File: main.rb
12
+
13
+
14
+ 1: def initialize(options = {})
15
+ 2: @logger = options[:logger] || Logger.new(STDOUT)
16
+ 3: @config = load_configuration
17
+ \e[0;33;49m> 4: @connections = []\e[0m
18
+ 5: @active = false
19
+ 6: setup_signal_handlers
20
+ 7: end
21
+
22
+ Choose your answer?
23
+ Bob Coder
24
+ Steve Developer
25
+ \e[0;32;49m> Alice Programmer\e[0m
26
+ Sandy Engineer
27
+ EXAMPLE
28
+ self.questions_per_round = 5
29
+
30
+ # Custom timing for this mini-game
31
+ def self.question_timeout
32
+ 20 # 20 seconds per question (slightly longer than Author Quiz since more code to read)
33
+ end
34
+
35
+ def self.question_display_time
36
+ 5 # 5 seconds between questions
37
+ end
38
+
39
+ def generate_questions(repo)
40
+ questions = []
41
+
42
+ # Check if repo exists and has commits
43
+ if repo.nil? || !repo.branches.size.positive?
44
+ puts "No repository with commits found, using sample questions" if $BLAME_GAME_DEBUG
45
+ return generate_sample_questions
46
+ end
47
+
48
+ begin
49
+ puts "Starting Blame Game question generation" if $BLAME_GAME_DEBUG
50
+ # Get all files in the repo
51
+ repo_path = repo.dir.path
52
+
53
+ # Get a more limited list of files tracked by git
54
+ # 1. Use git ls-files with specific extensions to limit scope
55
+ # 2. Exclude binary files and very large files right from the start
56
+ # 3. Limit to a reasonable number of files to consider
57
+ cmd = "cd #{repo_path} && git ls-files -- '*.rb' '*.js' '*.py' '*.html' '*.css' '*.ts' '*.jsx' '*.tsx' '*.md' '*.yml' '*.yaml' '*.json' '*.c' '*.cpp' '*.h' '*.java' | grep -v -E '\\.min\\.js$|\\.min\\.css$|\\.bundle\\.js$|\\.map$|\\.png$|\\.jpg$|\\.jpeg$|\\.gif$|\\.pdf$|\\.zip$|\\.tar$|\\.gz$|\\.jar$|\\.exe$|\\.bin$' | sort -R | head -100"
58
+ all_files = `#{cmd}`.split("\n").reject(&:empty?)
59
+
60
+ puts "Found #{all_files.size} files to choose from for Blame Game" if $BLAME_GAME_DEBUG
61
+
62
+ # Skip if no files found
63
+ if all_files.empty?
64
+ puts "No suitable files found in repository, using sample questions" if $BLAME_GAME_DEBUG
65
+ return generate_sample_questions
66
+ end
67
+
68
+ # Get all authors in advance to see if we have enough
69
+ all_authors_cmd = "cd #{repo_path} && git log --pretty=format:'%an' | sort | uniq"
70
+ all_authors = `#{all_authors_cmd}`.split("\n").uniq.reject(&:empty?)
71
+
72
+ puts "Found #{all_authors.size} unique authors in repository" if $BLAME_GAME_DEBUG
73
+
74
+ if all_authors.size < 2
75
+ puts "Not enough authors found (need at least 2), using sample questions" if $BLAME_GAME_DEBUG
76
+ return generate_sample_questions
77
+ end
78
+
79
+ # Try to generate the requested number of questions
80
+ # More attempts to increase chances of getting real data
81
+ attempts = 0
82
+ max_attempts = self.class.questions_per_round * 10
83
+
84
+ puts "Starting question generation process, will try up to #{max_attempts} attempts" if $BLAME_GAME_DEBUG
85
+
86
+ while questions.size < self.class.questions_per_round && attempts < max_attempts
87
+ attempts += 1
88
+
89
+ # Select a random file
90
+ file_path = all_files.sample
91
+
92
+ # Skip if file path is invalid or file doesn't exist
93
+ full_path = File.join(repo_path, file_path)
94
+ next unless File.exist?(full_path)
95
+
96
+ # Skip very large files (over 500 KB)
97
+ next if File.size(full_path) > 500_000
98
+
99
+ # Skip binary or non-text files
100
+ begin
101
+ # Test if this file can be read as text
102
+ File.read(full_path, 100).encode('UTF-8', :invalid => :replace, :undef => :replace)
103
+ rescue
104
+ next
105
+ end
106
+
107
+ # Skip checking line count, which is slow for large files
108
+ # Instead read the first chunk to estimate if it's large enough
109
+ content_preview = File.read(full_path, 1000) rescue nil
110
+ next unless content_preview
111
+
112
+ # Rough estimate if file has enough lines by checking for newlines
113
+ newline_count = content_preview.count("\n")
114
+ next if newline_count < 7
115
+
116
+ # Estimate total lines - if preview has enough lines, we can use that section
117
+ if newline_count >= 20
118
+ # Use lines from the preview which we already have
119
+ lines = content_preview.split("\n")
120
+ # Choose a line not too close to the beginning or end
121
+ target_idx = rand(4..(lines.size - 5))
122
+ # Content is just the section we already read, no need to read the whole file
123
+ file_content = lines
124
+ # Target line number is relative to the preview
125
+ target_line_num = target_idx + 1 # 1-based line numbers
126
+ # For display purposes, show the real line number
127
+ target_line_display = target_line_num
128
+ else
129
+ # For small files, do a quick line count
130
+ # This is much faster than counting all lines in a large file
131
+ line_count = content_preview.count("\n") + 1
132
+ file_content = File.readlines(full_path) rescue nil
133
+ next unless file_content
134
+ target_line_num = rand(4..(line_count - 4))
135
+ target_line_display = target_line_num
136
+ end
137
+
138
+ # Get git blame for the selected line with better error checking
139
+ # Use a more efficient git blame approach - only get what we need
140
+ begin
141
+ # Extract both the author name and commit date
142
+ blame_cmd = "cd #{repo_path} && git blame -L #{target_line_num},#{target_line_num} --line-porcelain #{file_path}"
143
+ blame_output = `#{blame_cmd}`
144
+
145
+ # Extract author name
146
+ author_name_match = blame_output.match(/^author (.+)$/)
147
+ author_name = author_name_match ? author_name_match[1].strip : ""
148
+
149
+ # Extract commit date (author-time and author-tz)
150
+ author_time_match = blame_output.match(/^author-time (\d+)$/)
151
+ author_tz_match = blame_output.match(/^author-tz (.+)$/)
152
+
153
+ commit_date = nil
154
+ if author_time_match
155
+ timestamp = author_time_match[1].to_i
156
+ commit_date = Time.at(timestamp).strftime("%Y-%m-%d")
157
+ end
158
+
159
+ # Skip if we couldn't get a valid author
160
+ if author_name.empty? || author_name.include?("fatal:") || author_name == "Not Committed Yet"
161
+ puts "Skipping uncommitted or invalid line" if $BLAME_GAME_DEBUG
162
+ next
163
+ end
164
+
165
+ puts "Found line committed by: #{author_name} on #{commit_date}" if $BLAME_GAME_DEBUG
166
+
167
+ # We already have all authors, so we don't need to fetch them again
168
+ # But we should still check if we have enough authors to make this challenging
169
+ if all_authors.size < 4 # Need at least 3 incorrect options + 1 correct
170
+ puts "Not enough unique authors in the repository: #{all_authors.size}" if $BLAME_GAME_DEBUG
171
+ next
172
+ end
173
+ rescue => e
174
+ puts "Error getting blame info: #{e.message}" if $BLAME_GAME_DEBUG
175
+ next
176
+ end
177
+
178
+ # Get context lines (3 before, target line, 3 after)
179
+ # Use the content we already read if possible, otherwise read from file
180
+ if newline_count >= 20 && target_idx >= 3 && target_idx <= (lines.size - 4)
181
+ # We can use the lines we already have from the preview
182
+ context_lines = lines[(target_idx-3)..(target_idx+3)]
183
+ start_idx = target_idx - 3
184
+ else
185
+ # Get relevant content directly - much more efficient than reading entire file
186
+ context_cmd = "cd #{repo_path} && tail -n +#{[target_line_num-3, 1].max} #{file_path} | head -7"
187
+ context_output = `#{context_cmd}`
188
+ context_lines = context_output.split("\n")
189
+ start_idx = [target_line_num - 4, 0].max
190
+ end
191
+
192
+ next unless context_lines && context_lines.size > 0
193
+
194
+ # Add line indicators for display
195
+ display_lines = []
196
+ context_lines.each_with_index do |line, idx|
197
+ line_num = start_idx + idx + 1
198
+
199
+ # Mark the target line with > prefix
200
+ prefix = (line_num == target_line_num) ? "> " : " "
201
+
202
+ # Clean the line for display (handle tab characters, trim long lines)
203
+ clean_line = line.gsub("\t", " ").rstrip
204
+ if clean_line.length > 100
205
+ clean_line = clean_line[0..97] + "..."
206
+ end
207
+
208
+ display_lines << "#{prefix}#{line_num}: #{clean_line}"
209
+ end
210
+
211
+ # Create context string with file path and highlighted lines
212
+ context = "File: #{file_path}\n\n#{display_lines.join("\n")}"
213
+
214
+ # Get incorrect options (other authors)
215
+ incorrect_authors = all_authors.reject { |a| a == author_name }.shuffle.take(3)
216
+
217
+ # If we don't have enough distinct authors, generate some
218
+ if incorrect_authors.size < 3
219
+ sample_authors = ["Alice", "Bob", "Charlie", "David", "Emma"].reject { |a| a == author_name }
220
+ incorrect_authors += sample_authors.take(3 - incorrect_authors.size)
221
+ end
222
+
223
+ # Create options array with the correct answer and incorrect ones
224
+ all_options = ([author_name] + incorrect_authors).shuffle
225
+
226
+ # Create the question with date information
227
+ question_text = "Who committed the highlighted line of code"
228
+ question_text += commit_date ? " on #{commit_date}?" : "?"
229
+
230
+ questions << {
231
+ question: question_text,
232
+ context: context,
233
+ options: all_options,
234
+ correct_answer: author_name
235
+ }
236
+ end
237
+
238
+ rescue => e
239
+ # Silently fail and use sample questions instead
240
+ return generate_sample_questions
241
+ end
242
+
243
+ # Make sure we have enough questions or use fallback
244
+ if questions.size < self.class.questions_per_round
245
+ puts "Could only generate #{questions.size} questions, using sample questions to fill the rest" if $BLAME_GAME_DEBUG
246
+ sample_questions = generate_sample_questions
247
+ # Add enough sample questions to reach the required number
248
+ questions += sample_questions[0...(self.class.questions_per_round - questions.size)]
249
+ end
250
+
251
+ puts "Generated #{questions.size} questions for Blame Game" if $BLAME_GAME_DEBUG
252
+ questions
253
+ rescue => e
254
+ puts "Error in BlameGame#generate_questions: #{e.message}\n#{e.backtrace.join("\n")}" if $BLAME_GAME_DEBUG
255
+ generate_sample_questions
256
+ end
257
+
258
+ def evaluate_answers(question, player_answers)
259
+ results = {}
260
+
261
+ player_answers.each do |player_name, answer_data|
262
+ answered = answer_data[:answered] || false
263
+ player_answer = answer_data[:answer]
264
+ correct = player_answer == question[:correct_answer]
265
+
266
+ points = correct ? 10 : 0
267
+
268
+ # Bonus points for fast answers (if correct)
269
+ if correct
270
+ time_taken = answer_data[:time_taken] || self.class.question_timeout
271
+
272
+ if time_taken < 5
273
+ points += 5
274
+ elsif time_taken < 10
275
+ points += 3
276
+ elsif time_taken < 15
277
+ points += 1
278
+ end
279
+ end
280
+
281
+ results[player_name] = {
282
+ answer: player_answer,
283
+ correct: correct,
284
+ points: points
285
+ }
286
+ end
287
+
288
+ results
289
+ end
290
+
291
+ def generate_sample_questions
292
+ # Create sample questions in case the repo doesn't have enough data
293
+ sample_authors = ["Alice", "Bob", "Charlie", "David", "Emma"]
294
+ sample_files = ["main.rb", "utils.js", "config.py", "index.html", "styles.css"]
295
+ # Sample dates for the commits
296
+ sample_dates = ["2024-01-15", "2024-02-20", "2024-03-05", "2024-03-10", "2024-03-11"]
297
+
298
+ questions = []
299
+
300
+ self.class.questions_per_round.times do |i|
301
+ # Select a random author, file, and date for this sample
302
+ correct_author = sample_authors.sample
303
+ file_name = sample_files.sample
304
+ commit_date = sample_dates.sample
305
+
306
+ # Create sample code context
307
+ context_lines = []
308
+
309
+ case file_name
310
+ when "main.rb"
311
+ context_lines = [
312
+ "def initialize(options = {})",
313
+ " @logger = options[:logger] || Logger.new(STDOUT)",
314
+ " @config = load_configuration",
315
+ " @connections = []",
316
+ " @active = false",
317
+ " setup_signal_handlers",
318
+ "end"
319
+ ]
320
+ when "utils.js"
321
+ context_lines = [
322
+ "function formatDate(date) {",
323
+ " const day = String(date.getDate()).padStart(2, '0');",
324
+ " const month = String(date.getMonth() + 1).padStart(2, '0');",
325
+ " const year = date.getFullYear();",
326
+ " return `${year}-${month}-${day}`;",
327
+ " // TODO: Add support for different formats",
328
+ "};"
329
+ ]
330
+ when "config.py"
331
+ context_lines = [
332
+ "class Config:",
333
+ " DEBUG = False",
334
+ " TESTING = False",
335
+ " DATABASE_URI = os.environ.get('DATABASE_URI')",
336
+ " SECRET_KEY = os.environ.get('SECRET_KEY')",
337
+ " SESSION_COOKIE_SECURE = True",
338
+ " TEMPLATES_AUTO_RELOAD = True"
339
+ ]
340
+ when "index.html"
341
+ context_lines = [
342
+ "<header>",
343
+ " <nav>",
344
+ " <ul>",
345
+ " <li><a href=\"/\">Home</a></li>",
346
+ " <li><a href=\"/about\">About</a></li>",
347
+ " <li><a href=\"/contact\">Contact</a></li>",
348
+ " </ul>"
349
+ ]
350
+ when "styles.css"
351
+ context_lines = [
352
+ "body {",
353
+ " font-family: 'Helvetica', sans-serif;",
354
+ " line-height: 1.6;",
355
+ " color: #333;",
356
+ " margin: 0;",
357
+ " padding: 20px;",
358
+ " background-color: #f5f5f5;"
359
+ ]
360
+ end
361
+
362
+ # Mark a random line as the target (line 4 is index 3)
363
+ target_idx = 3 # Middle line
364
+
365
+ # Add line numbers and indicators
366
+ display_lines = []
367
+ context_lines.each_with_index do |line, idx|
368
+ line_num = idx + 1
369
+ prefix = (idx == target_idx) ? "> " : " "
370
+ display_lines << "#{prefix}#{line_num}: #{line}"
371
+ end
372
+
373
+ # Create context string
374
+ context = "File: #{file_name} (SAMPLE)\n\n#{display_lines.join("\n")}"
375
+
376
+ # Get incorrect options (other authors)
377
+ incorrect_authors = sample_authors.reject { |a| a == correct_author }.sample(3)
378
+ all_options = ([correct_author] + incorrect_authors).shuffle
379
+
380
+ # Create the sample question with date
381
+ questions << {
382
+ question: "Who committed the highlighted line of code on #{commit_date}? (SAMPLE)",
383
+ context: context,
384
+ options: all_options,
385
+ correct_answer: correct_author
386
+ }
387
+ end
388
+
389
+ questions
390
+ end
391
+ end
392
+ end
@@ -0,0 +1,241 @@
1
+ module GitGameShow
2
+ class BranchDetective < MiniGame
3
+ self.name = "Branch Detective"
4
+ self.description = "Identify which branch a commit belongs to!"
5
+ self.example = <<~EXAMPLE
6
+ Which branch was this commit originally made on?
7
+
8
+ "Add pagination to users list"
9
+
10
+ abcdef7 (by Jane Developer on 2023-05-20 16:45:12)
11
+
12
+ Choose your answer:
13
+ development
14
+ \e[0;32;49m> feature/user-management\e[0m
15
+ main
16
+ hotfix/login-issue
17
+ EXAMPLE
18
+ self.questions_per_round = 5
19
+
20
+ # Custom timing for this mini-game
21
+ def self.question_timeout
22
+ 15 # 15 seconds per question
23
+ end
24
+
25
+ def self.question_display_time
26
+ 5 # 5 seconds between questions
27
+ end
28
+
29
+ def generate_questions(repo)
30
+ @repo = repo
31
+ begin
32
+ # Get all branches (both local and remote)
33
+ branches = {}
34
+
35
+ # Use Git command to get all local and remote branches
36
+
37
+ # get all remote branches from the git repository
38
+ all_remotes_cmd = "cd #{@repo.dir.path} && git branch -r"
39
+ all_branch_output = `#{all_remotes_cmd}`
40
+
41
+ # Parse branch names and clean them up
42
+ branch_names = all_branch_output.split("\n").map do |branch|
43
+ branch = branch.gsub(/^\* /, '').strip # Remove the * prefix from current branch
44
+
45
+ # Skip special branches like HEAD
46
+ next if branch == 'HEAD' || branch =~ /HEAD detached/
47
+
48
+ branch
49
+ end.compact.uniq # Remove nils and duplicates
50
+
51
+ # Filter out any empty branch names
52
+ branch_names.reject!(&:empty?)
53
+
54
+ # Need at least 3 branches to make interesting questions
55
+ if branch_names.size < 5
56
+ return generate_sample_questions
57
+ end
58
+
59
+ branch_names = branch_names.sample(100) if branch_names.size > 100
60
+
61
+ branch_names.each do |branch|
62
+ # Get commits for this branch
63
+ branches[branch] = get_commits_for_branch(branch)
64
+ end
65
+
66
+ # Generate questions
67
+ questions = []
68
+
69
+ self.class.questions_per_round.times do
70
+ # Select branches that have commits
71
+ branch_options = branch_names.sample(4)
72
+
73
+
74
+ # If we don't have 4 valid branches, pad with duplicates and ensure uniqueness later
75
+ if branch_options.size < 4
76
+ branch_options = branch_names.dup
77
+ while branch_options.size < 4
78
+ branch_options << branch_names.sample
79
+ end
80
+ end
81
+
82
+ # Choose a random branch as the correct answer
83
+ correct_branch = branch_options.sample
84
+
85
+ # Choose a random commit from this branch
86
+ commit = branches[correct_branch].sample
87
+
88
+ # Create the question
89
+ commit_date = commit[:date] #.split(" ")[0..2].join(" ") + " " + commit[:date].split(" ")[4]
90
+ commit_short_sha = commit[:sha][0..6]
91
+
92
+ # Format the commit message for display
93
+ message = commit[:message].lines.first&.strip || "No message"
94
+ message = message.length > 50 ? "#{message[0...47]}..." : message
95
+
96
+ questions << {
97
+ question: "Which branch was this commit originally made on?\n\n \"#{message}\"",
98
+ commit_info: "#{commit_short_sha} (by #{commit[:author]} on #{commit_date})",
99
+ options: branch_options.uniq.shuffle,
100
+ correct_answer: correct_branch
101
+ }
102
+ end
103
+
104
+ # If we couldn't generate enough questions, fill with sample questions
105
+ if questions.size < self.class.questions_per_round
106
+ sample_questions = generate_sample_questions
107
+ questions += sample_questions[0...(self.class.questions_per_round - questions.size)]
108
+ end
109
+
110
+ questions
111
+ rescue => e
112
+ # If any errors occur, fall back to sample questions
113
+ generate_sample_questions
114
+ end
115
+ end
116
+
117
+ # Generate sample questions
118
+ def generate_sample_questions
119
+ questions = []
120
+
121
+ # Sample data with branch names and commits
122
+ sample_branches = [
123
+ "main", "develop", "feature/user-auth", "bugfix/login",
124
+ "feature/payment", "hotfix/security", "release/v2.0", "staging"
125
+ ]
126
+
127
+ # Sample commit data
128
+ sample_commits = [
129
+ { message: "[SAMPLE] Add user authentication flow", author: "Jane Doe", date: "2023-05-15 14:30:22", sha: "a1b2c3d" },
130
+ { message: "[SAMPLE] Fix login page styling issues", author: "John Smith", date: "2023-05-18 10:15:45", sha: "e4f5g6h" },
131
+ { message: "[SAMPLE] Implement password reset functionality", author: "Alice Johnson", date: "2023-05-20 16:45:12", sha: "i7j8k9l" },
132
+ { message: "[SAMPLE] Add payment gateway integration", author: "Bob Brown", date: "2023-05-22 09:20:33", sha: "m2n3o4p" },
133
+ { message: "[SAMPLE] Update README with API documentation", author: "Charlie Davis", date: "2023-05-25 11:05:56", sha: "q5r6s7t" }
134
+ ]
135
+
136
+ # Generate sample questions
137
+ self.class.questions_per_round.times do |i|
138
+ # Select a random commit
139
+ commit = sample_commits[i % sample_commits.size]
140
+
141
+ # Select 4 random branch names
142
+ branch_options = sample_branches.sample(4)
143
+
144
+ # Choose a correct branch
145
+ correct_branch = branch_options.sample
146
+
147
+ questions << {
148
+ question: "Which branch was this commit originally made on?\n\n \"#{commit[:message]}\"",
149
+ commit_info: "#{commit[:sha]} (by #{commit[:author]} on #{commit[:date]})",
150
+ options: branch_options,
151
+ correct_answer: correct_branch
152
+ }
153
+ end
154
+
155
+ questions
156
+ end
157
+
158
+ def evaluate_answers(question, player_answers)
159
+ results = {}
160
+
161
+ player_answers.each do |player_name, answer_data|
162
+ answered = answer_data[:answered] || false
163
+ player_answer = answer_data[:answer]
164
+ time_taken = answer_data[:time_taken] || self.class.question_timeout
165
+
166
+ # Check if the answer is correct
167
+ correct = player_answer == question[:correct_answer]
168
+
169
+ # Calculate points
170
+ points = 0
171
+
172
+ # Base points for correct answer
173
+ if correct
174
+ points = 10
175
+
176
+ # Bonus points for fast answers
177
+ if time_taken < 5
178
+ points += 5 # Very fast (under 5 seconds)
179
+ elsif time_taken < 10
180
+ points += 3 # Pretty fast (under 10 seconds)
181
+ elsif time_taken < 12
182
+ points += 1 # Somewhat fast (under 12 seconds)
183
+ end
184
+ end
185
+
186
+ # Store the results
187
+ results[player_name] = {
188
+ answer: player_answer,
189
+ correct: correct,
190
+ points: points
191
+ }
192
+ end
193
+
194
+ results
195
+ end
196
+
197
+ private
198
+
199
+ def get_commits_for_branch branch
200
+ unique_commits = []
201
+
202
+ # Try different ways to reference the branch
203
+ begin
204
+ # Try a few different ways to reference the branch
205
+ got_commits = false
206
+
207
+ # First try as a local branch
208
+ cmd = "cd #{@repo.dir.path} && git log --pretty '#{branch}' --max-count=5 2>/dev/null"
209
+ commit_output = `#{cmd}`
210
+
211
+ # Process commits if we found any
212
+ commits = commit_output.split("commit ")[1..-1]
213
+
214
+ commits.each do |commit|
215
+
216
+ # Extract commit info
217
+ sha = commit.lines[0].split(" ")[0].strip
218
+ author = commit.lines[1].gsub("Author: ", "").split("<")[0].strip
219
+ date = commit.lines[2].gsub("Date: ", "").strip
220
+ message = commit.lines[4..-1].join("\n")
221
+
222
+ next unless message.length > 10
223
+ next if message.include?("Merge pull request")
224
+ # Store this commit info
225
+ unique_commits << {
226
+ sha: sha,
227
+ message: message,
228
+ author: author,
229
+ date: date
230
+ }
231
+ end
232
+
233
+ rescue => e
234
+ # If we hit any errors with this branch, just skip it
235
+ []
236
+ end
237
+
238
+ unique_commits
239
+ end
240
+ end
241
+ end