git_game_show 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/git_game_show/game_server.rb +65 -17
- data/lib/git_game_show/mini_game.rb +12 -12
- data/lib/git_game_show/player_client.rb +42 -14
- data/lib/git_game_show/version.rb +1 -1
- data/mini_games/author_quiz.rb +45 -32
- data/mini_games/blame_game.rb +392 -0
- data/mini_games/branch_detective.rb +241 -0
- data/mini_games/commit_message_completion.rb +49 -36
- data/mini_games/date_ordering_quiz.rb +64 -48
- data/mini_games/file_quiz.rb +13 -0
- metadata +4 -2
@@ -2,24 +2,37 @@ module GitGameShow
|
|
2
2
|
class CommitMessageCompletion < MiniGame
|
3
3
|
self.name = "Complete the Commit"
|
4
4
|
self.description = "Complete the missing part of these commit messages! (20 seconds per question)"
|
5
|
+
self.example = <<~EXAMPLE
|
6
|
+
Complete this commit message:
|
7
|
+
|
8
|
+
"Fix memory __________ in the background worker"
|
9
|
+
|
10
|
+
2d9a45c (Feb 25, 2025)
|
11
|
+
|
12
|
+
Choose your answer:
|
13
|
+
usage
|
14
|
+
allocation
|
15
|
+
\e[0;32;49m> leak\e[0m
|
16
|
+
error
|
17
|
+
EXAMPLE
|
5
18
|
self.questions_per_round = 5
|
6
|
-
|
19
|
+
|
7
20
|
# Custom timing for this mini-game (20 seconds instead of 15)
|
8
21
|
def self.question_timeout
|
9
22
|
20 # 20 seconds per question
|
10
23
|
end
|
11
|
-
|
24
|
+
|
12
25
|
def self.question_display_time
|
13
26
|
5 # 5 seconds between questions
|
14
27
|
end
|
15
|
-
|
28
|
+
|
16
29
|
def generate_questions(repo)
|
17
30
|
begin
|
18
31
|
commits = get_all_commits(repo)
|
19
|
-
|
32
|
+
|
20
33
|
# Filter commits with message length > 10 characters
|
21
34
|
valid_commits = commits.select { |commit| commit.message.strip.length > 10 }
|
22
|
-
|
35
|
+
|
23
36
|
# Fall back to sample questions if not enough valid commits
|
24
37
|
if valid_commits.size < self.class.questions_per_round
|
25
38
|
return generate_sample_questions
|
@@ -28,44 +41,44 @@ module GitGameShow
|
|
28
41
|
# If there's any error, fall back to sample questions
|
29
42
|
return generate_sample_questions
|
30
43
|
end
|
31
|
-
|
44
|
+
|
32
45
|
questions = []
|
33
46
|
valid_questions = 0
|
34
47
|
attempts = 0
|
35
48
|
max_attempts = 100 # Prevent infinite loops
|
36
|
-
|
49
|
+
|
37
50
|
# Keep trying until we have exactly 5 questions or reach max attempts
|
38
51
|
while valid_questions < self.class.questions_per_round && attempts < max_attempts
|
39
52
|
attempts += 1
|
40
|
-
|
53
|
+
|
41
54
|
# Get a random commit
|
42
55
|
commit = valid_commits.sample
|
43
56
|
message = commit.message.strip
|
44
|
-
|
57
|
+
|
45
58
|
# Split message into beginning and end parts
|
46
59
|
words = message.split(/\s+/)
|
47
|
-
|
60
|
+
|
48
61
|
# Skip if too few words
|
49
62
|
if words.size < 4
|
50
63
|
next
|
51
64
|
end
|
52
|
-
|
65
|
+
|
53
66
|
# Determine how much to hide (1/3 to 1/2 of words)
|
54
67
|
hide_count = [words.size / 3, 2].max
|
55
68
|
hide_start = rand(0..(words.size - hide_count))
|
56
69
|
hidden_words = words[hide_start...(hide_start + hide_count)]
|
57
|
-
|
70
|
+
|
58
71
|
# Replace hidden words with blanks
|
59
72
|
words[hide_start...(hide_start + hide_count)] = ['________'] * hide_count
|
60
|
-
|
73
|
+
|
61
74
|
# Create question and actual answer
|
62
75
|
question_text = words.join(' ')
|
63
76
|
correct_answer = hidden_words.join(' ')
|
64
|
-
|
77
|
+
|
65
78
|
# Generate incorrect options
|
66
79
|
other_messages = get_commit_messages(valid_commits) - [message]
|
67
80
|
other_messages_parts = []
|
68
|
-
|
81
|
+
|
69
82
|
# Get parts of other messages that have similar length
|
70
83
|
other_messages.each do |other_msg|
|
71
84
|
other_words = other_msg.split(/\s+/)
|
@@ -74,17 +87,17 @@ module GitGameShow
|
|
74
87
|
other_messages_parts << other_words[part_start...(part_start + hide_count)].join(' ')
|
75
88
|
end
|
76
89
|
end
|
77
|
-
|
90
|
+
|
78
91
|
# Only proceed if we have enough options
|
79
92
|
if other_messages_parts.size < 3
|
80
93
|
next
|
81
94
|
end
|
82
|
-
|
95
|
+
|
83
96
|
other_options = other_messages_parts.sample(3)
|
84
|
-
|
97
|
+
|
85
98
|
# Create options array with the correct answer and incorrect ones
|
86
99
|
all_options = ([correct_answer] + other_options).shuffle
|
87
|
-
|
100
|
+
|
88
101
|
# Format consistently with other mini-games
|
89
102
|
questions << {
|
90
103
|
question: "Complete this commit message:\n\n \"#{question_text}\"",
|
@@ -92,22 +105,22 @@ module GitGameShow
|
|
92
105
|
options: all_options,
|
93
106
|
correct_answer: correct_answer
|
94
107
|
}
|
95
|
-
|
108
|
+
|
96
109
|
valid_questions += 1
|
97
110
|
end
|
98
|
-
|
111
|
+
|
99
112
|
# If we couldn't generate enough questions, fall back to sample questions
|
100
113
|
if questions.size < self.class.questions_per_round
|
101
114
|
return generate_sample_questions
|
102
115
|
end
|
103
|
-
|
116
|
+
|
104
117
|
questions
|
105
118
|
end
|
106
|
-
|
119
|
+
|
107
120
|
# Generate sample questions when there aren't enough commits
|
108
121
|
def generate_sample_questions
|
109
122
|
questions = []
|
110
|
-
|
123
|
+
|
111
124
|
# Sample commit messages that are realistic
|
112
125
|
sample_messages = [
|
113
126
|
{
|
@@ -151,14 +164,14 @@ module GitGameShow
|
|
151
164
|
date: "Mar 15, 2025"
|
152
165
|
}
|
153
166
|
]
|
154
|
-
|
167
|
+
|
155
168
|
# Create a question for each sample
|
156
169
|
self.class.questions_per_round.times do |i|
|
157
170
|
sample = sample_messages[i % sample_messages.size]
|
158
|
-
|
171
|
+
|
159
172
|
# Create options array with the correct answer and incorrect ones
|
160
173
|
all_options = ([sample[:missing_part]] + sample[:wrong_options]).shuffle
|
161
|
-
|
174
|
+
|
162
175
|
# Format consistently with other mini-games
|
163
176
|
questions << {
|
164
177
|
question: "Complete this commit message:\n\n \"#{sample[:blank_text]}\"",
|
@@ -167,22 +180,22 @@ module GitGameShow
|
|
167
180
|
correct_answer: sample[:missing_part]
|
168
181
|
}
|
169
182
|
end
|
170
|
-
|
183
|
+
|
171
184
|
questions
|
172
185
|
end
|
173
|
-
|
186
|
+
|
174
187
|
def evaluate_answers(question, player_answers)
|
175
188
|
results = {}
|
176
|
-
|
189
|
+
|
177
190
|
player_answers.each do |player_name, answer_data|
|
178
191
|
player_answer = answer_data[:answer]
|
179
192
|
correct = player_answer == question[:correct_answer]
|
180
|
-
|
193
|
+
|
181
194
|
points = 0
|
182
|
-
|
195
|
+
|
183
196
|
if correct
|
184
197
|
points = 10 # Base points for correct answer
|
185
|
-
|
198
|
+
|
186
199
|
# Bonus points for fast answers, adjusted for 20-second time limit
|
187
200
|
time_taken = answer_data[:time_taken] || 20
|
188
201
|
if time_taken < 7 # Increased from 5 to 7 seconds
|
@@ -191,15 +204,15 @@ module GitGameShow
|
|
191
204
|
points += 3
|
192
205
|
end
|
193
206
|
end
|
194
|
-
|
207
|
+
|
195
208
|
results[player_name] = {
|
196
209
|
answer: player_answer,
|
197
210
|
correct: correct,
|
198
211
|
points: points
|
199
212
|
}
|
200
213
|
end
|
201
|
-
|
214
|
+
|
202
215
|
results
|
203
216
|
end
|
204
217
|
end
|
205
|
-
end
|
218
|
+
end
|
@@ -2,23 +2,38 @@ module GitGameShow
|
|
2
2
|
class DateOrderingQuiz < MiniGame
|
3
3
|
self.name = "Commit Timeline"
|
4
4
|
self.description = "Put these commits in chronological order! (20 seconds per question)"
|
5
|
+
self.example = <<~EXAMPLE
|
6
|
+
Put these commits in chronological order (oldest to newest):
|
7
|
+
|
8
|
+
Current order:
|
9
|
+
Add payment gateway integration (m2n3o4p)
|
10
|
+
Fix login page styling issues (e4f5g6h)
|
11
|
+
Update README with API documentation (q5r6s7t)
|
12
|
+
Implement password reset functionality (i7j8k9l)
|
13
|
+
|
14
|
+
Drag items to reorder:
|
15
|
+
\e[0;32;49m1. Create README.md (q5r6s7t)\e[0m
|
16
|
+
\e[0;32;49m2. Fix login page styling issues (e4f5g6h)\e[0m
|
17
|
+
\e[0;32;49m3. Implement password reset functionality (i7j8k9l)\e[0m
|
18
|
+
\e[0;32;49m4. Add payment gateway integration (m2n3o4p)\e[0m
|
19
|
+
EXAMPLE
|
5
20
|
self.questions_per_round = 5
|
6
|
-
|
21
|
+
|
7
22
|
# Custom timing for this mini-game
|
8
23
|
def self.question_timeout
|
9
24
|
20 # 20 seconds per question
|
10
25
|
end
|
11
|
-
|
26
|
+
|
12
27
|
def self.question_display_time
|
13
28
|
5 # 5 seconds between questions
|
14
29
|
end
|
15
|
-
|
30
|
+
|
16
31
|
def generate_questions(repo)
|
17
32
|
begin
|
18
33
|
# Get commits with valid dates
|
19
34
|
all_commits = get_all_commits(repo)
|
20
35
|
commits = all_commits.select { |commit| commit.date.is_a?(Time) || commit.date.respond_to?(:to_time) }
|
21
|
-
|
36
|
+
|
22
37
|
# If we don't have enough commits for the game, generate sample questions
|
23
38
|
if commits.size < 10
|
24
39
|
return generate_sample_questions
|
@@ -27,9 +42,9 @@ module GitGameShow
|
|
27
42
|
# If any errors occur, fall back to sample questions
|
28
43
|
return generate_sample_questions
|
29
44
|
end
|
30
|
-
|
45
|
+
|
31
46
|
questions = []
|
32
|
-
|
47
|
+
|
33
48
|
# Generate questions
|
34
49
|
self.class.questions_per_round.times do
|
35
50
|
begin
|
@@ -37,30 +52,30 @@ module GitGameShow
|
|
37
52
|
# to avoid ties in the ordering
|
38
53
|
pool = commits.dup
|
39
54
|
selected_commits = []
|
40
|
-
|
55
|
+
|
41
56
|
# Select 4 commits with different dates
|
42
57
|
while selected_commits.size < 4 && !pool.empty?
|
43
58
|
commit = pool.sample
|
44
59
|
pool.delete(commit)
|
45
|
-
|
60
|
+
|
46
61
|
# Check if date is unique among selected commits
|
47
62
|
date_duplicate = selected_commits.any? do |selected|
|
48
63
|
# Convert dates to Time objects for comparison
|
49
64
|
commit_time = commit.date.is_a?(Time) ? commit.date : commit.date.to_time
|
50
65
|
selected_time = selected.date.is_a?(Time) ? selected.date : selected.date.to_time
|
51
|
-
|
66
|
+
|
52
67
|
# Check if times are within 1 minute of each other (avoid duplicates)
|
53
68
|
(commit_time - selected_time).abs < 60
|
54
69
|
end
|
55
|
-
|
70
|
+
|
56
71
|
selected_commits << commit unless date_duplicate
|
57
72
|
end
|
58
|
-
|
73
|
+
|
59
74
|
# If we couldn't get 4 commits with different dates, try again with the full pool
|
60
75
|
if selected_commits.size < 4
|
61
76
|
selected_commits = commits.sample(4)
|
62
77
|
end
|
63
|
-
|
78
|
+
|
64
79
|
# Create the options using commit message and short SHA
|
65
80
|
options = selected_commits.map do |commit|
|
66
81
|
# Get first line of commit message, handle multi-line messages
|
@@ -69,23 +84,23 @@ module GitGameShow
|
|
69
84
|
message = message.length > 40 ? "#{message[0...37]}..." : message
|
70
85
|
"#{message} (#{commit.sha[0..6]})"
|
71
86
|
end
|
72
|
-
|
87
|
+
|
73
88
|
# Shuffle options for initial display
|
74
89
|
shuffled_options = options.shuffle
|
75
|
-
|
90
|
+
|
76
91
|
# Determine correct order by date
|
77
92
|
correct_order = selected_commits.sort_by(&:date).map do |commit|
|
78
93
|
message = commit.message.lines.first&.strip || "No message"
|
79
94
|
message = message.length > 40 ? "#{message[0...37]}..." : message
|
80
95
|
"#{message} (#{commit.sha[0..6]})"
|
81
96
|
end
|
82
|
-
|
97
|
+
|
83
98
|
# Get commit dates for displaying in results
|
84
99
|
commit_dates = selected_commits.map do |commit|
|
85
100
|
date_string = commit.date.strftime('%Y-%m-%d %H:%M:%S')
|
86
101
|
"#{commit.sha[0..6]}: #{date_string}"
|
87
102
|
end
|
88
|
-
|
103
|
+
|
89
104
|
# Store the question data
|
90
105
|
questions << {
|
91
106
|
question: "Put these commits in chronological order (oldest to newest):",
|
@@ -99,20 +114,20 @@ module GitGameShow
|
|
99
114
|
next
|
100
115
|
end
|
101
116
|
end
|
102
|
-
|
117
|
+
|
103
118
|
# If we couldn't generate enough questions, fill with sample questions
|
104
119
|
if questions.size < self.class.questions_per_round
|
105
120
|
sample_questions = generate_sample_questions
|
106
121
|
questions += sample_questions[0...(self.class.questions_per_round - questions.size)]
|
107
122
|
end
|
108
|
-
|
123
|
+
|
109
124
|
questions
|
110
125
|
end
|
111
|
-
|
126
|
+
|
112
127
|
# Generate sample questions when we don't have enough commits
|
113
128
|
def generate_sample_questions
|
114
129
|
questions = []
|
115
|
-
|
130
|
+
|
116
131
|
# Sample data sets with commit messages and dates
|
117
132
|
sample_data = [
|
118
133
|
[
|
@@ -146,57 +161,58 @@ module GitGameShow
|
|
146
161
|
{ message: "Fix responsive design issues", sha: "y1z2a3b", date: "2023-05-25 16:45:00" }
|
147
162
|
]
|
148
163
|
]
|
149
|
-
|
164
|
+
|
150
165
|
# Generate one question from each sample set
|
151
166
|
self.class.questions_per_round.times do |i|
|
152
167
|
# Use modulo to cycle through samples if we have fewer samples than questions
|
153
168
|
sample_set = sample_data[i % sample_data.size]
|
154
|
-
|
169
|
+
|
155
170
|
# Format the options
|
156
171
|
options = sample_set.map { |item| "#{item[:message]} (#{item[:sha]})" }
|
157
|
-
|
172
|
+
|
158
173
|
# Determine correct order (they're already in order in our samples)
|
159
174
|
correct_order = options.dup
|
160
|
-
|
175
|
+
|
161
176
|
# Shuffle options for initial display
|
162
177
|
shuffled_options = options.shuffle
|
163
|
-
|
178
|
+
|
164
179
|
# Format dates for feedback
|
165
180
|
date_strings = sample_set.map { |item| "#{item[:sha]}: #{item[:date]}" }
|
166
|
-
|
181
|
+
|
167
182
|
questions << {
|
168
183
|
question: "Put these commits in chronological order (oldest to newest):",
|
169
184
|
options: shuffled_options,
|
170
|
-
correct_answer: correct_order,
|
185
|
+
correct_answer: correct_order,
|
171
186
|
question_type: 'ordering',
|
172
187
|
commit_dates: date_strings.join("\n")
|
173
188
|
}
|
174
189
|
end
|
175
|
-
|
190
|
+
|
176
191
|
questions
|
177
192
|
end
|
178
|
-
|
193
|
+
|
194
|
+
|
179
195
|
def evaluate_answers(question, player_answers)
|
180
196
|
results = {}
|
181
|
-
|
197
|
+
|
182
198
|
# Safety check for nil or empty responses
|
183
199
|
return results if player_answers.nil? || player_answers.empty?
|
184
200
|
return results unless question && question[:correct_answer]
|
185
|
-
|
201
|
+
|
186
202
|
# Get total number of items in the correct answer
|
187
203
|
total_items = question[:correct_answer].size
|
188
|
-
|
204
|
+
|
189
205
|
player_answers.each do |player_name, answer_data|
|
190
206
|
# Skip nil entries
|
191
207
|
next unless player_name && answer_data
|
192
|
-
|
208
|
+
|
193
209
|
# Extract player's answer with defensive checks
|
194
210
|
player_answer = answer_data[:answer]
|
195
211
|
time_taken = answer_data[:time_taken] || 20
|
196
|
-
|
212
|
+
|
197
213
|
# Initialize points
|
198
214
|
points = 0
|
199
|
-
|
215
|
+
|
200
216
|
# New scoring system: checks positions relative to each other item
|
201
217
|
if player_answer && !player_answer.empty?
|
202
218
|
# Create a mapping from item to its position in player's answer
|
@@ -204,26 +220,26 @@ module GitGameShow
|
|
204
220
|
player_answer.each_with_index do |item, index|
|
205
221
|
item_positions[item] = index if item # Skip nil items
|
206
222
|
end
|
207
|
-
|
223
|
+
|
208
224
|
# Create mapping from item to correct position
|
209
225
|
correct_positions = {}
|
210
226
|
question[:correct_answer].each_with_index do |item, index|
|
211
227
|
correct_positions[item] = index if item # Skip nil items
|
212
228
|
end
|
213
|
-
|
229
|
+
|
214
230
|
# For each item, calculate points based on relative positions
|
215
231
|
question[:correct_answer].each_with_index do |item, correct_index|
|
216
232
|
# Skip if the item isn't in the player's answer
|
217
233
|
next unless item && item_positions.key?(item)
|
218
|
-
|
234
|
+
|
219
235
|
player_index = item_positions[item]
|
220
|
-
|
236
|
+
|
221
237
|
# Check position relative to other items
|
222
238
|
question[:correct_answer].each_with_index do |other_item, other_correct_index|
|
223
239
|
next if !other_item || item == other_item || !item_positions.key?(other_item)
|
224
|
-
|
240
|
+
|
225
241
|
other_player_index = item_positions[other_item]
|
226
|
-
|
242
|
+
|
227
243
|
# If this item should be before the other item
|
228
244
|
if correct_index < other_correct_index
|
229
245
|
points += 1 if player_index < other_player_index
|
@@ -234,12 +250,12 @@ module GitGameShow
|
|
234
250
|
end
|
235
251
|
end
|
236
252
|
end
|
237
|
-
|
253
|
+
|
238
254
|
# Bonus points for perfect answer
|
239
255
|
perfect_score = total_items * (total_items - 1)
|
240
256
|
perfect_score = 1 if perfect_score <= 0 # Prevent division by zero
|
241
257
|
fully_correct = points == perfect_score
|
242
|
-
|
258
|
+
|
243
259
|
if fully_correct
|
244
260
|
# Additional time-based bonus (faster answers get more points)
|
245
261
|
if time_taken < 8 # Really fast (under 8 seconds)
|
@@ -248,7 +264,7 @@ module GitGameShow
|
|
248
264
|
points += 2
|
249
265
|
end
|
250
266
|
end
|
251
|
-
|
267
|
+
|
252
268
|
# Create detailed feedback
|
253
269
|
max_possible = total_items * (total_items - 1)
|
254
270
|
max_possible = 1 if max_possible <= 0 # Prevent division by zero
|
@@ -260,7 +276,7 @@ module GitGameShow
|
|
260
276
|
feedback += "\n • #{date_line}"
|
261
277
|
end
|
262
278
|
end
|
263
|
-
|
279
|
+
|
264
280
|
# Ensure we return all required fields
|
265
281
|
results[player_name] = {
|
266
282
|
answer: player_answer || [],
|
@@ -269,7 +285,7 @@ module GitGameShow
|
|
269
285
|
partial_score: feedback || ""
|
270
286
|
}
|
271
287
|
end
|
272
|
-
|
288
|
+
|
273
289
|
# Return a default result if somehow we ended up with empty results
|
274
290
|
if results.empty? && !player_answers.empty?
|
275
291
|
player_name = player_answers.keys.first
|
@@ -280,8 +296,8 @@ module GitGameShow
|
|
280
296
|
partial_score: "Error calculating score"
|
281
297
|
}
|
282
298
|
end
|
283
|
-
|
299
|
+
|
284
300
|
results
|
285
301
|
end
|
286
302
|
end
|
287
|
-
end
|
303
|
+
end
|
data/mini_games/file_quiz.rb
CHANGED
@@ -5,6 +5,19 @@ module GitGameShow
|
|
5
5
|
class FileQuiz < MiniGame
|
6
6
|
self.name = "File Quiz"
|
7
7
|
self.description = "Match the commit message to the right changed file!"
|
8
|
+
self.example = <<~EXAMPLE
|
9
|
+
Which file was most likely changed in this commit?
|
10
|
+
|
11
|
+
"Update documentation with new API endpoints"
|
12
|
+
|
13
|
+
abc123f (Mar 15, 2025)
|
14
|
+
|
15
|
+
Choose your answer:
|
16
|
+
src/main.js
|
17
|
+
css/styles.css
|
18
|
+
\e[0;32;49m> README.md\e[0m
|
19
|
+
lib/utils.js
|
20
|
+
EXAMPLE
|
8
21
|
self.questions_per_round = 5
|
9
22
|
|
10
23
|
# Custom timing for this mini-game (same as AuthorQuiz)
|
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.
|
4
|
+
version: 0.2.0
|
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-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -213,6 +213,8 @@ files:
|
|
213
213
|
- lib/git_game_show/updater.rb
|
214
214
|
- lib/git_game_show/version.rb
|
215
215
|
- mini_games/author_quiz.rb
|
216
|
+
- mini_games/blame_game.rb
|
217
|
+
- mini_games/branch_detective.rb
|
216
218
|
- mini_games/commit_message_completion.rb
|
217
219
|
- mini_games/date_ordering_quiz.rb
|
218
220
|
- mini_games/file_quiz.rb
|