git_game_show 0.1.7 → 0.1.9
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/cli.rb +1 -0
- data/lib/git_game_show/game_server.rb +1 -1
- data/lib/git_game_show/player_client.rb +5 -9
- data/lib/git_game_show/version.rb +1 -1
- data/mini_games/file_quiz.rb +304 -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: 6dd69e30776e28d63d3fd20db47716c0e957dace72788a5e0479047fe8cbe708
|
4
|
+
data.tar.gz: cd8e8579ec722b25a67ffd1c8268f35a724dcf75cd863420ffebad9025f1ebf4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8874ab5bb9a8349e9b22926e8456fef2aa1ce4bcdaf1ca14b170d82d6e0491a68db6c576eda153b92fbca942efa1861dd44d139d1d57e709fe2853e5c2b36fe6
|
7
|
+
data.tar.gz: 37c9292edb46b4e3be1283efbd09b3762c504696a5d875a16c7d8a12fdbc68265319f40b4db992e0c4fd71d42bf420abcce75bd3e62402c741ba38e459c265a2
|
data/lib/git_game_show/cli.rb
CHANGED
@@ -510,6 +510,7 @@ module GitGameShow
|
|
510
510
|
|
511
511
|
def display_ggs
|
512
512
|
clear_screen
|
513
|
+
puts ""
|
513
514
|
lines = [
|
514
515
|
" ██████╗ ".colorize(:red) + " ██████╗ ".colorize(:green) + " █████╗".colorize(:blue),
|
515
516
|
"██╔════╝ ".colorize(:red) + " ██╔════╝ ".colorize(:green) + " ██╔═══╝".colorize(:blue),
|
@@ -543,7 +543,7 @@ module GitGameShow
|
|
543
543
|
|
544
544
|
# Display a fun "Game Starting" animation
|
545
545
|
box_width = 40
|
546
|
-
puts "\n
|
546
|
+
puts "\n"
|
547
547
|
puts ("╭" + "─" * box_width + "╮").center(@game_width).colorize(:green)
|
548
548
|
puts ("│" + "Game starting...".center(box_width) + "│").center(@game_width).colorize(:green)
|
549
549
|
puts ("╰" + "─" * box_width + "╯").center(@game_width).colorize(:green)
|
@@ -600,8 +600,6 @@ module GitGameShow
|
|
600
600
|
mini_game = data['mini_game']
|
601
601
|
description = data['description']
|
602
602
|
|
603
|
-
puts "\n\n"
|
604
|
-
|
605
603
|
# Box is drawn with exactly 45 "━" characters for the top and bottom borders
|
606
604
|
# The top and bottom including borders are 48 characters wide
|
607
605
|
box_width = 42
|
@@ -610,6 +608,7 @@ module GitGameShow
|
|
610
608
|
box_middle = "│#{"Round #{round_num} of #{total_rounds}".center(box_width - 2)}│".center(@game_width)
|
611
609
|
|
612
610
|
# Output the box
|
611
|
+
puts "\n"
|
613
612
|
puts box_top.colorize(:green)
|
614
613
|
puts box_middle.colorize(:green)
|
615
614
|
puts box_bottom.colorize(:green)
|
@@ -643,9 +642,6 @@ module GitGameShow
|
|
643
642
|
|
644
643
|
# No need to reserve space for timer - it will be at the bottom of the screen
|
645
644
|
|
646
|
-
# Display question header
|
647
|
-
puts "\n"
|
648
|
-
|
649
645
|
# Draw a simple box for the question header
|
650
646
|
box_width = 42
|
651
647
|
box_top = ("╭" + "─" * (box_width - 2) + "╮").center(@game_width)
|
@@ -653,6 +649,7 @@ module GitGameShow
|
|
653
649
|
box_middle = "│#{"Question #{question_num} of #{total_questions}".center(box_width - 2)}│".center(@game_width)
|
654
650
|
|
655
651
|
# Output the question box
|
652
|
+
puts "\n"
|
656
653
|
puts box_top.colorize(:light_blue)
|
657
654
|
puts box_middle.colorize(:light_blue)
|
658
655
|
puts box_bottom.colorize(:light_blue)
|
@@ -867,8 +864,6 @@ module GitGameShow
|
|
867
864
|
# Start with a clean screen
|
868
865
|
clear_screen
|
869
866
|
|
870
|
-
puts "\n"
|
871
|
-
|
872
867
|
# Box is drawn with exactly 45 "━" characters for the top and bottom borders
|
873
868
|
# The top and bottom including borders are 48 characters wide
|
874
869
|
box_width = 40
|
@@ -877,6 +872,7 @@ module GitGameShow
|
|
877
872
|
box_middle = "│#{'Round Results'.center(box_width)}│".center(@game_width)
|
878
873
|
|
879
874
|
# Output the box
|
875
|
+
puts "\n"
|
880
876
|
puts box_top.colorize(:light_blue)
|
881
877
|
puts box_middle.colorize(:light_blue)
|
882
878
|
puts box_bottom.colorize(:light_blue)
|
@@ -988,7 +984,7 @@ module GitGameShow
|
|
988
984
|
clear_screen
|
989
985
|
|
990
986
|
box_width = 40
|
991
|
-
puts ""
|
987
|
+
puts "\n"
|
992
988
|
puts ("╭" + "─" * box_width + "╮").center(@game_width).colorize(:yellow)
|
993
989
|
puts "│#{'Scoreboard'.center(box_width)}┃".center(@game_width).colorize(:yellow)
|
994
990
|
puts ("╰" + "─" * box_width + "╯").center(@game_width).colorize(:yellow)
|
@@ -0,0 +1,304 @@
|
|
1
|
+
module GitGameShow
|
2
|
+
# Disable debug mode for normal operation
|
3
|
+
$FILE_QUIZ_DEBUG = false
|
4
|
+
|
5
|
+
class FileQuiz < MiniGame
|
6
|
+
self.name = "File Quiz"
|
7
|
+
self.description = "Match the commit message to the right changed file!"
|
8
|
+
self.questions_per_round = 5
|
9
|
+
|
10
|
+
# Custom timing for this mini-game (same as AuthorQuiz)
|
11
|
+
def self.question_timeout
|
12
|
+
15 # 15 seconds per question
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.question_display_time
|
16
|
+
5 # 5 seconds between questions
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_questions(repo)
|
20
|
+
begin
|
21
|
+
# Use the same approach as AuthorQuiz - get ALL commits using the helper method
|
22
|
+
commits = get_all_commits(repo)
|
23
|
+
|
24
|
+
# Check if we got any commits at all
|
25
|
+
if commits.nil? || commits.empty?
|
26
|
+
# Silently fall back to sample questions
|
27
|
+
return generate_sample_questions
|
28
|
+
end
|
29
|
+
|
30
|
+
# Shuffle commits for better variety
|
31
|
+
commits.shuffle!
|
32
|
+
|
33
|
+
# Process commits to find ones with good file changes
|
34
|
+
valid_commits = []
|
35
|
+
|
36
|
+
commits.each do |commit|
|
37
|
+
# Get changed files for this commit
|
38
|
+
changed_files = []
|
39
|
+
|
40
|
+
begin
|
41
|
+
# Use standard Git methods instead of run_command (silently)
|
42
|
+
|
43
|
+
# Use git commands quietly without showing output
|
44
|
+
if commit.parents.empty?
|
45
|
+
# For first commit, get files directly
|
46
|
+
cmd = "cd #{repo.dir.path} && git show --name-only --pretty=format: #{commit.sha} 2>/dev/null"
|
47
|
+
diff_output = `#{cmd}`
|
48
|
+
else
|
49
|
+
# For normal commits with parents
|
50
|
+
cmd = "cd #{repo.dir.path} && git diff --name-only #{commit.sha}^ #{commit.sha} 2>/dev/null"
|
51
|
+
diff_output = `#{cmd}`
|
52
|
+
|
53
|
+
# If diff returns empty, try using show as fallback
|
54
|
+
if diff_output.strip.empty?
|
55
|
+
cmd = "cd #{repo.dir.path} && git show --name-only --pretty=format: #{commit.sha} 2>/dev/null"
|
56
|
+
diff_output = `#{cmd}`
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Parse the output to get changed files (quietly)
|
61
|
+
changed_files = diff_output.split("\n").reject(&:empty?)
|
62
|
+
|
63
|
+
# Skip if no files changed (can't create a question for this)
|
64
|
+
next if changed_files.empty?
|
65
|
+
|
66
|
+
# Super relaxed - accept ANY commit with files that changed
|
67
|
+
# No more filtering by number of files
|
68
|
+
|
69
|
+
# Create structure with relevant commit data
|
70
|
+
valid_commits << {
|
71
|
+
sha: commit.sha,
|
72
|
+
message: commit.message,
|
73
|
+
author: commit.author.name,
|
74
|
+
date: commit.date,
|
75
|
+
files: changed_files
|
76
|
+
}
|
77
|
+
|
78
|
+
# Once we have enough commits, we can stop
|
79
|
+
# Get more commits to have a better selection
|
80
|
+
break if valid_commits.size >= self.class.questions_per_round * 5
|
81
|
+
rescue => e
|
82
|
+
# Silently skip problematic commits
|
83
|
+
next
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Never fall back to samples as long as we have at least 1 valid commit
|
88
|
+
if valid_commits.empty?
|
89
|
+
# Silently fall back to sample questions if needed
|
90
|
+
return generate_sample_questions
|
91
|
+
end
|
92
|
+
|
93
|
+
# Prioritize commits that modified interesting files (not just .gitignore etc.)
|
94
|
+
prioritized_commits = valid_commits.sort_by do |commit|
|
95
|
+
# Higher score = more interesting commit
|
96
|
+
score = 0
|
97
|
+
|
98
|
+
# Prioritize based on file types
|
99
|
+
commit[:files].each do |file|
|
100
|
+
ext = File.extname(file).downcase
|
101
|
+
|
102
|
+
case ext
|
103
|
+
when '.rb', '.js', '.py', '.java', '.tsx', '.jsx'
|
104
|
+
score += 3 # Source code is most interesting
|
105
|
+
when '.html', '.css', '.scss'
|
106
|
+
score += 2 # Templates and styles are interesting
|
107
|
+
when '.md', '.txt', '.json', '.yaml', '.yml'
|
108
|
+
score += 1 # Config and docs are moderately interesting
|
109
|
+
when '', '.gitignore', '.gitattributes'
|
110
|
+
score -= 1 # Less interesting files
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Prioritize based on commit message length - longer messages are often more descriptive
|
115
|
+
message_length = commit[:message].to_s.strip.length
|
116
|
+
score += [message_length / 20, 5].min
|
117
|
+
|
118
|
+
# Return negative score so highest scores come first in sort
|
119
|
+
-score
|
120
|
+
end
|
121
|
+
|
122
|
+
# Take as many commits as we need for questions
|
123
|
+
needed_commits = [self.class.questions_per_round, prioritized_commits.size].min
|
124
|
+
selected_commits = prioritized_commits.take(needed_commits)
|
125
|
+
|
126
|
+
# Create questions from selected commits
|
127
|
+
questions = []
|
128
|
+
|
129
|
+
selected_commits.each do |commit|
|
130
|
+
# Choose the most interesting file as the correct answer
|
131
|
+
files = commit[:files]
|
132
|
+
|
133
|
+
# Skip if somehow we got a commit with no files
|
134
|
+
next if files.empty?
|
135
|
+
|
136
|
+
# Score files by interestingness
|
137
|
+
scored_files = files.map do |file|
|
138
|
+
ext = File.extname(file).downcase
|
139
|
+
|
140
|
+
# Start with base score by extension
|
141
|
+
score = case ext
|
142
|
+
when '.rb', '.js', '.py', '.java', '.tsx', '.jsx'
|
143
|
+
10 # Source code is most interesting
|
144
|
+
when '.html', '.css', '.scss'
|
145
|
+
8 # Templates and styles are interesting
|
146
|
+
when '.md', '.txt'
|
147
|
+
6 # Documentation
|
148
|
+
when '.json', '.yaml', '.yml'
|
149
|
+
4 # Config files
|
150
|
+
when '', '.gitignore', '.gitattributes'
|
151
|
+
0 # Less interesting files
|
152
|
+
else
|
153
|
+
5 # Other files are neutral
|
154
|
+
end
|
155
|
+
|
156
|
+
# Shorter paths are usually more recognizable
|
157
|
+
score -= [file.length / 10, 5].min
|
158
|
+
|
159
|
+
# Prefer files in main directories (src, lib, app) over deeply nested ones
|
160
|
+
if file.start_with?('src/', 'lib/', 'app/')
|
161
|
+
score += 3
|
162
|
+
end
|
163
|
+
|
164
|
+
[file, score]
|
165
|
+
end
|
166
|
+
|
167
|
+
# Sort by score (highest first) and select most interesting file
|
168
|
+
scored_files.sort_by! { |_, score| -score }
|
169
|
+
correct_file = scored_files.first[0]
|
170
|
+
|
171
|
+
# Get incorrect options from other commits
|
172
|
+
other_files = []
|
173
|
+
other_commits = selected_commits - [commit]
|
174
|
+
|
175
|
+
# Collect files from other commits
|
176
|
+
other_commits.each do |other_commit|
|
177
|
+
other_commit[:files].each do |file|
|
178
|
+
other_files << file unless files.include?(file)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# If we don't have enough other files, use some from sample data
|
183
|
+
if other_files.size < 3
|
184
|
+
sample_files = [
|
185
|
+
"src/main.js", "lib/utils.js", "css/styles.css", "README.md",
|
186
|
+
"package.json", "Dockerfile", ".github/workflows/ci.yml",
|
187
|
+
"src/components/Header.js", "app/models/user.rb", "config/database.yml"
|
188
|
+
]
|
189
|
+
other_files += sample_files.reject { |f| files.include?(f) }
|
190
|
+
end
|
191
|
+
|
192
|
+
# Take up to 3 unique other files, prioritizing diverse ones
|
193
|
+
other_files = other_files.uniq.sample(3)
|
194
|
+
|
195
|
+
# Create options array with the correct answer and incorrect ones
|
196
|
+
all_options = ([correct_file] + other_files).shuffle
|
197
|
+
|
198
|
+
# Format the commit date nicely
|
199
|
+
nice_date = commit[:date].strftime('%b %d, %Y') rescue "Unknown date"
|
200
|
+
|
201
|
+
# Clean up commit message - take first line if multiple lines
|
202
|
+
message = commit[:message].to_s.split("\n").first.strip
|
203
|
+
|
204
|
+
# Format consistently with other mini-games
|
205
|
+
questions << {
|
206
|
+
question: "Which file was most likely changed in this commit?\n\n \"#{message}\"",
|
207
|
+
commit_info: "#{commit[:sha][0..6]} (#{nice_date})",
|
208
|
+
options: all_options,
|
209
|
+
correct_answer: correct_file
|
210
|
+
}
|
211
|
+
end
|
212
|
+
|
213
|
+
# If we still couldn't create enough questions, use a mix of real and samples
|
214
|
+
if questions.size < self.class.questions_per_round
|
215
|
+
# Add sample questions to fill the remaining slots (silently)
|
216
|
+
sample_questions = generate_sample_questions
|
217
|
+
remaining_slots = self.class.questions_per_round - questions.size
|
218
|
+
questions += sample_questions.take(remaining_slots)
|
219
|
+
end
|
220
|
+
|
221
|
+
return questions
|
222
|
+
rescue => e
|
223
|
+
# If anything fails, silently fall back to sample questions
|
224
|
+
return generate_sample_questions
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def evaluate_answers(question, player_answers)
|
229
|
+
results = {}
|
230
|
+
|
231
|
+
player_answers.each do |player_name, answer_data|
|
232
|
+
answered = answer_data[:answered] || false
|
233
|
+
player_answer = answer_data[:answer]
|
234
|
+
correct = player_answer == question[:correct_answer]
|
235
|
+
|
236
|
+
points = correct ? 10 : 0
|
237
|
+
|
238
|
+
# Bonus points for fast answers (if correct)
|
239
|
+
if correct
|
240
|
+
time_taken = answer_data[:time_taken] || 15
|
241
|
+
|
242
|
+
if time_taken < 5
|
243
|
+
points += 5
|
244
|
+
elsif time_taken < 10
|
245
|
+
points += 3
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
results[player_name] = {
|
250
|
+
answer: player_answer,
|
251
|
+
correct: correct,
|
252
|
+
points: points
|
253
|
+
}
|
254
|
+
end
|
255
|
+
|
256
|
+
results
|
257
|
+
end
|
258
|
+
|
259
|
+
# Generate sample questions only used when no repository data is available
|
260
|
+
def generate_sample_questions
|
261
|
+
# No debug message in production
|
262
|
+
questions = []
|
263
|
+
|
264
|
+
# Common file options
|
265
|
+
common_files = [
|
266
|
+
"src/main.js",
|
267
|
+
"README.md",
|
268
|
+
"lib/utils.js",
|
269
|
+
"css/styles.css"
|
270
|
+
]
|
271
|
+
|
272
|
+
# Common sample commit messages
|
273
|
+
sample_messages = [
|
274
|
+
"Update documentation with new API endpoints",
|
275
|
+
"Fix styling issues in mobile view",
|
276
|
+
"Add error handling for network failures",
|
277
|
+
"Refactor authentication module for better performance",
|
278
|
+
"Update dependencies to latest versions"
|
279
|
+
]
|
280
|
+
|
281
|
+
# Create different sample questions for variety
|
282
|
+
self.class.questions_per_round.times do |i|
|
283
|
+
# Use modulo to cycle through sample messages
|
284
|
+
message = sample_messages[i % sample_messages.size]
|
285
|
+
|
286
|
+
# Different correct answers for each question
|
287
|
+
correct_file = common_files[i % common_files.size]
|
288
|
+
|
289
|
+
# Options are all files with the correct one included
|
290
|
+
all_options = common_files.shuffle
|
291
|
+
|
292
|
+
# Create the question - clearly label as sample data
|
293
|
+
questions << {
|
294
|
+
question: "Which file was most likely changed in this commit?\n\n \"#{message} (SAMPLE)\"",
|
295
|
+
commit_info: "sample#{i} (Demo Question)",
|
296
|
+
options: all_options,
|
297
|
+
correct_answer: correct_file
|
298
|
+
}
|
299
|
+
end
|
300
|
+
|
301
|
+
questions
|
302
|
+
end
|
303
|
+
end
|
304
|
+
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.9
|
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
|