git_game_show 0.1.7 → 0.1.8
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 +1 -1
- data/lib/git_game_show/version.rb +1 -1
- data/mini_games/file_quiz.rb +278 -0
- metadata +3 -3
- data/mini_games/commit_message_quiz.rb +0 -589
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 719118cfd107443e62e4c5e91fe2ee425bf58d30b9c681df744e9e8fbc25f81d
|
4
|
+
data.tar.gz: 80cb0a7b0f9653d33e0e58057eb9fc85df59757486e452c22de2ea3508a0bcb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ba0dec0a4e51517994a4c574cb1d1c738780e2a812a4d516fb53f18d46d9c08661ec924a67dbb228d4efffe360e0f47bebe804464fe7b83e10ad053a859da96
|
7
|
+
data.tar.gz: acc62cb0dcaf6ae4d6249b8a1b4b5b6f03fc3d2d19016dc2f65a64991d31069b7b5b1786ff5c1566575c193473ba36d4ac3285385f46d74f81f30e6e21449a10
|
@@ -0,0 +1,278 @@
|
|
1
|
+
module GitGameShow
|
2
|
+
class FileQuiz < MiniGame
|
3
|
+
self.name = "File Quiz"
|
4
|
+
self.description = "Match the commit message to the right changed file!"
|
5
|
+
self.questions_per_round = 5
|
6
|
+
|
7
|
+
# Custom timing for this mini-game (same as AuthorQuiz)
|
8
|
+
def self.question_timeout
|
9
|
+
15 # 15 seconds per question
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.question_display_time
|
13
|
+
5 # 5 seconds between questions
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_questions(repo)
|
17
|
+
begin
|
18
|
+
# Get commits from the repo - based on similar approach as AuthorQuiz
|
19
|
+
# Start by getting a larger number of commits to ensure enough variety
|
20
|
+
commit_count = [self.class.questions_per_round * 10, 100].min
|
21
|
+
|
22
|
+
# Get all commits
|
23
|
+
commits = repo.log(commit_count).to_a
|
24
|
+
|
25
|
+
# Shuffle commits for better variety
|
26
|
+
commits.shuffle!
|
27
|
+
|
28
|
+
# Process commits to find ones with good file changes
|
29
|
+
valid_commits = []
|
30
|
+
|
31
|
+
commits.each do |commit|
|
32
|
+
# Get changed files for this commit
|
33
|
+
changed_files = []
|
34
|
+
|
35
|
+
begin
|
36
|
+
# Try to get the diff with previous commit
|
37
|
+
if commit.parents.empty?
|
38
|
+
# First commit - get the files directly
|
39
|
+
diff_output = repo.lib.run_command('show', ['--name-only', '--pretty=format:', commit.sha])
|
40
|
+
changed_files = diff_output.split("\n").reject(&:empty?)
|
41
|
+
else
|
42
|
+
# Regular commit with parent
|
43
|
+
diff_output = repo.lib.run_command('diff', ['--name-only', "#{commit.sha}^", commit.sha])
|
44
|
+
changed_files = diff_output.split("\n").reject(&:empty?)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Skip if no files or too many files (probably a merge or refactoring)
|
48
|
+
next if changed_files.empty?
|
49
|
+
next if changed_files.size > 8 # Skip large commits that likely changed multiple unrelated files
|
50
|
+
|
51
|
+
# Create structure with relevant commit data
|
52
|
+
valid_commits << {
|
53
|
+
sha: commit.sha,
|
54
|
+
message: commit.message,
|
55
|
+
author: commit.author.name,
|
56
|
+
date: commit.date,
|
57
|
+
files: changed_files
|
58
|
+
}
|
59
|
+
|
60
|
+
# Once we have enough commits, we can stop
|
61
|
+
break if valid_commits.size >= self.class.questions_per_round * 3
|
62
|
+
rescue => e
|
63
|
+
# Skip problematic commits
|
64
|
+
next
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# If we couldn't find enough good commits, fall back to samples
|
69
|
+
if valid_commits.size < self.class.questions_per_round
|
70
|
+
return generate_sample_questions
|
71
|
+
end
|
72
|
+
|
73
|
+
# Prioritize commits that modified interesting files (not just .gitignore etc.)
|
74
|
+
prioritized_commits = valid_commits.sort_by do |commit|
|
75
|
+
# Higher score = more interesting commit
|
76
|
+
score = 0
|
77
|
+
|
78
|
+
# Prioritize based on file types
|
79
|
+
commit[:files].each do |file|
|
80
|
+
ext = File.extname(file).downcase
|
81
|
+
|
82
|
+
case ext
|
83
|
+
when '.rb', '.js', '.py', '.java', '.tsx', '.jsx'
|
84
|
+
score += 3 # Source code is most interesting
|
85
|
+
when '.html', '.css', '.scss'
|
86
|
+
score += 2 # Templates and styles are interesting
|
87
|
+
when '.md', '.txt', '.json', '.yaml', '.yml'
|
88
|
+
score += 1 # Config and docs are moderately interesting
|
89
|
+
when '', '.gitignore', '.gitattributes'
|
90
|
+
score -= 1 # Less interesting files
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Prioritize based on commit message length - longer messages are often more descriptive
|
95
|
+
message_length = commit[:message].to_s.strip.length
|
96
|
+
score += [message_length / 20, 5].min
|
97
|
+
|
98
|
+
# Return negative score so highest scores come first in sort
|
99
|
+
-score
|
100
|
+
end
|
101
|
+
|
102
|
+
# Select top commits for questions
|
103
|
+
selected_commits = prioritized_commits.take(self.class.questions_per_round)
|
104
|
+
|
105
|
+
# Create questions from selected commits
|
106
|
+
questions = []
|
107
|
+
|
108
|
+
selected_commits.each do |commit|
|
109
|
+
# Choose the most interesting file as the correct answer
|
110
|
+
# (Sort by extension priority, then by path length to favor shorter paths)
|
111
|
+
files = commit[:files]
|
112
|
+
|
113
|
+
# Score files by interestingness
|
114
|
+
scored_files = files.map do |file|
|
115
|
+
ext = File.extname(file).downcase
|
116
|
+
|
117
|
+
# Start with base score by extension
|
118
|
+
score = case ext
|
119
|
+
when '.rb', '.js', '.py', '.java', '.tsx', '.jsx'
|
120
|
+
10 # Source code is most interesting
|
121
|
+
when '.html', '.css', '.scss'
|
122
|
+
8 # Templates and styles are interesting
|
123
|
+
when '.md', '.txt'
|
124
|
+
6 # Documentation
|
125
|
+
when '.json', '.yaml', '.yml'
|
126
|
+
4 # Config files
|
127
|
+
when '', '.gitignore', '.gitattributes'
|
128
|
+
0 # Less interesting files
|
129
|
+
else
|
130
|
+
5 # Other files are neutral
|
131
|
+
end
|
132
|
+
|
133
|
+
# Shorter paths are usually more recognizable
|
134
|
+
score -= [file.length / 10, 5].min
|
135
|
+
|
136
|
+
# Prefer files in main directories (src, lib, app) over deeply nested ones
|
137
|
+
if file.start_with?('src/', 'lib/', 'app/')
|
138
|
+
score += 3
|
139
|
+
end
|
140
|
+
|
141
|
+
[file, score]
|
142
|
+
end
|
143
|
+
|
144
|
+
# Sort by score (highest first) and select most interesting file
|
145
|
+
correct_file = scored_files.sort_by { |_, score| -score }.first[0]
|
146
|
+
|
147
|
+
# Get incorrect options from other commits
|
148
|
+
other_files = []
|
149
|
+
other_commits = selected_commits - [commit]
|
150
|
+
|
151
|
+
# Collect files from other commits
|
152
|
+
other_commits.each do |other_commit|
|
153
|
+
other_commit[:files].each do |file|
|
154
|
+
other_files << file unless files.include?(file)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# If we don't have enough other files, use some from sample data
|
159
|
+
if other_files.size < 3
|
160
|
+
sample_files = [
|
161
|
+
"src/main.js", "lib/utils.js", "css/styles.css", "README.md",
|
162
|
+
"package.json", "Dockerfile", ".github/workflows/ci.yml",
|
163
|
+
"src/components/Header.js", "app/models/user.rb", "config/database.yml"
|
164
|
+
]
|
165
|
+
other_files += sample_files.reject { |f| files.include?(f) }
|
166
|
+
end
|
167
|
+
|
168
|
+
# Take up to 3 unique other files, prioritizing diverse ones
|
169
|
+
other_files = other_files.uniq.sample(3)
|
170
|
+
|
171
|
+
# Create options array with the correct answer and incorrect ones
|
172
|
+
all_options = ([correct_file] + other_files).shuffle
|
173
|
+
|
174
|
+
# Format the commit date nicely
|
175
|
+
nice_date = commit[:date].strftime('%b %d, %Y') rescue "Unknown date"
|
176
|
+
|
177
|
+
# Clean up commit message - take first line if multiple lines
|
178
|
+
message = commit[:message].to_s.split("\n").first.strip
|
179
|
+
|
180
|
+
# Format consistently with other mini-games
|
181
|
+
questions << {
|
182
|
+
question: "Which file was most likely changed in this commit?\n\n \"#{message}\"",
|
183
|
+
commit_info: "#{commit[:sha][0..6]} (#{nice_date})",
|
184
|
+
options: all_options,
|
185
|
+
correct_answer: correct_file
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
# Final safety check - if we couldn't create enough questions, use samples
|
190
|
+
if questions.size < self.class.questions_per_round
|
191
|
+
return generate_sample_questions
|
192
|
+
end
|
193
|
+
|
194
|
+
return questions
|
195
|
+
rescue => e
|
196
|
+
# If anything fails, fall back to sample questions
|
197
|
+
return generate_sample_questions
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def evaluate_answers(question, player_answers)
|
202
|
+
results = {}
|
203
|
+
|
204
|
+
player_answers.each do |player_name, answer_data|
|
205
|
+
answered = answer_data[:answered] || false
|
206
|
+
player_answer = answer_data[:answer]
|
207
|
+
correct = player_answer == question[:correct_answer]
|
208
|
+
|
209
|
+
points = correct ? 10 : 0
|
210
|
+
|
211
|
+
# Bonus points for fast answers (if correct)
|
212
|
+
if correct
|
213
|
+
time_taken = answer_data[:time_taken] || 15
|
214
|
+
|
215
|
+
if time_taken < 5
|
216
|
+
points += 5
|
217
|
+
elsif time_taken < 10
|
218
|
+
points += 3
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
results[player_name] = {
|
223
|
+
answer: player_answer,
|
224
|
+
correct: correct,
|
225
|
+
points: points
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
229
|
+
results
|
230
|
+
end
|
231
|
+
|
232
|
+
# Generate sample questions only used when no repository data is available
|
233
|
+
def generate_sample_questions
|
234
|
+
questions = []
|
235
|
+
|
236
|
+
# Common file options
|
237
|
+
common_files = [
|
238
|
+
"src/main.js",
|
239
|
+
"README.md",
|
240
|
+
"lib/utils.js",
|
241
|
+
"css/styles.css"
|
242
|
+
]
|
243
|
+
|
244
|
+
# Common sample commit messages
|
245
|
+
sample_messages = [
|
246
|
+
"Update documentation with new API endpoints",
|
247
|
+
"Fix styling issues in mobile view",
|
248
|
+
"Add error handling for network failures",
|
249
|
+
"Refactor authentication module for better performance",
|
250
|
+
"Update dependencies to latest versions"
|
251
|
+
]
|
252
|
+
|
253
|
+
# Create different sample questions for variety
|
254
|
+
self.class.questions_per_round.times do |i|
|
255
|
+
# Use modulo to cycle through sample messages
|
256
|
+
message = sample_messages[i % sample_messages.size]
|
257
|
+
|
258
|
+
# Different correct answers for each question
|
259
|
+
correct_file = common_files[i % common_files.size]
|
260
|
+
|
261
|
+
# Options are all files with the correct one included
|
262
|
+
all_options = common_files.shuffle
|
263
|
+
|
264
|
+
# Create the question
|
265
|
+
questions << {
|
266
|
+
question: "Which file was most likely changed in this commit?\n\n \"#{message} (SAMPLE)\"",
|
267
|
+
commit_info: "sample#{i} (Demo Question)",
|
268
|
+
options: all_options,
|
269
|
+
correct_answer: correct_file
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
questions
|
274
|
+
end
|
275
|
+
|
276
|
+
# No private methods needed anymore
|
277
|
+
end
|
278
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git_game_show
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Paulson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -214,8 +214,8 @@ files:
|
|
214
214
|
- lib/git_game_show/version.rb
|
215
215
|
- mini_games/author_quiz.rb
|
216
216
|
- mini_games/commit_message_completion.rb
|
217
|
-
- mini_games/commit_message_quiz.rb
|
218
217
|
- mini_games/date_ordering_quiz.rb
|
218
|
+
- mini_games/file_quiz.rb
|
219
219
|
homepage: https://github.com/justinpaulson/git_game_show
|
220
220
|
licenses:
|
221
221
|
- MIT
|
@@ -1,589 +0,0 @@
|
|
1
|
-
module GitGameShow
|
2
|
-
class CommitMessageQuiz < MiniGame
|
3
|
-
self.name = "Commit Message Quiz"
|
4
|
-
self.description = "Match the commit message to the right changed file!"
|
5
|
-
self.questions_per_round = 5
|
6
|
-
|
7
|
-
# Custom timing for this mini-game (same as AuthorQuiz)
|
8
|
-
def self.question_timeout
|
9
|
-
15 # 15 seconds per question
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.question_display_time
|
13
|
-
5 # 5 seconds between questions
|
14
|
-
end
|
15
|
-
|
16
|
-
def generate_questions(repo)
|
17
|
-
begin
|
18
|
-
# FORCE SAMPLE QUESTIONS: This guarantees different questions every time
|
19
|
-
return generate_sample_questions
|
20
|
-
|
21
|
-
# COMPLETELY NEW APPROACH:
|
22
|
-
# 1. Use git command directly to get ALL commits
|
23
|
-
# 2. Space them out evenly across the repo's history
|
24
|
-
# 3. Select a random but diverse set for questions
|
25
|
-
|
26
|
-
# Get total number of commits in the repo to determine how far back to go
|
27
|
-
begin
|
28
|
-
# Use git directly to count all commits
|
29
|
-
commit_count_output = repo.lib.run_command('rev-list', ['--count', 'HEAD'])
|
30
|
-
total_commits = commit_count_output.to_i
|
31
|
-
|
32
|
-
# If we have very few commits, use them all
|
33
|
-
if total_commits < 20
|
34
|
-
commit_limit = total_commits
|
35
|
-
else
|
36
|
-
# Otherwise get a good sample
|
37
|
-
commit_limit = [500, total_commits].min
|
38
|
-
end
|
39
|
-
rescue => e
|
40
|
-
# Default to a reasonable limit if count fails
|
41
|
-
commit_limit = 200
|
42
|
-
end
|
43
|
-
|
44
|
-
# Debug messages removed
|
45
|
-
|
46
|
-
# Always get ALL potential commits
|
47
|
-
all_commits = []
|
48
|
-
|
49
|
-
if commit_limit > 0
|
50
|
-
begin
|
51
|
-
# Get commits directly with git and process manually
|
52
|
-
# This is more reliable than using the git gem
|
53
|
-
log_output = repo.lib.run_command('log', ['--pretty=format:%H|%ad|%s', '--date=iso', "-#{commit_limit}"])
|
54
|
-
commit_lines = log_output.split("\n")
|
55
|
-
|
56
|
-
# Random shuffle the commits first to avoid any ordering bias
|
57
|
-
shuffled_lines = commit_lines.shuffle
|
58
|
-
|
59
|
-
# Parse and process commits
|
60
|
-
shuffled_lines.each do |line|
|
61
|
-
parts = line.split('|', 3)
|
62
|
-
next if parts.size < 3
|
63
|
-
|
64
|
-
sha = parts[0]
|
65
|
-
date_str = parts[1]
|
66
|
-
message = parts[2]
|
67
|
-
|
68
|
-
# Try to get changed files
|
69
|
-
begin
|
70
|
-
diff_output = repo.lib.run_command('diff', ['--name-only', "#{sha}^", sha]) rescue nil
|
71
|
-
|
72
|
-
# For the first commit that has no parent
|
73
|
-
if diff_output.nil? || diff_output.empty?
|
74
|
-
diff_output = repo.lib.run_command('show', ['--name-only', '--pretty=format:', sha]) rescue ""
|
75
|
-
end
|
76
|
-
|
77
|
-
# Parse changed files
|
78
|
-
files = diff_output.split("\n").reject(&:empty?)
|
79
|
-
|
80
|
-
# Skip empty or very large change sets
|
81
|
-
next if files.empty?
|
82
|
-
next if files.size > 10
|
83
|
-
|
84
|
-
# Create proper commit data structure
|
85
|
-
all_commits << {
|
86
|
-
sha: sha,
|
87
|
-
date_str: date_str,
|
88
|
-
message: message,
|
89
|
-
files: files
|
90
|
-
}
|
91
|
-
|
92
|
-
# Once we have enough commits, we can stop processing
|
93
|
-
break if all_commits.size >= 30
|
94
|
-
|
95
|
-
rescue => e
|
96
|
-
# Skip this commit if we can't get files
|
97
|
-
next
|
98
|
-
end
|
99
|
-
end
|
100
|
-
rescue => e
|
101
|
-
# Error handling - just continue silently
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
# Debug message removed
|
106
|
-
|
107
|
-
# If we couldn't find enough commits, use sample questions
|
108
|
-
if all_commits.size < self.class.questions_per_round
|
109
|
-
return generate_sample_questions
|
110
|
-
end
|
111
|
-
|
112
|
-
# Select a diverse set of commits - just take random ones since we already shuffled
|
113
|
-
selected_commits = all_commits.sample(self.class.questions_per_round * 2)
|
114
|
-
|
115
|
-
# Now select final set with emphasis on file diversity
|
116
|
-
final_commits = []
|
117
|
-
file_types_seen = {}
|
118
|
-
|
119
|
-
selected_commits.each do |commit|
|
120
|
-
# Skip if we already have enough
|
121
|
-
break if final_commits.size >= self.class.questions_per_round
|
122
|
-
|
123
|
-
# Get the primary file type from first file
|
124
|
-
first_file = commit[:files].first
|
125
|
-
ext = File.extname(first_file).downcase
|
126
|
-
|
127
|
-
# If we haven't seen this file type yet, prioritize it
|
128
|
-
if !file_types_seen[ext]
|
129
|
-
file_types_seen[ext] = true
|
130
|
-
final_commits << commit
|
131
|
-
elsif final_commits.size < self.class.questions_per_round
|
132
|
-
# Add this commit only if we need more
|
133
|
-
final_commits << commit
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
# If we still don't have enough, add more random ones
|
138
|
-
if final_commits.size < self.class.questions_per_round
|
139
|
-
remaining = selected_commits - final_commits
|
140
|
-
final_commits += remaining.sample(self.class.questions_per_round - final_commits.size)
|
141
|
-
end
|
142
|
-
|
143
|
-
# Debug message removed
|
144
|
-
|
145
|
-
questions = []
|
146
|
-
|
147
|
-
# Use selected commits for questions
|
148
|
-
final_commits.take(self.class.questions_per_round).each do |commit_data|
|
149
|
-
# Get the commit data
|
150
|
-
sha = commit_data[:sha]
|
151
|
-
short_sha = sha[0..6]
|
152
|
-
message = commit_data[:message]
|
153
|
-
files = commit_data[:files]
|
154
|
-
date_str = commit_data[:date_str]
|
155
|
-
|
156
|
-
# Take first line of message if multiple lines
|
157
|
-
message = message.split("\n").first.strip if message.include?("\n")
|
158
|
-
|
159
|
-
# Select the correct file (first one for simplicity)
|
160
|
-
correct_file = files.first
|
161
|
-
|
162
|
-
# Get file paths from other commits to use as incorrect options
|
163
|
-
other_files = []
|
164
|
-
other_commits = final_commits - [commit_data]
|
165
|
-
|
166
|
-
# Collect files from other commits
|
167
|
-
other_commits.each do |other_commit|
|
168
|
-
other_commit[:files].each do |file|
|
169
|
-
other_files << file unless files.include?(file)
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
# If we don't have enough other files, use some from sample data
|
174
|
-
if other_files.size < 3
|
175
|
-
sample_files = [
|
176
|
-
"src/main.js", "lib/utils.js", "css/styles.css", "README.md",
|
177
|
-
"package.json", "Dockerfile", ".github/workflows/ci.yml",
|
178
|
-
"src/components/Header.js", "app/models/user.rb", "config/database.yml"
|
179
|
-
]
|
180
|
-
other_files += sample_files.reject { |f| files.include?(f) }
|
181
|
-
end
|
182
|
-
|
183
|
-
# Take up to 3 unique other files
|
184
|
-
other_files = other_files.uniq.sample(3)
|
185
|
-
|
186
|
-
# Create options array with the correct answer and incorrect ones
|
187
|
-
all_options = ([correct_file] + other_files).shuffle
|
188
|
-
|
189
|
-
# Format the commit date nicely if possible
|
190
|
-
nice_date = begin
|
191
|
-
parsed_date = Time.parse(date_str)
|
192
|
-
parsed_date.strftime('%b %d, %Y')
|
193
|
-
rescue
|
194
|
-
date_str
|
195
|
-
end
|
196
|
-
|
197
|
-
# Format consistently with other mini-games
|
198
|
-
questions << {
|
199
|
-
question: "Which file was most likely changed in this commit?\n\n \"#{message}\"",
|
200
|
-
commit_info: "#{short_sha} (#{nice_date})",
|
201
|
-
options: all_options,
|
202
|
-
correct_answer: correct_file
|
203
|
-
}
|
204
|
-
end
|
205
|
-
|
206
|
-
return questions
|
207
|
-
rescue => e
|
208
|
-
# If anything fails, fall back to sample questions
|
209
|
-
return generate_sample_questions
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
def evaluate_answers(question, player_answers)
|
214
|
-
results = {}
|
215
|
-
|
216
|
-
player_answers.each do |player_name, answer_data|
|
217
|
-
player_answer = answer_data[:answer]
|
218
|
-
correct = player_answer == question[:correct_answer]
|
219
|
-
|
220
|
-
points = 0
|
221
|
-
|
222
|
-
if correct
|
223
|
-
points = 10 # Base points for correct answer
|
224
|
-
|
225
|
-
# Bonus points for fast answers, identical to AuthorQuiz
|
226
|
-
time_taken = answer_data[:time_taken] || 15
|
227
|
-
if time_taken < 5
|
228
|
-
points += 5
|
229
|
-
elsif time_taken < 10
|
230
|
-
points += 3
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
results[player_name] = {
|
235
|
-
answer: player_answer,
|
236
|
-
correct: correct,
|
237
|
-
points: points
|
238
|
-
}
|
239
|
-
end
|
240
|
-
|
241
|
-
results
|
242
|
-
end
|
243
|
-
|
244
|
-
# Generate sample questions with a lot more variety
|
245
|
-
def generate_sample_questions
|
246
|
-
questions = []
|
247
|
-
|
248
|
-
# MUCH larger set of sample files that might be changed in a project
|
249
|
-
common_files = [
|
250
|
-
# Frontend files
|
251
|
-
"src/main.js", "src/app.js", "src/index.js", "src/router.js",
|
252
|
-
"src/components/Header.js", "src/components/Footer.js", "src/components/Sidebar.js",
|
253
|
-
"src/components/Navigation.js", "src/components/UserProfile.js", "src/components/Dashboard.js",
|
254
|
-
"src/views/Home.vue", "src/views/Login.vue", "src/views/Settings.vue",
|
255
|
-
"public/index.html", "public/favicon.ico", "public/manifest.json",
|
256
|
-
|
257
|
-
# Styling files
|
258
|
-
"css/styles.css", "css/main.css", "styles/theme.scss", "styles/variables.scss",
|
259
|
-
"src/assets/styles.css", "src/styles/global.css", "sass/main.scss",
|
260
|
-
|
261
|
-
# Backend files
|
262
|
-
"lib/utils.js", "lib/helpers.js", "lib/auth.js", "lib/database.js",
|
263
|
-
"server/index.js", "server/api.js", "server/middleware/auth.js",
|
264
|
-
"app/controllers/users_controller.rb", "app/models/user.rb", "app/models/post.rb",
|
265
|
-
"app/services/authentication_service.rb", "app/helpers/application_helper.rb",
|
266
|
-
|
267
|
-
# Configuration files
|
268
|
-
"config/webpack.config.js", "config/database.yml", "config/routes.rb",
|
269
|
-
"config/application.rb", ".eslintrc.js", ".prettierrc", "tsconfig.json",
|
270
|
-
"babel.config.js", "webpack.config.js", "vite.config.js", "jest.config.js",
|
271
|
-
|
272
|
-
# Documentation files
|
273
|
-
"README.md", "CONTRIBUTING.md", "LICENSE", "CHANGELOG.md", "docs/API.md",
|
274
|
-
"docs/setup.md", "docs/deployment.md", "docs/architecture.md",
|
275
|
-
|
276
|
-
# DevOps files
|
277
|
-
"Dockerfile", "docker-compose.yml", ".github/workflows/ci.yml",
|
278
|
-
".github/workflows/deploy.yml", ".gitlab-ci.yml", "Jenkinsfile",
|
279
|
-
|
280
|
-
# Testing files
|
281
|
-
"tests/unit/login.test.js", "tests/integration/auth.test.js",
|
282
|
-
"spec/models/user_spec.rb", "spec/controllers/posts_controller_spec.rb",
|
283
|
-
"__tests__/components/Header.test.tsx", "cypress/integration/login.spec.js",
|
284
|
-
|
285
|
-
# Assets
|
286
|
-
"public/images/logo.png", "public/images/banner.jpg", "src/assets/icons/home.svg",
|
287
|
-
"public/fonts/OpenSans.woff2", "public/data/countries.json"
|
288
|
-
]
|
289
|
-
|
290
|
-
# MUCH larger set of sample commit messages with realistic commit hashes
|
291
|
-
sample_commits = [
|
292
|
-
# UI/Frontend commits
|
293
|
-
{
|
294
|
-
message: "Fix navigation bar styling on mobile devices",
|
295
|
-
file: "css/styles.css",
|
296
|
-
sha: rand(0xfffff).to_s(16),
|
297
|
-
date: "Mar #{rand(1..28)}, #{2023 + rand(3)}"
|
298
|
-
},
|
299
|
-
{
|
300
|
-
message: "Add responsive design for dashboard components",
|
301
|
-
file: "src/components/Dashboard.js",
|
302
|
-
sha: rand(0xfffff).to_s(16),
|
303
|
-
date: "Jan #{rand(1..28)}, #{2023 + rand(3)}"
|
304
|
-
},
|
305
|
-
{
|
306
|
-
message: "Update color scheme in theme variables",
|
307
|
-
file: "styles/variables.scss",
|
308
|
-
sha: rand(0xfffff).to_s(16),
|
309
|
-
date: "Apr #{rand(1..28)}, #{2023 + rand(3)}"
|
310
|
-
},
|
311
|
-
{
|
312
|
-
message: "Implement dark mode toggle in user settings",
|
313
|
-
file: "src/views/Settings.vue",
|
314
|
-
sha: rand(0xfffff).to_s(16),
|
315
|
-
date: "May #{rand(1..28)}, #{2023 + rand(3)}"
|
316
|
-
},
|
317
|
-
|
318
|
-
# Backend/API commits
|
319
|
-
{
|
320
|
-
message: "Fix user authentication bug in login flow",
|
321
|
-
file: "lib/auth.js",
|
322
|
-
sha: rand(0xfffff).to_s(16),
|
323
|
-
date: "Feb #{rand(1..28)}, #{2023 + rand(3)}"
|
324
|
-
},
|
325
|
-
{
|
326
|
-
message: "Add rate limiting to API endpoints",
|
327
|
-
file: "server/middleware/auth.js",
|
328
|
-
sha: rand(0xfffff).to_s(16),
|
329
|
-
date: "Jun #{rand(1..28)}, #{2023 + rand(3)}"
|
330
|
-
},
|
331
|
-
{
|
332
|
-
message: "Optimize database queries for user profile page",
|
333
|
-
file: "app/controllers/users_controller.rb",
|
334
|
-
sha: rand(0xfffff).to_s(16),
|
335
|
-
date: "Jul #{rand(1..28)}, #{2023 + rand(3)}"
|
336
|
-
},
|
337
|
-
|
338
|
-
# Testing commits
|
339
|
-
{
|
340
|
-
message: "Add unit tests for authentication service",
|
341
|
-
file: "tests/unit/login.test.js",
|
342
|
-
sha: rand(0xfffff).to_s(16),
|
343
|
-
date: "Feb #{rand(1..28)}, #{2023 + rand(3)}"
|
344
|
-
},
|
345
|
-
{
|
346
|
-
message: "Fix flaky integration tests for payment flow",
|
347
|
-
file: "tests/integration/auth.test.js",
|
348
|
-
sha: rand(0xfffff).to_s(16),
|
349
|
-
date: "Mar #{rand(1..28)}, #{2023 + rand(3)}"
|
350
|
-
},
|
351
|
-
{
|
352
|
-
message: "Add E2E tests for user registration",
|
353
|
-
file: "cypress/integration/login.spec.js",
|
354
|
-
sha: rand(0xfffff).to_s(16),
|
355
|
-
date: "Apr #{rand(1..28)}, #{2023 + rand(3)}"
|
356
|
-
},
|
357
|
-
|
358
|
-
# DevOps/Infrastructure commits
|
359
|
-
{
|
360
|
-
message: "Update CI pipeline to run tests in parallel",
|
361
|
-
file: ".github/workflows/ci.yml",
|
362
|
-
sha: rand(0xfffff).to_s(16),
|
363
|
-
date: "May #{rand(1..28)}, #{2023 + rand(3)}"
|
364
|
-
},
|
365
|
-
{
|
366
|
-
message: "Add Docker support for development environment",
|
367
|
-
file: "Dockerfile",
|
368
|
-
sha: rand(0xfffff).to_s(16),
|
369
|
-
date: "Jun #{rand(1..28)}, #{2023 + rand(3)}"
|
370
|
-
},
|
371
|
-
{
|
372
|
-
message: "Configure automatic deployment to staging",
|
373
|
-
file: ".github/workflows/deploy.yml",
|
374
|
-
sha: rand(0xfffff).to_s(16),
|
375
|
-
date: "Jul #{rand(1..28)}, #{2023 + rand(3)}"
|
376
|
-
},
|
377
|
-
|
378
|
-
# Documentation commits
|
379
|
-
{
|
380
|
-
message: "Update README with new installation instructions",
|
381
|
-
file: "README.md",
|
382
|
-
sha: rand(0xfffff).to_s(16),
|
383
|
-
date: "Aug #{rand(1..28)}, #{2023 + rand(3)}"
|
384
|
-
},
|
385
|
-
{
|
386
|
-
message: "Add API documentation for new endpoints",
|
387
|
-
file: "docs/API.md",
|
388
|
-
sha: rand(0xfffff).to_s(16),
|
389
|
-
date: "Sep #{rand(1..28)}, #{2023 + rand(3)}"
|
390
|
-
},
|
391
|
-
{
|
392
|
-
message: "Update CHANGELOG for v2.3.0 release",
|
393
|
-
file: "CHANGELOG.md",
|
394
|
-
sha: rand(0xfffff).to_s(16),
|
395
|
-
date: "Oct #{rand(1..28)}, #{2023 + rand(3)}"
|
396
|
-
}
|
397
|
-
]
|
398
|
-
|
399
|
-
# Randomize which commits we use for each round
|
400
|
-
selected_commits = sample_commits.sample(self.class.questions_per_round * 2)
|
401
|
-
|
402
|
-
# Create questions from sample data
|
403
|
-
self.class.questions_per_round.times do |i|
|
404
|
-
# Different commit each time regardless of how many rounds we play
|
405
|
-
sample_commit = selected_commits[i]
|
406
|
-
|
407
|
-
# Correct file for this sample commit
|
408
|
-
correct_file = sample_commit[:file]
|
409
|
-
|
410
|
-
# Get other files as incorrect options
|
411
|
-
other_files = common_files.reject { |f| f == correct_file }.sample(3)
|
412
|
-
|
413
|
-
# All options with the correct one included
|
414
|
-
all_options = ([correct_file] + other_files).shuffle
|
415
|
-
|
416
|
-
questions << {
|
417
|
-
question: "Which file was most likely changed in this commit?\n\n \"#{sample_commit[:message]}\"",
|
418
|
-
commit_info: "#{sample_commit[:sha]} (#{sample_commit[:date]})",
|
419
|
-
options: all_options,
|
420
|
-
correct_answer: correct_file
|
421
|
-
}
|
422
|
-
end
|
423
|
-
|
424
|
-
# Randomize the question order
|
425
|
-
questions.shuffle
|
426
|
-
end
|
427
|
-
|
428
|
-
private
|
429
|
-
|
430
|
-
# Helper method to get commits with their changed files
|
431
|
-
# Optionally filter by date (commits after the specified date)
|
432
|
-
def get_recent_commits_with_files(repo, count, after_date = nil)
|
433
|
-
begin
|
434
|
-
# Get commits
|
435
|
-
commits = repo.log(count).to_a
|
436
|
-
|
437
|
-
# Filter by date if specified
|
438
|
-
if after_date
|
439
|
-
commits = commits.select do |commit|
|
440
|
-
begin
|
441
|
-
commit_time = commit.date.is_a?(Time) ? commit.date : Time.parse(commit.date.to_s)
|
442
|
-
commit_time > after_date
|
443
|
-
rescue
|
444
|
-
false # Skip commits with unparseable dates
|
445
|
-
end
|
446
|
-
end
|
447
|
-
end
|
448
|
-
|
449
|
-
commits_with_files = commits.map do |commit|
|
450
|
-
# Get diff from previous commit
|
451
|
-
diff_files = []
|
452
|
-
|
453
|
-
begin
|
454
|
-
# Use git command directly for simplicity
|
455
|
-
diff_output = repo.lib.run_command('diff', ['--name-only', "#{commit.sha}^", commit.sha])
|
456
|
-
diff_files = diff_output.split("\n").reject(&:empty?)
|
457
|
-
rescue => e
|
458
|
-
# Handle the case when the commit is the first commit (no parent)
|
459
|
-
if commit.parent.nil?
|
460
|
-
begin
|
461
|
-
diff_output = repo.lib.run_command('show', ['--name-only', '--pretty=format:', commit.sha])
|
462
|
-
diff_files = diff_output.split("\n").reject(&:empty?)
|
463
|
-
rescue => e
|
464
|
-
# If we can't get files for this commit, just use an empty array
|
465
|
-
diff_files = []
|
466
|
-
end
|
467
|
-
end
|
468
|
-
end
|
469
|
-
|
470
|
-
# Skip commits that modified too many files (likely big refactors or dependency updates)
|
471
|
-
next nil if diff_files.size > 20
|
472
|
-
|
473
|
-
# Skip commits with no files
|
474
|
-
next nil if diff_files.empty?
|
475
|
-
|
476
|
-
{
|
477
|
-
commit: commit,
|
478
|
-
files: diff_files,
|
479
|
-
file_types: get_file_types(diff_files) # Store file types for better selection
|
480
|
-
}
|
481
|
-
end
|
482
|
-
|
483
|
-
# Filter out nil entries from commits that were skipped
|
484
|
-
commits_with_files.compact
|
485
|
-
rescue => e
|
486
|
-
# If anything fails, return an empty array
|
487
|
-
[]
|
488
|
-
end
|
489
|
-
end
|
490
|
-
|
491
|
-
# Helper method to categorize file types based on extension
|
492
|
-
def get_file_types(files)
|
493
|
-
types = {}
|
494
|
-
|
495
|
-
files.each do |file|
|
496
|
-
ext = File.extname(file).downcase
|
497
|
-
types[ext] ||= 0
|
498
|
-
types[ext] += 1
|
499
|
-
end
|
500
|
-
|
501
|
-
types
|
502
|
-
end
|
503
|
-
|
504
|
-
# Select diverse commits to ensure variety in questions
|
505
|
-
def select_diverse_commits(commits, count)
|
506
|
-
return commits.sample(count) if commits.size <= count
|
507
|
-
|
508
|
-
# Strategy: Select commits that provide maximum diversity in:
|
509
|
-
# 1. Time periods
|
510
|
-
# 2. File types
|
511
|
-
# 3. Author variety
|
512
|
-
selected = []
|
513
|
-
|
514
|
-
# First, sort by date to get a chronological view
|
515
|
-
sorted_by_date = commits.sort_by do |c|
|
516
|
-
begin
|
517
|
-
date = c[:commit].date
|
518
|
-
date.is_a?(Time) ? date : Time.parse(date.to_s)
|
519
|
-
rescue
|
520
|
-
Time.now
|
521
|
-
end
|
522
|
-
end
|
523
|
-
|
524
|
-
# Divide into time buckets to ensure time diversity
|
525
|
-
bucket_size = [(sorted_by_date.size / 5).ceil, 1].max
|
526
|
-
time_buckets = sorted_by_date.each_slice(bucket_size).to_a
|
527
|
-
|
528
|
-
# Take one from each time bucket first (prioritizing time diversity)
|
529
|
-
time_buckets.each do |bucket|
|
530
|
-
break if selected.size >= count
|
531
|
-
selected << bucket.sample
|
532
|
-
end
|
533
|
-
|
534
|
-
remaining = commits - selected
|
535
|
-
|
536
|
-
# Next, group remaining by file type
|
537
|
-
file_type_groups = {}
|
538
|
-
remaining.each do |commit|
|
539
|
-
# Find most common file type in this commit
|
540
|
-
primary_type = commit[:file_types].max_by { |_, count| count }&.first || "unknown"
|
541
|
-
file_type_groups[primary_type] ||= []
|
542
|
-
file_type_groups[primary_type] << commit
|
543
|
-
end
|
544
|
-
|
545
|
-
# Add one from each file type group
|
546
|
-
file_type_groups.keys.shuffle.each do |file_type|
|
547
|
-
break if selected.size >= count
|
548
|
-
next if file_type_groups[file_type].empty?
|
549
|
-
|
550
|
-
commit = file_type_groups[file_type].sample
|
551
|
-
selected << commit
|
552
|
-
remaining.delete(commit)
|
553
|
-
end
|
554
|
-
|
555
|
-
# Group remaining by author
|
556
|
-
author_groups = {}
|
557
|
-
remaining.each do |commit|
|
558
|
-
begin
|
559
|
-
author = commit[:commit].author.name || "unknown"
|
560
|
-
author_groups[author] ||= []
|
561
|
-
author_groups[author] << commit
|
562
|
-
rescue
|
563
|
-
# Skip if author info not available
|
564
|
-
end
|
565
|
-
end
|
566
|
-
|
567
|
-
# Add one from each author group
|
568
|
-
author_groups.keys.shuffle.each do |author|
|
569
|
-
break if selected.size >= count
|
570
|
-
next if author_groups[author].empty?
|
571
|
-
|
572
|
-
commit = author_groups[author].sample
|
573
|
-
selected << commit
|
574
|
-
remaining.delete(commit)
|
575
|
-
end
|
576
|
-
|
577
|
-
# If we still need more, add random remaining commits
|
578
|
-
if selected.size < count && !remaining.empty?
|
579
|
-
selected += remaining.sample(count - selected.size)
|
580
|
-
end
|
581
|
-
|
582
|
-
# Ensure we have exactly the requested number
|
583
|
-
selected = selected.take(count)
|
584
|
-
|
585
|
-
# Return the selected commits in random order to avoid predictable patterns
|
586
|
-
selected.shuffle
|
587
|
-
end
|
588
|
-
end
|
589
|
-
end
|