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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0eaec4e8df244f6bce785020f5e51e64c3ce2a4a4a0f34e24b35067d00ff46b
4
- data.tar.gz: 1370dec1ae250bce28fc021353353a1a3fcf31b19e376128065377a5e7d0b280
3
+ metadata.gz: 6dd69e30776e28d63d3fd20db47716c0e957dace72788a5e0479047fe8cbe708
4
+ data.tar.gz: cd8e8579ec722b25a67ffd1c8268f35a724dcf75cd863420ffebad9025f1ebf4
5
5
  SHA512:
6
- metadata.gz: 9b32957cf357b2cd3d1d04c48fc285307cc948e7fcb150ec364a1e7ec2d41a552e894207935b8912729ffa6d763a42006508b741afe65af8dab0110227cd3c6e
7
- data.tar.gz: 1993c667e5c49e90c4c36a7f2888e61573bfda38113be4fa41dd91d095a339023bfd242d5fcbf87c07fae4f3da563815862d103e7b5cfce3665acccf85c15e13
6
+ metadata.gz: 8874ab5bb9a8349e9b22926e8456fef2aa1ce4bcdaf1ca14b170d82d6e0491a68db6c576eda153b92fbca942efa1861dd44d139d1d57e709fe2853e5c2b36fe6
7
+ data.tar.gz: 37c9292edb46b4e3be1283efbd09b3762c504696a5d875a16c7d8a12fdbc68265319f40b4db992e0c4fd71d42bf420abcce75bd3e62402c741ba38e459c265a2
@@ -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),
@@ -1592,7 +1592,7 @@ module GitGameShow
1592
1592
  # Enable all mini-games
1593
1593
  [
1594
1594
  GitGameShow::AuthorQuiz,
1595
- GitGameShow::CommitMessageQuiz,
1595
+ GitGameShow::FileQuiz,
1596
1596
  GitGameShow::CommitMessageCompletion,
1597
1597
  GitGameShow::DateOrderingQuiz
1598
1598
  ]
@@ -543,7 +543,7 @@ module GitGameShow
543
543
 
544
544
  # Display a fun "Game Starting" animation
545
545
  box_width = 40
546
- puts "\n\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)
@@ -1,4 +1,4 @@
1
1
  module GitGameShow
2
2
  # Current version of Git Game Show
3
- VERSION = "0.1.7"
3
+ VERSION = "0.1.9"
4
4
  end
@@ -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.7
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-05 00:00:00.000000000 Z
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