git_game_show 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +110 -0
- data/bin/git-game-show +5 -0
- data/lib/git_game_show/cli.rb +537 -0
- data/lib/git_game_show/game_server.rb +1224 -0
- data/lib/git_game_show/mini_game.rb +57 -0
- data/lib/git_game_show/player_client.rb +1145 -0
- data/lib/git_game_show/version.rb +4 -0
- data/lib/git_game_show.rb +49 -0
- data/mini_games/author_quiz.rb +142 -0
- data/mini_games/commit_message_completion.rb +205 -0
- data/mini_games/commit_message_quiz.rb +589 -0
- data/mini_games/date_ordering_quiz.rb +230 -0
- metadata +245 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'git'
|
2
|
+
require 'colorize'
|
3
|
+
require 'tty-prompt'
|
4
|
+
require 'tty-table'
|
5
|
+
require 'tty-cursor'
|
6
|
+
require 'json'
|
7
|
+
require 'eventmachine'
|
8
|
+
require 'websocket-eventmachine-server'
|
9
|
+
require 'websocket-client-simple'
|
10
|
+
require 'thor'
|
11
|
+
require 'uri'
|
12
|
+
require 'clipboard'
|
13
|
+
require 'readline'
|
14
|
+
require 'timeout'
|
15
|
+
|
16
|
+
# Define module and constants first before loading any other files
|
17
|
+
module GitGameShow
|
18
|
+
# VERSION is defined in version.rb
|
19
|
+
|
20
|
+
# Default configuration
|
21
|
+
DEFAULT_CONFIG = {
|
22
|
+
port: 3030,
|
23
|
+
rounds: 3,
|
24
|
+
question_timeout: 30, # seconds
|
25
|
+
question_display_time: 5, # seconds to show results before next question
|
26
|
+
transition_delay: 5 # seconds between rounds
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
# Message types for WebSocket communication
|
30
|
+
module MessageType
|
31
|
+
JOIN_REQUEST = 'join_request'
|
32
|
+
JOIN_RESPONSE = 'join_response'
|
33
|
+
GAME_START = 'game_start'
|
34
|
+
QUESTION = 'question'
|
35
|
+
ANSWER = 'answer'
|
36
|
+
ANSWER_FEEDBACK = 'answer_feedback' # New message type for immediate feedback
|
37
|
+
ROUND_RESULT = 'round_result'
|
38
|
+
SCOREBOARD = 'scoreboard'
|
39
|
+
GAME_END = 'game_end'
|
40
|
+
GAME_RESET = 'game_reset' # New message type for resetting the game
|
41
|
+
CHAT = 'chat'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Load all files in the git_game_show directory
|
46
|
+
Dir[File.join(__dir__, 'git_game_show', '*.rb')].sort.each { |file| require file }
|
47
|
+
|
48
|
+
# Load all mini-games
|
49
|
+
Dir[File.join(__dir__, '..', 'mini_games', '*.rb')].sort.each { |file| require file }
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module GitGameShow
|
2
|
+
class AuthorQuiz < MiniGame
|
3
|
+
self.name = "Author Quiz"
|
4
|
+
self.description = "Guess which team member made each commit!"
|
5
|
+
self.questions_per_round = 5
|
6
|
+
|
7
|
+
# Custom timing for this mini-game (overrides default config)
|
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
|
+
# For testing, manually add some questions if we can't get meaningful ones from repo
|
18
|
+
questions = []
|
19
|
+
|
20
|
+
begin
|
21
|
+
# Get commits from the past year
|
22
|
+
one_year_ago = Time.now - (365 * 24 * 60 * 60) # One year in seconds
|
23
|
+
|
24
|
+
# Get all commits
|
25
|
+
all_commits = get_all_commits(repo)
|
26
|
+
|
27
|
+
# Filter commits from the past year, if any
|
28
|
+
commits_from_past_year = all_commits.select do |commit|
|
29
|
+
# Be safe with date parsing
|
30
|
+
begin
|
31
|
+
commit_time = commit.date.is_a?(Time) ? commit.date : Time.parse(commit.date.to_s)
|
32
|
+
commit_time > one_year_ago
|
33
|
+
rescue
|
34
|
+
false # If we can't parse the date, exclude it
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# If we don't have enough commits from the past year, use all commits
|
39
|
+
commits = commits_from_past_year.size >= 10 ? commits_from_past_year : all_commits
|
40
|
+
|
41
|
+
authors = get_commit_authors(commits)
|
42
|
+
|
43
|
+
# We need at least 2 authors for this game to be meaningful
|
44
|
+
if authors.size < 2
|
45
|
+
return generate_sample_questions
|
46
|
+
end
|
47
|
+
|
48
|
+
# Shuffle all commits to ensure randomness, then select commits for questions
|
49
|
+
selected_commits = commits.shuffle.sample(self.class.questions_per_round)
|
50
|
+
|
51
|
+
selected_commits.each do |commit|
|
52
|
+
# Get the real author
|
53
|
+
correct_author = commit.author.name
|
54
|
+
|
55
|
+
# Get incorrect options (other authors)
|
56
|
+
incorrect_authors = shuffled_excluding(authors, correct_author).take(3)
|
57
|
+
|
58
|
+
# Create options array with the correct answer and incorrect ones
|
59
|
+
all_options = ([correct_author] + incorrect_authors).shuffle
|
60
|
+
|
61
|
+
# Extract the commit message, but handle multi-line messages gracefully
|
62
|
+
message_lines = commit.message.lines.reject(&:empty?)
|
63
|
+
message_preview = message_lines.first&.strip || "No message"
|
64
|
+
|
65
|
+
# For longer messages, add an indication that there's more
|
66
|
+
if message_lines.size > 1
|
67
|
+
message_preview += "..."
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create a more compact question format to avoid overflows
|
71
|
+
# Add proper indentation to the commit message with spaces
|
72
|
+
questions << {
|
73
|
+
question: "Who authored this commit?\n\n \"#{message_preview}\"",
|
74
|
+
commit_info: "#{commit.sha[0..7]} (#{commit.date.strftime('%b %d, %Y')})",
|
75
|
+
options: all_options,
|
76
|
+
correct_answer: correct_author
|
77
|
+
}
|
78
|
+
end
|
79
|
+
rescue => e
|
80
|
+
# Silently fail and use sample questions instead
|
81
|
+
return generate_sample_questions
|
82
|
+
end
|
83
|
+
|
84
|
+
# If we couldn't generate enough questions, add sample ones
|
85
|
+
if questions.empty?
|
86
|
+
return generate_sample_questions
|
87
|
+
end
|
88
|
+
|
89
|
+
questions
|
90
|
+
end
|
91
|
+
|
92
|
+
def generate_sample_questions
|
93
|
+
# Create sample questions in case the repo doesn't have enough data
|
94
|
+
sample_authors = ["Alice", "Bob", "Charlie", "David", "Emma"]
|
95
|
+
|
96
|
+
questions = []
|
97
|
+
|
98
|
+
5.times do |i|
|
99
|
+
correct_author = sample_authors.sample
|
100
|
+
incorrect_authors = sample_authors.reject { |a| a == correct_author }.sample(3)
|
101
|
+
|
102
|
+
all_options = ([correct_author] + incorrect_authors).shuffle
|
103
|
+
|
104
|
+
questions << {
|
105
|
+
question: "Who authored this commit?\n\n \"Sample commit message ##{i+1}\"",
|
106
|
+
commit_info: "abc123#{i} (Jan #{i+1}, 2025)",
|
107
|
+
options: all_options,
|
108
|
+
correct_answer: correct_author
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
questions
|
113
|
+
end
|
114
|
+
|
115
|
+
def evaluate_answers(question, player_answers)
|
116
|
+
results = {}
|
117
|
+
|
118
|
+
player_answers.each do |player_name, answer_data|
|
119
|
+
answered = answer_data[:answered] || false
|
120
|
+
player_answer = answer_data[:answer]
|
121
|
+
correct = player_answer == question[:correct_answer]
|
122
|
+
|
123
|
+
points = correct ? 10 : 0
|
124
|
+
|
125
|
+
# Bonus points for fast answers (if correct)
|
126
|
+
if correct && answer_data[:time_taken] < 5
|
127
|
+
points += 5
|
128
|
+
elsif correct && answer_data[:time_taken] < 10
|
129
|
+
points += 3
|
130
|
+
end
|
131
|
+
|
132
|
+
results[player_name] = {
|
133
|
+
answer: player_answer,
|
134
|
+
correct: correct,
|
135
|
+
points: points
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
results
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
module GitGameShow
|
2
|
+
class CommitMessageCompletion < MiniGame
|
3
|
+
self.name = "Complete the Commit"
|
4
|
+
self.description = "Complete the missing part of these commit messages! (20 seconds per question)"
|
5
|
+
self.questions_per_round = 5
|
6
|
+
|
7
|
+
# Custom timing for this mini-game (20 seconds instead of 15)
|
8
|
+
def self.question_timeout
|
9
|
+
20 # 20 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
|
+
commits = get_all_commits(repo)
|
19
|
+
|
20
|
+
# Filter commits with message length > 10 characters
|
21
|
+
valid_commits = commits.select { |commit| commit.message.strip.length > 10 }
|
22
|
+
|
23
|
+
# Fall back to sample questions if not enough valid commits
|
24
|
+
if valid_commits.size < self.class.questions_per_round
|
25
|
+
return generate_sample_questions
|
26
|
+
end
|
27
|
+
rescue => e
|
28
|
+
# If there's any error, fall back to sample questions
|
29
|
+
return generate_sample_questions
|
30
|
+
end
|
31
|
+
|
32
|
+
questions = []
|
33
|
+
valid_questions = 0
|
34
|
+
attempts = 0
|
35
|
+
max_attempts = 100 # Prevent infinite loops
|
36
|
+
|
37
|
+
# Keep trying until we have exactly 5 questions or reach max attempts
|
38
|
+
while valid_questions < self.class.questions_per_round && attempts < max_attempts
|
39
|
+
attempts += 1
|
40
|
+
|
41
|
+
# Get a random commit
|
42
|
+
commit = valid_commits.sample
|
43
|
+
message = commit.message.strip
|
44
|
+
|
45
|
+
# Split message into beginning and end parts
|
46
|
+
words = message.split(/\s+/)
|
47
|
+
|
48
|
+
# Skip if too few words
|
49
|
+
if words.size < 4
|
50
|
+
next
|
51
|
+
end
|
52
|
+
|
53
|
+
# Determine how much to hide (1/3 to 1/2 of words)
|
54
|
+
hide_count = [words.size / 3, 2].max
|
55
|
+
hide_start = rand(0..(words.size - hide_count))
|
56
|
+
hidden_words = words[hide_start...(hide_start + hide_count)]
|
57
|
+
|
58
|
+
# Replace hidden words with blanks
|
59
|
+
words[hide_start...(hide_start + hide_count)] = ['________'] * hide_count
|
60
|
+
|
61
|
+
# Create question and actual answer
|
62
|
+
question_text = words.join(' ')
|
63
|
+
correct_answer = hidden_words.join(' ')
|
64
|
+
|
65
|
+
# Generate incorrect options
|
66
|
+
other_messages = get_commit_messages(valid_commits) - [message]
|
67
|
+
other_messages_parts = []
|
68
|
+
|
69
|
+
# Get parts of other messages that have similar length
|
70
|
+
other_messages.each do |other_msg|
|
71
|
+
other_words = other_msg.split(/\s+/)
|
72
|
+
if other_words.size >= hide_count
|
73
|
+
part_start = rand(0..(other_words.size - hide_count))
|
74
|
+
other_messages_parts << other_words[part_start...(part_start + hide_count)].join(' ')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Only proceed if we have enough options
|
79
|
+
if other_messages_parts.size < 3
|
80
|
+
next
|
81
|
+
end
|
82
|
+
|
83
|
+
other_options = other_messages_parts.sample(3)
|
84
|
+
|
85
|
+
# Create options array with the correct answer and incorrect ones
|
86
|
+
all_options = ([correct_answer] + other_options).shuffle
|
87
|
+
|
88
|
+
# Format consistently with other mini-games
|
89
|
+
questions << {
|
90
|
+
question: "Complete this commit message:\n\n \"#{question_text}\"",
|
91
|
+
commit_info: "#{commit.sha[0..7]} (#{commit.date.strftime('%b %d, %Y')})",
|
92
|
+
options: all_options,
|
93
|
+
correct_answer: correct_answer
|
94
|
+
}
|
95
|
+
|
96
|
+
valid_questions += 1
|
97
|
+
end
|
98
|
+
|
99
|
+
# If we couldn't generate enough questions, fall back to sample questions
|
100
|
+
if questions.size < self.class.questions_per_round
|
101
|
+
return generate_sample_questions
|
102
|
+
end
|
103
|
+
|
104
|
+
questions
|
105
|
+
end
|
106
|
+
|
107
|
+
# Generate sample questions when there aren't enough commits
|
108
|
+
def generate_sample_questions
|
109
|
+
questions = []
|
110
|
+
|
111
|
+
# Sample commit messages that are realistic
|
112
|
+
sample_messages = [
|
113
|
+
{
|
114
|
+
full_message: "Add user authentication with OAuth2 support",
|
115
|
+
blank_text: "Add user __________ with OAuth2 support",
|
116
|
+
missing_part: "authentication",
|
117
|
+
wrong_options: ["registration", "profile", "settings"],
|
118
|
+
sha: "f8c7b3e",
|
119
|
+
date: "Mar 10, 2025"
|
120
|
+
},
|
121
|
+
{
|
122
|
+
full_message: "Fix memory leak in the background worker process",
|
123
|
+
blank_text: "Fix memory __________ in the background worker",
|
124
|
+
missing_part: "leak",
|
125
|
+
wrong_options: ["usage", "allocation", "error"],
|
126
|
+
sha: "2d9a45c",
|
127
|
+
date: "Feb 25, 2025"
|
128
|
+
},
|
129
|
+
{
|
130
|
+
full_message: "Update dependencies to latest stable versions",
|
131
|
+
blank_text: "Update __________ to latest stable versions",
|
132
|
+
missing_part: "dependencies",
|
133
|
+
wrong_options: ["documentation", "configurations", "references"],
|
134
|
+
sha: "7b3e9d1",
|
135
|
+
date: "Mar 5, 2025"
|
136
|
+
},
|
137
|
+
{
|
138
|
+
full_message: "Improve error handling in API response layer",
|
139
|
+
blank_text: "Improve error __________ in API response layer",
|
140
|
+
missing_part: "handling",
|
141
|
+
wrong_options: ["messages", "logging", "codes"],
|
142
|
+
sha: "c4e91a2",
|
143
|
+
date: "Feb 28, 2025"
|
144
|
+
},
|
145
|
+
{
|
146
|
+
full_message: "Add comprehensive test coverage for payment module",
|
147
|
+
blank_text: "Add comprehensive __________ coverage for payment module",
|
148
|
+
missing_part: "test",
|
149
|
+
wrong_options: ["code", "feature", "security"],
|
150
|
+
sha: "9f5d7e3",
|
151
|
+
date: "Mar 15, 2025"
|
152
|
+
}
|
153
|
+
]
|
154
|
+
|
155
|
+
# Create a question for each sample
|
156
|
+
self.class.questions_per_round.times do |i|
|
157
|
+
sample = sample_messages[i % sample_messages.size]
|
158
|
+
|
159
|
+
# Create options array with the correct answer and incorrect ones
|
160
|
+
all_options = ([sample[:missing_part]] + sample[:wrong_options]).shuffle
|
161
|
+
|
162
|
+
# Format consistently with other mini-games
|
163
|
+
questions << {
|
164
|
+
question: "Complete this commit message:\n\n \"#{sample[:blank_text]}\"",
|
165
|
+
commit_info: "#{sample[:sha]} (#{sample[:date]})",
|
166
|
+
options: all_options,
|
167
|
+
correct_answer: sample[:missing_part]
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
questions
|
172
|
+
end
|
173
|
+
|
174
|
+
def evaluate_answers(question, player_answers)
|
175
|
+
results = {}
|
176
|
+
|
177
|
+
player_answers.each do |player_name, answer_data|
|
178
|
+
player_answer = answer_data[:answer]
|
179
|
+
correct = player_answer == question[:correct_answer]
|
180
|
+
|
181
|
+
points = 0
|
182
|
+
|
183
|
+
if correct
|
184
|
+
points = 10 # Base points for correct answer
|
185
|
+
|
186
|
+
# Bonus points for fast answers, adjusted for 20-second time limit
|
187
|
+
time_taken = answer_data[:time_taken] || 20
|
188
|
+
if time_taken < 7 # Increased from 5 to 7 seconds
|
189
|
+
points += 5
|
190
|
+
elsif time_taken < 13 # Increased from 10 to 13 seconds
|
191
|
+
points += 3
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
results[player_name] = {
|
196
|
+
answer: player_answer,
|
197
|
+
correct: correct,
|
198
|
+
points: points
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
results
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|