git_game_show 0.1.10 → 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.
- checksums.yaml +4 -4
- data/lib/git_game_show/game_server.rb +64 -17
- data/lib/git_game_show/mini_game.rb +12 -12
- data/lib/git_game_show/player_client.rb +41 -13
- data/lib/git_game_show/version.rb +1 -1
- data/mini_games/author_quiz.rb +45 -32
- data/mini_games/blame_game.rb +392 -0
- data/mini_games/branch_detective.rb +13 -0
- data/mini_games/commit_message_completion.rb +49 -36
- data/mini_games/date_ordering_quiz.rb +64 -48
- data/mini_games/file_quiz.rb +13 -0
- metadata +3 -2
@@ -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
|
@@ -2,6 +2,19 @@ module GitGameShow
|
|
2
2
|
class BranchDetective < MiniGame
|
3
3
|
self.name = "Branch Detective"
|
4
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
|
5
18
|
self.questions_per_round = 5
|
6
19
|
|
7
20
|
# Custom timing for this mini-game
|
@@ -2,24 +2,37 @@ module GitGameShow
|
|
2
2
|
class CommitMessageCompletion < MiniGame
|
3
3
|
self.name = "Complete the Commit"
|
4
4
|
self.description = "Complete the missing part of these commit messages! (20 seconds per question)"
|
5
|
+
self.example = <<~EXAMPLE
|
6
|
+
Complete this commit message:
|
7
|
+
|
8
|
+
"Fix memory __________ in the background worker"
|
9
|
+
|
10
|
+
2d9a45c (Feb 25, 2025)
|
11
|
+
|
12
|
+
Choose your answer:
|
13
|
+
usage
|
14
|
+
allocation
|
15
|
+
\e[0;32;49m> leak\e[0m
|
16
|
+
error
|
17
|
+
EXAMPLE
|
5
18
|
self.questions_per_round = 5
|
6
|
-
|
19
|
+
|
7
20
|
# Custom timing for this mini-game (20 seconds instead of 15)
|
8
21
|
def self.question_timeout
|
9
22
|
20 # 20 seconds per question
|
10
23
|
end
|
11
|
-
|
24
|
+
|
12
25
|
def self.question_display_time
|
13
26
|
5 # 5 seconds between questions
|
14
27
|
end
|
15
|
-
|
28
|
+
|
16
29
|
def generate_questions(repo)
|
17
30
|
begin
|
18
31
|
commits = get_all_commits(repo)
|
19
|
-
|
32
|
+
|
20
33
|
# Filter commits with message length > 10 characters
|
21
34
|
valid_commits = commits.select { |commit| commit.message.strip.length > 10 }
|
22
|
-
|
35
|
+
|
23
36
|
# Fall back to sample questions if not enough valid commits
|
24
37
|
if valid_commits.size < self.class.questions_per_round
|
25
38
|
return generate_sample_questions
|
@@ -28,44 +41,44 @@ module GitGameShow
|
|
28
41
|
# If there's any error, fall back to sample questions
|
29
42
|
return generate_sample_questions
|
30
43
|
end
|
31
|
-
|
44
|
+
|
32
45
|
questions = []
|
33
46
|
valid_questions = 0
|
34
47
|
attempts = 0
|
35
48
|
max_attempts = 100 # Prevent infinite loops
|
36
|
-
|
49
|
+
|
37
50
|
# Keep trying until we have exactly 5 questions or reach max attempts
|
38
51
|
while valid_questions < self.class.questions_per_round && attempts < max_attempts
|
39
52
|
attempts += 1
|
40
|
-
|
53
|
+
|
41
54
|
# Get a random commit
|
42
55
|
commit = valid_commits.sample
|
43
56
|
message = commit.message.strip
|
44
|
-
|
57
|
+
|
45
58
|
# Split message into beginning and end parts
|
46
59
|
words = message.split(/\s+/)
|
47
|
-
|
60
|
+
|
48
61
|
# Skip if too few words
|
49
62
|
if words.size < 4
|
50
63
|
next
|
51
64
|
end
|
52
|
-
|
65
|
+
|
53
66
|
# Determine how much to hide (1/3 to 1/2 of words)
|
54
67
|
hide_count = [words.size / 3, 2].max
|
55
68
|
hide_start = rand(0..(words.size - hide_count))
|
56
69
|
hidden_words = words[hide_start...(hide_start + hide_count)]
|
57
|
-
|
70
|
+
|
58
71
|
# Replace hidden words with blanks
|
59
72
|
words[hide_start...(hide_start + hide_count)] = ['________'] * hide_count
|
60
|
-
|
73
|
+
|
61
74
|
# Create question and actual answer
|
62
75
|
question_text = words.join(' ')
|
63
76
|
correct_answer = hidden_words.join(' ')
|
64
|
-
|
77
|
+
|
65
78
|
# Generate incorrect options
|
66
79
|
other_messages = get_commit_messages(valid_commits) - [message]
|
67
80
|
other_messages_parts = []
|
68
|
-
|
81
|
+
|
69
82
|
# Get parts of other messages that have similar length
|
70
83
|
other_messages.each do |other_msg|
|
71
84
|
other_words = other_msg.split(/\s+/)
|
@@ -74,17 +87,17 @@ module GitGameShow
|
|
74
87
|
other_messages_parts << other_words[part_start...(part_start + hide_count)].join(' ')
|
75
88
|
end
|
76
89
|
end
|
77
|
-
|
90
|
+
|
78
91
|
# Only proceed if we have enough options
|
79
92
|
if other_messages_parts.size < 3
|
80
93
|
next
|
81
94
|
end
|
82
|
-
|
95
|
+
|
83
96
|
other_options = other_messages_parts.sample(3)
|
84
|
-
|
97
|
+
|
85
98
|
# Create options array with the correct answer and incorrect ones
|
86
99
|
all_options = ([correct_answer] + other_options).shuffle
|
87
|
-
|
100
|
+
|
88
101
|
# Format consistently with other mini-games
|
89
102
|
questions << {
|
90
103
|
question: "Complete this commit message:\n\n \"#{question_text}\"",
|
@@ -92,22 +105,22 @@ module GitGameShow
|
|
92
105
|
options: all_options,
|
93
106
|
correct_answer: correct_answer
|
94
107
|
}
|
95
|
-
|
108
|
+
|
96
109
|
valid_questions += 1
|
97
110
|
end
|
98
|
-
|
111
|
+
|
99
112
|
# If we couldn't generate enough questions, fall back to sample questions
|
100
113
|
if questions.size < self.class.questions_per_round
|
101
114
|
return generate_sample_questions
|
102
115
|
end
|
103
|
-
|
116
|
+
|
104
117
|
questions
|
105
118
|
end
|
106
|
-
|
119
|
+
|
107
120
|
# Generate sample questions when there aren't enough commits
|
108
121
|
def generate_sample_questions
|
109
122
|
questions = []
|
110
|
-
|
123
|
+
|
111
124
|
# Sample commit messages that are realistic
|
112
125
|
sample_messages = [
|
113
126
|
{
|
@@ -151,14 +164,14 @@ module GitGameShow
|
|
151
164
|
date: "Mar 15, 2025"
|
152
165
|
}
|
153
166
|
]
|
154
|
-
|
167
|
+
|
155
168
|
# Create a question for each sample
|
156
169
|
self.class.questions_per_round.times do |i|
|
157
170
|
sample = sample_messages[i % sample_messages.size]
|
158
|
-
|
171
|
+
|
159
172
|
# Create options array with the correct answer and incorrect ones
|
160
173
|
all_options = ([sample[:missing_part]] + sample[:wrong_options]).shuffle
|
161
|
-
|
174
|
+
|
162
175
|
# Format consistently with other mini-games
|
163
176
|
questions << {
|
164
177
|
question: "Complete this commit message:\n\n \"#{sample[:blank_text]}\"",
|
@@ -167,22 +180,22 @@ module GitGameShow
|
|
167
180
|
correct_answer: sample[:missing_part]
|
168
181
|
}
|
169
182
|
end
|
170
|
-
|
183
|
+
|
171
184
|
questions
|
172
185
|
end
|
173
|
-
|
186
|
+
|
174
187
|
def evaluate_answers(question, player_answers)
|
175
188
|
results = {}
|
176
|
-
|
189
|
+
|
177
190
|
player_answers.each do |player_name, answer_data|
|
178
191
|
player_answer = answer_data[:answer]
|
179
192
|
correct = player_answer == question[:correct_answer]
|
180
|
-
|
193
|
+
|
181
194
|
points = 0
|
182
|
-
|
195
|
+
|
183
196
|
if correct
|
184
197
|
points = 10 # Base points for correct answer
|
185
|
-
|
198
|
+
|
186
199
|
# Bonus points for fast answers, adjusted for 20-second time limit
|
187
200
|
time_taken = answer_data[:time_taken] || 20
|
188
201
|
if time_taken < 7 # Increased from 5 to 7 seconds
|
@@ -191,15 +204,15 @@ module GitGameShow
|
|
191
204
|
points += 3
|
192
205
|
end
|
193
206
|
end
|
194
|
-
|
207
|
+
|
195
208
|
results[player_name] = {
|
196
209
|
answer: player_answer,
|
197
210
|
correct: correct,
|
198
211
|
points: points
|
199
212
|
}
|
200
213
|
end
|
201
|
-
|
214
|
+
|
202
215
|
results
|
203
216
|
end
|
204
217
|
end
|
205
|
-
end
|
218
|
+
end
|