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,1224 @@
|
|
1
|
+
module GitGameShow
|
2
|
+
class GameServer
|
3
|
+
attr_reader :port, :password, :rounds, :repo, :players, :current_round, :game_state
|
4
|
+
|
5
|
+
def initialize(port:, password:, rounds:, repo:)
|
6
|
+
@port = port
|
7
|
+
@password = password
|
8
|
+
@rounds = rounds
|
9
|
+
@repo = repo
|
10
|
+
@players = {} # WebSocket connections by player name
|
11
|
+
@scores = {} # Player scores
|
12
|
+
@current_round = 0
|
13
|
+
@game_state = :lobby # :lobby, :playing, :ended
|
14
|
+
@mini_games = load_mini_games
|
15
|
+
@current_mini_game = nil
|
16
|
+
@round_questions = []
|
17
|
+
@current_question_index = 0
|
18
|
+
@question_start_time = nil
|
19
|
+
@player_answers = {}
|
20
|
+
@used_mini_games = [] # Track which mini-games have been used
|
21
|
+
@available_mini_games = [] # Mini-games still available in the current cycle
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
EM.run do
|
26
|
+
setup_server
|
27
|
+
|
28
|
+
# Setup console commands for the host
|
29
|
+
setup_console_commands
|
30
|
+
|
31
|
+
puts "Server running at ws://0.0.0.0:#{port}".colorize(:green)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def start_with_ui(join_link = nil)
|
36
|
+
# Display UI
|
37
|
+
@show_host_ui = true
|
38
|
+
@join_link = join_link
|
39
|
+
@message_log = []
|
40
|
+
@players = {}
|
41
|
+
@cursor = TTY::Cursor
|
42
|
+
|
43
|
+
# Get terminal dimensions
|
44
|
+
@terminal_width = `tput cols`.to_i rescue 80
|
45
|
+
@terminal_height = `tput lines`.to_i rescue 24
|
46
|
+
|
47
|
+
# Calculate layout
|
48
|
+
@main_width = (@terminal_width * 0.7).to_i
|
49
|
+
@sidebar_width = @terminal_width - @main_width - 3 # 3 for border
|
50
|
+
|
51
|
+
# The fixed line for command input (near bottom of screen)
|
52
|
+
@command_line = @terminal_height - 3
|
53
|
+
|
54
|
+
# Clear screen and hide cursor
|
55
|
+
print @cursor.clear_screen
|
56
|
+
print @cursor.hide
|
57
|
+
|
58
|
+
# Draw initial UI
|
59
|
+
draw_ui_frame
|
60
|
+
draw_welcome_banner
|
61
|
+
draw_join_link
|
62
|
+
draw_sidebar
|
63
|
+
draw_command_prompt
|
64
|
+
|
65
|
+
# Set up buffer for events
|
66
|
+
@event_buffer = []
|
67
|
+
|
68
|
+
# Start the server
|
69
|
+
EM.run do
|
70
|
+
setup_server
|
71
|
+
setup_fixed_console_commands
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def draw_ui_frame
|
76
|
+
# Clear screen
|
77
|
+
print @cursor.clear_screen
|
78
|
+
|
79
|
+
# Draw horizontal divider line between main area and command area
|
80
|
+
print @cursor.move_to(0, @command_line - 1)
|
81
|
+
print "=" * @terminal_width
|
82
|
+
|
83
|
+
# Draw vertical divider line between main area and sidebar
|
84
|
+
(0...@command_line-1).each do |line|
|
85
|
+
print @cursor.move_to(@main_width, line)
|
86
|
+
print "│"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def draw_welcome_banner
|
91
|
+
# Position cursor at top left
|
92
|
+
lines = [
|
93
|
+
" ██████╗ ".colorize(:red) + " ██████╗ ".colorize(:green) + " █████╗".colorize(:blue),
|
94
|
+
"██╔════╝ ".colorize(:red) + " ██╔════╝ ".colorize(:green) + " ██╔═══╝".colorize(:blue),
|
95
|
+
"██║ ███╗".colorize(:red) + " ██║ ███╗".colorize(:green) + " ███████╗".colorize(:blue),
|
96
|
+
"██║ ██║".colorize(:red) + " ██║ ██║".colorize(:green) + " ╚════██║".colorize(:blue),
|
97
|
+
"╚██████╔╝".colorize(:red) + " ╚██████╔╝".colorize(:green) + " ██████╔╝".colorize(:blue),
|
98
|
+
" ╚═════╝ ".colorize(:red) + " ╚═════╝ ".colorize(:green) + " ╚═════╝ ".colorize(:blue),
|
99
|
+
]
|
100
|
+
|
101
|
+
start_y = 1
|
102
|
+
lines.each_with_index do |line, i|
|
103
|
+
print @cursor.move_to((@main_width - 28) / 2, start_y + i)
|
104
|
+
print line
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def draw_join_link
|
109
|
+
# Copy the join link to clipboard
|
110
|
+
Clipboard.copy(@join_link)
|
111
|
+
|
112
|
+
link_box_width = [@join_link.length + 6, @main_width - 10].min
|
113
|
+
start_x = (@main_width - link_box_width) / 2
|
114
|
+
start_y = 13
|
115
|
+
|
116
|
+
print @cursor.move_to(start_x, start_y)
|
117
|
+
print "┌" + "─" * (link_box_width - 2) + "┐"
|
118
|
+
|
119
|
+
print @cursor.move_to(start_x, start_y + 1)
|
120
|
+
print "│" + " JOIN LINK (Copied to Clipboard) ".center(link_box_width - 2).colorize(:green) + "│"
|
121
|
+
|
122
|
+
print @cursor.move_to(start_x, start_y + 2)
|
123
|
+
print "│" + @join_link.center(link_box_width - 2).colorize(:yellow) + "│"
|
124
|
+
|
125
|
+
print @cursor.move_to(start_x, start_y + 3)
|
126
|
+
print "└" + "─" * (link_box_width - 2) + "┘"
|
127
|
+
|
128
|
+
# Also log that the link was copied
|
129
|
+
log_message("Join link copied to clipboard", :green)
|
130
|
+
end
|
131
|
+
|
132
|
+
def draw_sidebar
|
133
|
+
# Draw sidebar header
|
134
|
+
print @cursor.move_to(@main_width + 2, 1)
|
135
|
+
print "PLAYERS".colorize(:cyan)
|
136
|
+
|
137
|
+
print @cursor.move_to(@main_width + 2, 2)
|
138
|
+
print "═" * (@sidebar_width - 2)
|
139
|
+
|
140
|
+
update_player_list
|
141
|
+
end
|
142
|
+
|
143
|
+
def update_player_list
|
144
|
+
# Clear player area
|
145
|
+
(3..(@command_line-3)).each do |line|
|
146
|
+
print @cursor.move_to(@main_width + 2, line)
|
147
|
+
print " " * (@sidebar_width - 2)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Show player count
|
151
|
+
print @cursor.move_to(@main_width + 2, 3)
|
152
|
+
print "Total: #{@players.size} player(s)".colorize(:yellow)
|
153
|
+
|
154
|
+
# Calculate available space for the player list
|
155
|
+
max_visible_players = @command_line - 8 # Allow space for headers, counts and scrolling indicators
|
156
|
+
|
157
|
+
# List players with scrolling if needed
|
158
|
+
if @players.empty?
|
159
|
+
print @cursor.move_to(@main_width + 2, 5)
|
160
|
+
print "Waiting for players...".colorize(:light_black)
|
161
|
+
else
|
162
|
+
# Show scrolling indicator if needed
|
163
|
+
if @players.size > max_visible_players
|
164
|
+
print @cursor.move_to(@main_width + 2, 4)
|
165
|
+
print "Showing #{max_visible_players} of #{@players.size}:".colorize(:light_yellow)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Determine which players to display (for now, show first N players)
|
169
|
+
visible_players = @players.keys.take(max_visible_players)
|
170
|
+
|
171
|
+
# Display visible players
|
172
|
+
visible_players.each_with_index do |name, index|
|
173
|
+
print @cursor.move_to(@main_width + 2, 5 + index)
|
174
|
+
# Truncate long names
|
175
|
+
truncated_name = name.length > (@sidebar_width - 6) ?
|
176
|
+
"#{name[0...(@sidebar_width-9)]}..." :
|
177
|
+
name
|
178
|
+
|
179
|
+
if index < 9
|
180
|
+
print "#{index + 1}. #{truncated_name}".colorize(:light_blue)
|
181
|
+
else
|
182
|
+
print "#{index + 1}. #{truncated_name}".colorize(:light_blue)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# If there are more players than can be shown, add an indicator
|
187
|
+
if @players.size > max_visible_players
|
188
|
+
print @cursor.move_to(@main_width + 2, 5 + max_visible_players)
|
189
|
+
print "... and #{@players.size - max_visible_players} more".colorize(:light_black)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Return cursor to command prompt
|
194
|
+
draw_command_prompt
|
195
|
+
end
|
196
|
+
|
197
|
+
def log_message(message, color = :white)
|
198
|
+
# Add message to log
|
199
|
+
@message_log << {text: message, color: color}
|
200
|
+
|
201
|
+
# Keep only last few messages
|
202
|
+
@message_log = @message_log.last(15) if @message_log.size > 15
|
203
|
+
|
204
|
+
# Redraw message area
|
205
|
+
draw_message_area
|
206
|
+
|
207
|
+
# Return cursor to command prompt
|
208
|
+
draw_command_prompt
|
209
|
+
end
|
210
|
+
|
211
|
+
def draw_message_area
|
212
|
+
# Calculate message area dimensions
|
213
|
+
message_area_start = 18
|
214
|
+
message_area_height = @command_line - message_area_start - 2
|
215
|
+
|
216
|
+
# Clear message area
|
217
|
+
(message_area_start..(@command_line-2)).each do |line|
|
218
|
+
print @cursor.move_to(1, line)
|
219
|
+
print " " * (@main_width - 2)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Draw most recent messages
|
223
|
+
display_messages = @message_log.last(message_area_height)
|
224
|
+
display_messages.each_with_index do |msg, index|
|
225
|
+
print @cursor.move_to(1, message_area_start + index)
|
226
|
+
# Truncate message to fit within main width to prevent overflow
|
227
|
+
truncated_text = msg[:text][0...(@main_width - 3)]
|
228
|
+
print truncated_text.colorize(msg[:color])
|
229
|
+
end
|
230
|
+
|
231
|
+
# No need to call draw_command_prompt here as it's already called by log_message
|
232
|
+
end
|
233
|
+
|
234
|
+
def draw_command_prompt
|
235
|
+
# Clear command line and two lines below to prevent commit info bleeding
|
236
|
+
print @cursor.move_to(0, @command_line)
|
237
|
+
print " " * @terminal_width
|
238
|
+
print @cursor.move_to(0, @command_line + 1)
|
239
|
+
print " " * @terminal_width
|
240
|
+
print @cursor.move_to(0, @command_line + 2)
|
241
|
+
print " " * @terminal_width
|
242
|
+
|
243
|
+
# Draw command prompt
|
244
|
+
print @cursor.move_to(0, @command_line)
|
245
|
+
print "Command> ".colorize(:green)
|
246
|
+
|
247
|
+
# Position cursor after prompt
|
248
|
+
print @cursor.move_to(9, @command_line)
|
249
|
+
print @cursor.show
|
250
|
+
end
|
251
|
+
|
252
|
+
def display_welcome_banner
|
253
|
+
puts <<-BANNER.colorize(:green)
|
254
|
+
██████╗ ██╗████████╗ ██████╗ █████╗ ███╗ ███╗███████╗
|
255
|
+
██╔════╝ ██║╚══██╔══╝ ██╔════╝ ██╔══██╗████╗ ████║██╔════╝
|
256
|
+
██║ ███╗██║ ██║ ██║ ███╗███████║██╔████╔██║█████╗
|
257
|
+
██║ ██║██║ ██║ ██║ ██║██╔══██║██║╚██╔╝██║██╔══╝
|
258
|
+
╚██████╔╝██║ ██║ ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗
|
259
|
+
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
|
260
|
+
BANNER
|
261
|
+
puts "\n SERVER STARTED - PORT: #{port}\n".colorize(:light_blue)
|
262
|
+
end
|
263
|
+
|
264
|
+
private
|
265
|
+
|
266
|
+
def setup_server
|
267
|
+
WebSocket::EventMachine::Server.start(host: '0.0.0.0', port: port) do |ws|
|
268
|
+
ws.onopen do
|
269
|
+
# Connection is logged when a player successfully joins
|
270
|
+
end
|
271
|
+
|
272
|
+
ws.onmessage do |msg|
|
273
|
+
handle_message(ws, msg)
|
274
|
+
end
|
275
|
+
|
276
|
+
ws.onclose do
|
277
|
+
handle_player_disconnect(ws)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def handle_message(ws, msg)
|
283
|
+
begin
|
284
|
+
data = JSON.parse(msg)
|
285
|
+
case data['type']
|
286
|
+
when MessageType::JOIN_REQUEST
|
287
|
+
handle_join_request(ws, data)
|
288
|
+
when MessageType::ANSWER
|
289
|
+
handle_answer(data)
|
290
|
+
when MessageType::CHAT
|
291
|
+
broadcast_message(data)
|
292
|
+
else
|
293
|
+
puts "Unknown message type: #{data['type']}".colorize(:red)
|
294
|
+
end
|
295
|
+
rescue JSON::ParserError => e
|
296
|
+
puts "Invalid message format: #{e.message}".colorize(:red)
|
297
|
+
rescue => e
|
298
|
+
puts "Error processing message: #{e.message}".colorize(:red)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def handle_join_request(ws, data)
|
303
|
+
player_name = data['name']
|
304
|
+
sent_password = data['password']
|
305
|
+
|
306
|
+
response = {
|
307
|
+
type: MessageType::JOIN_RESPONSE
|
308
|
+
}
|
309
|
+
|
310
|
+
# Check if game is already in progress
|
311
|
+
if @game_state != :lobby
|
312
|
+
response.merge!(success: false, message: "Game is already in progress")
|
313
|
+
# Validate password
|
314
|
+
elsif sent_password != password
|
315
|
+
response.merge!(success: false, message: "Incorrect password")
|
316
|
+
# Check for duplicate names
|
317
|
+
elsif @players.key?(player_name)
|
318
|
+
response.merge!(success: false, message: "Player name already taken")
|
319
|
+
else
|
320
|
+
# Add player to the game
|
321
|
+
@players[player_name] = ws
|
322
|
+
@scores[player_name] = 0
|
323
|
+
|
324
|
+
# Include current player list in the response
|
325
|
+
response.merge!(
|
326
|
+
success: true,
|
327
|
+
message: "Successfully joined the game",
|
328
|
+
players: @players.keys
|
329
|
+
)
|
330
|
+
|
331
|
+
# Notify all existing players about the new player
|
332
|
+
broadcast_message({
|
333
|
+
type: 'player_joined',
|
334
|
+
name: player_name,
|
335
|
+
players: @players.keys
|
336
|
+
}, exclude: player_name)
|
337
|
+
|
338
|
+
# Log message for player joining
|
339
|
+
log_message("🟢 #{player_name} has joined the game", :green)
|
340
|
+
# Update player list in sidebar
|
341
|
+
update_player_list
|
342
|
+
end
|
343
|
+
|
344
|
+
ws.send(response.to_json)
|
345
|
+
end
|
346
|
+
|
347
|
+
def handle_player_disconnect(ws)
|
348
|
+
# Find the player who disconnected
|
349
|
+
player_name = @players.key(ws)
|
350
|
+
return unless player_name
|
351
|
+
|
352
|
+
# Remove the player
|
353
|
+
@players.delete(player_name)
|
354
|
+
|
355
|
+
# Log message for player leaving
|
356
|
+
log_message("🔴 #{player_name} has left the game", :yellow)
|
357
|
+
# Update player list in sidebar
|
358
|
+
update_player_list
|
359
|
+
|
360
|
+
# Notify other players
|
361
|
+
broadcast_message({
|
362
|
+
type: 'player_left',
|
363
|
+
name: player_name,
|
364
|
+
players: @players.keys
|
365
|
+
})
|
366
|
+
end
|
367
|
+
|
368
|
+
def handle_answer(data)
|
369
|
+
return unless @game_state == :playing
|
370
|
+
|
371
|
+
player_name = data['name']
|
372
|
+
answer = data['answer']
|
373
|
+
question_id = data['question_id']
|
374
|
+
|
375
|
+
# Make sure the answer is for the current question
|
376
|
+
return unless question_id == @current_question_id
|
377
|
+
|
378
|
+
# Don't allow duplicate answers
|
379
|
+
return if @player_answers.dig(player_name, :answered)
|
380
|
+
|
381
|
+
# Calculate time taken to answer
|
382
|
+
time_taken = Time.now - @question_start_time
|
383
|
+
|
384
|
+
# Get current question
|
385
|
+
current_question = @round_questions[@current_question_index]
|
386
|
+
|
387
|
+
# Handle nil answer (timeout) differently
|
388
|
+
if answer.nil?
|
389
|
+
# For timeouts, set a special "TIMEOUT" answer with 0 points
|
390
|
+
@player_answers[player_name] = {
|
391
|
+
answer: "TIMEOUT",
|
392
|
+
time_taken: time_taken,
|
393
|
+
answered: true,
|
394
|
+
correct: false,
|
395
|
+
points: 0
|
396
|
+
}
|
397
|
+
|
398
|
+
# Send timeout feedback to the player
|
399
|
+
feedback = {
|
400
|
+
type: MessageType::ANSWER_FEEDBACK,
|
401
|
+
answer: "TIMEOUT",
|
402
|
+
correct: false,
|
403
|
+
correct_answer: current_question[:correct_answer],
|
404
|
+
points: 0
|
405
|
+
}
|
406
|
+
@players[player_name]&.send(feedback.to_json)
|
407
|
+
|
408
|
+
# Log the timeout
|
409
|
+
truncated_name = player_name.length > 15 ? "#{player_name[0...12]}..." : player_name
|
410
|
+
log_message("#{truncated_name} timed out after #{time_taken.round(2)}s ⏰", :yellow)
|
411
|
+
else
|
412
|
+
# Regular answer processing
|
413
|
+
# Calculate points for this answer
|
414
|
+
correct = answer == current_question[:correct_answer]
|
415
|
+
points = 0
|
416
|
+
|
417
|
+
if correct
|
418
|
+
points = 10 # Base points for correct answer
|
419
|
+
|
420
|
+
# Bonus points for fast answers
|
421
|
+
if time_taken < 5
|
422
|
+
points += 5
|
423
|
+
elsif time_taken < 10
|
424
|
+
points += 3
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# Store the answer with points pre-calculated
|
429
|
+
@player_answers[player_name] = {
|
430
|
+
answer: answer,
|
431
|
+
time_taken: time_taken,
|
432
|
+
answered: true,
|
433
|
+
correct: correct,
|
434
|
+
points: points
|
435
|
+
}
|
436
|
+
|
437
|
+
# Send immediate feedback to this player only
|
438
|
+
send_answer_feedback(player_name, answer, correct, current_question, points)
|
439
|
+
|
440
|
+
# Log this answer - ensure the name is not too long
|
441
|
+
truncated_name = player_name.length > 15 ? "#{player_name[0...12]}..." : player_name
|
442
|
+
log_message("#{truncated_name} answered in #{time_taken.round(2)}s: #{correct ? "Correct ✓" : "Wrong ✗"}", correct ? :green : :red)
|
443
|
+
end
|
444
|
+
|
445
|
+
# Check if all players have answered, regardless of timeout or manual answer
|
446
|
+
check_all_answered
|
447
|
+
end
|
448
|
+
|
449
|
+
def send_answer_feedback(player_name, answer, correct, question, points=0)
|
450
|
+
# Send feedback only to the player who answered
|
451
|
+
ws = @players[player_name]
|
452
|
+
return unless ws
|
453
|
+
|
454
|
+
feedback = {
|
455
|
+
type: MessageType::ANSWER_FEEDBACK,
|
456
|
+
answer: answer,
|
457
|
+
correct: correct,
|
458
|
+
correct_answer: question[:correct_answer],
|
459
|
+
points: points # Include points in the feedback
|
460
|
+
}
|
461
|
+
|
462
|
+
ws.send(feedback.to_json)
|
463
|
+
end
|
464
|
+
|
465
|
+
def check_all_answered
|
466
|
+
# If all players have answered, log it but WAIT for the full timeout
|
467
|
+
# This ensures consistent timing regardless of how fast people answer
|
468
|
+
if @player_answers.keys.size == @players.size
|
469
|
+
timeout_sec = GitGameShow::DEFAULT_CONFIG[:question_timeout]
|
470
|
+
log_message("All players have answered - waiting for timeout (#{timeout_sec}s)", :cyan)
|
471
|
+
# We don't immediately evaluate anymore - we wait for the timer
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def evaluate_answers
|
476
|
+
return unless @current_mini_game && @current_question_index < @round_questions.size
|
477
|
+
|
478
|
+
# We can't actually cancel timers in the current EM implementation
|
479
|
+
# Just set a flag indicating that we've already evaluated this question
|
480
|
+
return if @question_already_evaluated
|
481
|
+
@question_already_evaluated = true
|
482
|
+
|
483
|
+
current_question = @round_questions[@current_question_index]
|
484
|
+
|
485
|
+
# Use our pre-calculated answers instead of running evaluation again
|
486
|
+
# This ensures consistency between immediate feedback and final results
|
487
|
+
results = {}
|
488
|
+
@player_answers.each do |player_name, answer_data|
|
489
|
+
results[player_name] = {
|
490
|
+
answer: answer_data[:answer],
|
491
|
+
correct: answer_data[:correct],
|
492
|
+
points: answer_data[:points]
|
493
|
+
}
|
494
|
+
end
|
495
|
+
|
496
|
+
# Update scores
|
497
|
+
results.each do |player, result|
|
498
|
+
@scores[player] += result[:points]
|
499
|
+
end
|
500
|
+
|
501
|
+
# Send results to all players
|
502
|
+
broadcast_message({
|
503
|
+
type: MessageType::ROUND_RESULT,
|
504
|
+
question: current_question,
|
505
|
+
results: results,
|
506
|
+
correct_answer: current_question[:correct_answer],
|
507
|
+
scores: @scores.sort_by { |_, score| -score }.to_h # Include current scores
|
508
|
+
})
|
509
|
+
|
510
|
+
# Log current scores for the host
|
511
|
+
log_message("Current scores:", :cyan)
|
512
|
+
@scores.sort_by { |_, score| -score }.each do |player, score|
|
513
|
+
# Truncate player names if too long
|
514
|
+
truncated_name = player.length > 15 ? "#{player[0...12]}..." : player
|
515
|
+
log_message("#{truncated_name}: #{score} points", :light_blue)
|
516
|
+
end
|
517
|
+
|
518
|
+
# Move to next question or round
|
519
|
+
@current_question_index += 1
|
520
|
+
@player_answers = {}
|
521
|
+
@question_already_evaluated = false
|
522
|
+
|
523
|
+
if @current_question_index >= @round_questions.size
|
524
|
+
# End of round
|
525
|
+
EM.add_timer(GitGameShow::DEFAULT_CONFIG[:transition_delay]) do
|
526
|
+
start_next_round
|
527
|
+
end
|
528
|
+
else
|
529
|
+
# Next question - use mini-game specific timing if available
|
530
|
+
display_time = @current_mini_game.class.respond_to?(:question_display_time) ?
|
531
|
+
@current_mini_game.class.question_display_time :
|
532
|
+
GitGameShow::DEFAULT_CONFIG[:question_display_time]
|
533
|
+
|
534
|
+
log_message("Next question in #{display_time} seconds...", :cyan)
|
535
|
+
EM.add_timer(display_time) do
|
536
|
+
ask_next_question
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
def start_game
|
542
|
+
# If players are in an ended state, reset them first
|
543
|
+
if @game_state == :ended
|
544
|
+
log_message("Resetting players from previous game...", :light_black)
|
545
|
+
begin
|
546
|
+
broadcast_message({
|
547
|
+
type: MessageType::GAME_RESET,
|
548
|
+
message: "Get ready! The host is starting a new game..."
|
549
|
+
})
|
550
|
+
# Give players a moment to see the reset message
|
551
|
+
sleep(1)
|
552
|
+
rescue => e
|
553
|
+
log_message("Error sending reset message: #{e.message}", :red)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
# Only start if we're in lobby state (which includes after reset)
|
558
|
+
return unless @game_state == :lobby
|
559
|
+
return if @players.empty?
|
560
|
+
|
561
|
+
@game_state = :playing
|
562
|
+
@current_round = 0
|
563
|
+
|
564
|
+
# Reset the mini-game tracking for a new game
|
565
|
+
@used_mini_games = []
|
566
|
+
@available_mini_games = []
|
567
|
+
|
568
|
+
broadcast_message({
|
569
|
+
type: MessageType::GAME_START,
|
570
|
+
rounds: @rounds,
|
571
|
+
players: @players.keys
|
572
|
+
})
|
573
|
+
|
574
|
+
log_message("Game started with #{@players.size} players", :green)
|
575
|
+
|
576
|
+
start_next_round
|
577
|
+
end
|
578
|
+
|
579
|
+
def start_next_round
|
580
|
+
@current_round += 1
|
581
|
+
|
582
|
+
# Reset question evaluation flag for the new round
|
583
|
+
@question_already_evaluated = false
|
584
|
+
|
585
|
+
# Check if we've completed all rounds
|
586
|
+
if @current_round > @rounds
|
587
|
+
log_message("All rounds completed! Showing final scores...", :green)
|
588
|
+
EM.next_tick { end_game } # Use next_tick to ensure it runs after current operations
|
589
|
+
return
|
590
|
+
end
|
591
|
+
|
592
|
+
# Select mini-game for this round with better variety
|
593
|
+
@current_mini_game = select_next_mini_game.new
|
594
|
+
@round_questions = @current_mini_game.generate_questions(@repo)
|
595
|
+
@current_question_index = 0
|
596
|
+
|
597
|
+
# Announce new round
|
598
|
+
broadcast_message({
|
599
|
+
type: 'round_start',
|
600
|
+
round: @current_round,
|
601
|
+
total_rounds: @rounds,
|
602
|
+
mini_game: @current_mini_game.class.name,
|
603
|
+
description: @current_mini_game.class.description
|
604
|
+
})
|
605
|
+
|
606
|
+
log_message("Starting round #{@current_round}: #{@current_mini_game.class.name}", :cyan)
|
607
|
+
|
608
|
+
# Start the first question after a short delay
|
609
|
+
EM.add_timer(3) do
|
610
|
+
ask_next_question
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
def ask_next_question
|
615
|
+
return if @current_question_index >= @round_questions.size
|
616
|
+
|
617
|
+
# Log information for debugging
|
618
|
+
log_message("Preparing question #{@current_question_index + 1} of #{@round_questions.size}", :cyan)
|
619
|
+
|
620
|
+
# Reset the evaluation flag for the new question
|
621
|
+
@question_already_evaluated = false
|
622
|
+
|
623
|
+
# Save current question without printing it to console
|
624
|
+
current_question = @round_questions[@current_question_index]
|
625
|
+
@current_question_id = "#{@current_round}-#{@current_question_index}"
|
626
|
+
@question_start_time = Time.now
|
627
|
+
@player_answers = {}
|
628
|
+
|
629
|
+
# Send question to all players
|
630
|
+
# Use mini-game specific timeout if available, otherwise use default
|
631
|
+
timeout = @current_mini_game.class.respond_to?(:question_timeout) ?
|
632
|
+
@current_mini_game.class.question_timeout :
|
633
|
+
GitGameShow::DEFAULT_CONFIG[:question_timeout]
|
634
|
+
|
635
|
+
question_data = {
|
636
|
+
type: MessageType::QUESTION,
|
637
|
+
question_id: @current_question_id,
|
638
|
+
question: current_question[:question],
|
639
|
+
options: current_question[:options],
|
640
|
+
timeout: timeout,
|
641
|
+
round: @current_round,
|
642
|
+
question_number: @current_question_index + 1,
|
643
|
+
total_questions: @round_questions.size
|
644
|
+
}
|
645
|
+
|
646
|
+
# Add question_type if it's a special question type (like ordering)
|
647
|
+
if current_question[:question_type]
|
648
|
+
question_data[:question_type] = current_question[:question_type]
|
649
|
+
end
|
650
|
+
|
651
|
+
# Add commit info if available (for AuthorQuiz)
|
652
|
+
if current_question[:commit_info]
|
653
|
+
question_data[:commit_info] = current_question[:commit_info]
|
654
|
+
end
|
655
|
+
|
656
|
+
# Don't log detailed question info to prevent author lists from showing
|
657
|
+
log_message("Question #{@current_question_index + 1}/#{@round_questions.size}", :cyan)
|
658
|
+
|
659
|
+
log_message("Broadcasting question to players...", :cyan)
|
660
|
+
broadcast_message(question_data)
|
661
|
+
|
662
|
+
# Set a timer for question timeout - ALWAYS evaluate after timeout
|
663
|
+
# Use same timeout value we sent to clients
|
664
|
+
EM.add_timer(timeout) do
|
665
|
+
log_message("Question timeout (#{timeout}s) - evaluating", :yellow)
|
666
|
+
evaluate_answers unless @current_question_index >= @round_questions.size
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
def broadcast_scoreboard
|
671
|
+
broadcast_message({
|
672
|
+
type: MessageType::SCOREBOARD,
|
673
|
+
scores: @scores.sort_by { |_, score| -score }.to_h
|
674
|
+
})
|
675
|
+
end
|
676
|
+
|
677
|
+
def end_game
|
678
|
+
@game_state = :ended
|
679
|
+
|
680
|
+
# Safety check - make sure we have scores
|
681
|
+
if @scores.empty?
|
682
|
+
log_message("Game ended, but no scores were recorded.", :yellow)
|
683
|
+
|
684
|
+
# Reset game state for potential restart
|
685
|
+
@current_round = 0
|
686
|
+
@game_state = :lobby
|
687
|
+
@current_mini_game = nil
|
688
|
+
@round_questions = []
|
689
|
+
@current_question_index = 0
|
690
|
+
@question_already_evaluated = false
|
691
|
+
@player_answers = {}
|
692
|
+
|
693
|
+
# Update UI
|
694
|
+
update_player_list
|
695
|
+
log_message("Ready for a new game! Type 'start' when players have joined.", :green)
|
696
|
+
return
|
697
|
+
end
|
698
|
+
|
699
|
+
# Determine the winner
|
700
|
+
winner = @scores.max_by { |_, score| score }
|
701
|
+
|
702
|
+
# Safety check - ensure winner isn't nil
|
703
|
+
if winner.nil?
|
704
|
+
log_message("Error: Could not determine winner. No valid scores found.", :red)
|
705
|
+
|
706
|
+
# Reset and return early
|
707
|
+
@scores = {}
|
708
|
+
@current_round = 0
|
709
|
+
@game_state = :lobby
|
710
|
+
update_player_list
|
711
|
+
return
|
712
|
+
end
|
713
|
+
|
714
|
+
# Notify all players
|
715
|
+
begin
|
716
|
+
broadcast_message({
|
717
|
+
type: MessageType::GAME_END,
|
718
|
+
winner: winner[0],
|
719
|
+
scores: @scores.sort_by { |_, score| -score }.to_h
|
720
|
+
})
|
721
|
+
rescue => e
|
722
|
+
log_message("Error broadcasting final results: #{e.message}", :red)
|
723
|
+
end
|
724
|
+
|
725
|
+
# Display the final results on screen
|
726
|
+
display_final_results(winner)
|
727
|
+
|
728
|
+
# Reset game state for potential restart
|
729
|
+
@scores = {}
|
730
|
+
@current_round = 0
|
731
|
+
@game_state = :lobby
|
732
|
+
@current_mini_game = nil
|
733
|
+
@round_questions = []
|
734
|
+
@current_question_index = 0
|
735
|
+
@question_already_evaluated = false
|
736
|
+
@player_answers = {}
|
737
|
+
|
738
|
+
# Re-initialize player scores for existing players
|
739
|
+
@players.keys.each do |player_name|
|
740
|
+
@scores[player_name] = 0
|
741
|
+
end
|
742
|
+
|
743
|
+
# Don't reset players yet - let them stay on the leaderboard screen
|
744
|
+
# They'll be reset when a new game starts
|
745
|
+
log_message("Players will remain on the leaderboard screen until a new game starts", :light_black)
|
746
|
+
|
747
|
+
# Update UI
|
748
|
+
update_player_list
|
749
|
+
log_message("Game ended! Type 'start' to play again or 'exit' to quit.", :cyan)
|
750
|
+
end
|
751
|
+
|
752
|
+
def display_final_results(winner)
|
753
|
+
begin
|
754
|
+
# Use log messages instead of clearing screen
|
755
|
+
divider = "=" * (@main_width - 5)
|
756
|
+
log_message(divider, :yellow)
|
757
|
+
log_message("🏆 GAME OVER - FINAL SCORES 🏆", :yellow)
|
758
|
+
|
759
|
+
# Safety check for winner
|
760
|
+
if !winner || !winner[0] || !winner[1]
|
761
|
+
log_message("Error: Invalid winner data", :red)
|
762
|
+
log_message("Ready for a new game! Type 'start' when players have joined.", :green)
|
763
|
+
return
|
764
|
+
end
|
765
|
+
|
766
|
+
# Announce winner
|
767
|
+
winner_name = winner[0].to_s
|
768
|
+
winner_name = winner_name.length > 20 ? "#{winner_name[0...17]}..." : winner_name
|
769
|
+
log_message("Winner: #{winner_name} with #{winner[1]} points!", :green)
|
770
|
+
|
771
|
+
# Safety check for scores
|
772
|
+
if @scores.nil? || @scores.empty?
|
773
|
+
log_message("No scores available to display", :yellow)
|
774
|
+
log_message(divider, :yellow)
|
775
|
+
log_message("Ready for a new game! Type 'start' when players have joined.", :green)
|
776
|
+
return
|
777
|
+
end
|
778
|
+
|
779
|
+
# List players in console (but limit to avoid taking too much space)
|
780
|
+
log_message("Leaderboard:", :cyan)
|
781
|
+
|
782
|
+
leaderboard_entries = []
|
783
|
+
|
784
|
+
# Sort scores safely
|
785
|
+
begin
|
786
|
+
sorted_scores = @scores.sort_by { |_, score| -(score || 0) }
|
787
|
+
rescue => e
|
788
|
+
log_message("Error sorting scores: #{e.message}", :red)
|
789
|
+
sorted_scores = @scores.to_a
|
790
|
+
end
|
791
|
+
|
792
|
+
# Show limited entries in console
|
793
|
+
sorted_scores.take(10).each_with_index do |(name, score), index|
|
794
|
+
# Safely handle name and score
|
795
|
+
player_name = name.to_s
|
796
|
+
player_score = score || 0
|
797
|
+
|
798
|
+
# Truncate name if needed
|
799
|
+
display_name = player_name.length > 15 ? "#{player_name[0...12]}..." : player_name
|
800
|
+
|
801
|
+
# Format based on position
|
802
|
+
case index
|
803
|
+
when 0
|
804
|
+
log_message("🥇 #{display_name}: #{player_score} points", :yellow)
|
805
|
+
when 1
|
806
|
+
log_message("🥈 #{display_name}: #{player_score} points", :light_blue)
|
807
|
+
when 2
|
808
|
+
log_message("🥉 #{display_name}: #{player_score} points", :light_magenta)
|
809
|
+
else
|
810
|
+
log_message("#{(index + 1).to_s}. #{display_name}: #{player_score} points", :white)
|
811
|
+
end
|
812
|
+
end
|
813
|
+
|
814
|
+
# If there are more players than shown, add a note
|
815
|
+
if sorted_scores.size > 10
|
816
|
+
log_message("... and #{sorted_scores.size - 10} more (see full results in file)", :light_black)
|
817
|
+
end
|
818
|
+
|
819
|
+
# Build complete entries array for file
|
820
|
+
sorted_scores.each_with_index do |(name, score), index|
|
821
|
+
# Use safe values
|
822
|
+
player_name = name.to_s
|
823
|
+
player_score = score || 0
|
824
|
+
|
825
|
+
# Add medals for file format
|
826
|
+
medal = case index
|
827
|
+
when 0 then "🥇"
|
828
|
+
when 1 then "🥈"
|
829
|
+
when 2 then "🥉"
|
830
|
+
else "#{index + 1}."
|
831
|
+
end
|
832
|
+
|
833
|
+
leaderboard_entries << "#{medal} #{player_name}: #{player_score} points"
|
834
|
+
end
|
835
|
+
|
836
|
+
# Save leaderboard to file
|
837
|
+
filename = save_leaderboard_to_file(winner, leaderboard_entries)
|
838
|
+
|
839
|
+
log_message(divider, :yellow)
|
840
|
+
if filename
|
841
|
+
log_message("Leaderboard saved to: #{filename}", :cyan)
|
842
|
+
else
|
843
|
+
log_message("Failed to save leaderboard to file", :red)
|
844
|
+
end
|
845
|
+
log_message("Ready for a new game! Type 'start' when players have joined.", :green)
|
846
|
+
rescue => e
|
847
|
+
# Catch-all error handling
|
848
|
+
log_message("Error displaying final results: #{e.message}", :red)
|
849
|
+
log_message("Game has ended. Type 'start' for a new game or 'exit' to quit.", :yellow)
|
850
|
+
end
|
851
|
+
end
|
852
|
+
|
853
|
+
def save_leaderboard_to_file(winner, leaderboard_entries)
|
854
|
+
begin
|
855
|
+
# Validate parameters
|
856
|
+
if !winner || !leaderboard_entries
|
857
|
+
log_message("Error: Invalid data for leaderboard file", :red)
|
858
|
+
return nil
|
859
|
+
end
|
860
|
+
|
861
|
+
# Create a unique filename with timestamp
|
862
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
863
|
+
filename = "git_game_show_results_#{timestamp}.txt"
|
864
|
+
|
865
|
+
# Use a base path that should be writable
|
866
|
+
begin
|
867
|
+
# First try current directory
|
868
|
+
file_path = File.join(Dir.pwd, filename)
|
869
|
+
|
870
|
+
# Test if we can write there
|
871
|
+
unless File.writable?(Dir.pwd)
|
872
|
+
# If not, try user's home directory
|
873
|
+
file_path = File.join(Dir.home, filename)
|
874
|
+
filename = File.join(Dir.home, filename) # Update filename to show full path
|
875
|
+
end
|
876
|
+
rescue
|
877
|
+
# If all else fails, use /tmp (Unix) or %TEMP% (Windows)
|
878
|
+
temp_dir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
|
879
|
+
file_path = File.join(temp_dir, filename)
|
880
|
+
filename = file_path # Update filename to show full path
|
881
|
+
end
|
882
|
+
|
883
|
+
# Get repo name from git directory path safely
|
884
|
+
begin
|
885
|
+
repo_name = @repo && @repo.dir ? File.basename(@repo.dir.path) : "Unknown"
|
886
|
+
rescue
|
887
|
+
repo_name = "Unknown"
|
888
|
+
end
|
889
|
+
|
890
|
+
File.open(file_path, "w") do |file|
|
891
|
+
# Write header
|
892
|
+
file.puts "GIT GAME SHOW - FINAL RESULTS"
|
893
|
+
file.puts "==========================="
|
894
|
+
file.puts "Date: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
895
|
+
file.puts "Repository: #{repo_name}"
|
896
|
+
file.puts "Players: #{@players.keys.size}"
|
897
|
+
file.puts ""
|
898
|
+
|
899
|
+
# Write winner safely
|
900
|
+
begin
|
901
|
+
winner_name = winner[0].to_s
|
902
|
+
winner_score = winner[1].to_i
|
903
|
+
file.puts "WINNER: #{winner_name} with #{winner_score} points!"
|
904
|
+
rescue
|
905
|
+
file.puts "WINNER: Unknown (error retrieving winner data)"
|
906
|
+
end
|
907
|
+
file.puts ""
|
908
|
+
|
909
|
+
# Write full leaderboard
|
910
|
+
file.puts "FULL LEADERBOARD:"
|
911
|
+
file.puts "---------------"
|
912
|
+
if leaderboard_entries.empty?
|
913
|
+
file.puts "No entries recorded"
|
914
|
+
else
|
915
|
+
leaderboard_entries.each do |entry|
|
916
|
+
file.puts entry.to_s
|
917
|
+
end
|
918
|
+
end
|
919
|
+
|
920
|
+
# Write footer
|
921
|
+
file.puts ""
|
922
|
+
file.puts "Thanks for playing Git Game Show!"
|
923
|
+
end
|
924
|
+
|
925
|
+
return filename
|
926
|
+
rescue => e
|
927
|
+
log_message("Error saving leaderboard: #{e.message}", :red)
|
928
|
+
return nil
|
929
|
+
end
|
930
|
+
end
|
931
|
+
|
932
|
+
# Removed old full-screen methods as we now use log_message based approach
|
933
|
+
|
934
|
+
def broadcast_message(message, exclude: nil)
|
935
|
+
@players.each do |player_name, ws|
|
936
|
+
# Skip excluded player if specified
|
937
|
+
next if exclude && player_name == exclude
|
938
|
+
ws.send(message.to_json)
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
def setup_console_commands
|
943
|
+
Thread.new do
|
944
|
+
prompt = TTY::Prompt.new
|
945
|
+
|
946
|
+
loop do
|
947
|
+
command = prompt.select("Host commands:", {
|
948
|
+
"Start game" => :start,
|
949
|
+
"Show players" => :players,
|
950
|
+
"Show scoreboard" => :scoreboard,
|
951
|
+
"End game" => :end,
|
952
|
+
"Exit server" => :exit
|
953
|
+
})
|
954
|
+
|
955
|
+
case command
|
956
|
+
when :start
|
957
|
+
if @players.size < 1
|
958
|
+
puts "Need at least one player to start".colorize(:red)
|
959
|
+
else
|
960
|
+
start_game
|
961
|
+
end
|
962
|
+
when :players
|
963
|
+
puts "Connected players:".colorize(:cyan)
|
964
|
+
@players.keys.each { |name| puts "- #{name}" }
|
965
|
+
when :scoreboard
|
966
|
+
puts "Current scores:".colorize(:cyan)
|
967
|
+
@scores.sort_by { |_, score| -score }.each do |name, score|
|
968
|
+
puts "- #{name}: #{score}"
|
969
|
+
end
|
970
|
+
when :end
|
971
|
+
puts "Ending game...".colorize(:yellow)
|
972
|
+
end_game
|
973
|
+
when :exit
|
974
|
+
puts "Shutting down server...".colorize(:yellow)
|
975
|
+
EM.stop_event_loop
|
976
|
+
break
|
977
|
+
end
|
978
|
+
end
|
979
|
+
end
|
980
|
+
end
|
981
|
+
|
982
|
+
def setup_fixed_console_commands
|
983
|
+
# Log initial instructions
|
984
|
+
log_message("Available commands: help, start, exit", :cyan)
|
985
|
+
log_message("Type a command and press Enter", :cyan)
|
986
|
+
|
987
|
+
# Show that the server is ready
|
988
|
+
log_message("Server is running. Waiting for players to join...", :green)
|
989
|
+
|
990
|
+
# Handle commands in a separate thread
|
991
|
+
Thread.new do
|
992
|
+
loop do
|
993
|
+
# Show command prompt and get input
|
994
|
+
draw_command_prompt
|
995
|
+
command = gets&.chomp&.downcase
|
996
|
+
next unless command
|
997
|
+
|
998
|
+
# Process commands
|
999
|
+
case command
|
1000
|
+
when 'start'
|
1001
|
+
if @players.empty?
|
1002
|
+
log_message("Need at least one player to start", :red)
|
1003
|
+
else
|
1004
|
+
log_message("Starting game with #{@players.size} players...", :green)
|
1005
|
+
start_game
|
1006
|
+
end
|
1007
|
+
# 'players' command removed - player list is always visible in sidebar
|
1008
|
+
when 'help'
|
1009
|
+
log_message("Available commands:", :cyan)
|
1010
|
+
log_message(" start - Start the game with current players", :cyan)
|
1011
|
+
log_message(" end - End current game and show final scores", :cyan)
|
1012
|
+
log_message(" reset - Manually reset players to waiting room (after game ends)", :cyan)
|
1013
|
+
log_message(" help - Show this help message", :cyan)
|
1014
|
+
log_message(" exit - Shut down the server and exit", :cyan)
|
1015
|
+
when 'end'
|
1016
|
+
if @game_state == :playing
|
1017
|
+
log_message("Ending game early...", :yellow)
|
1018
|
+
end_game
|
1019
|
+
elsif @game_state == :ended
|
1020
|
+
log_message("Game already ended. Type 'start' to begin a new game.", :yellow)
|
1021
|
+
else
|
1022
|
+
log_message("No game in progress to end", :yellow)
|
1023
|
+
end
|
1024
|
+
when 'reset'
|
1025
|
+
# Add a separate reset command for manually resetting players
|
1026
|
+
if @game_state == :ended
|
1027
|
+
log_message("Manually resetting all players to waiting room state...", :yellow)
|
1028
|
+
|
1029
|
+
# Send a game reset message to all players
|
1030
|
+
broadcast_message({
|
1031
|
+
type: MessageType::GAME_RESET,
|
1032
|
+
message: "Game has been reset by the host. Waiting for a new game to start."
|
1033
|
+
})
|
1034
|
+
|
1035
|
+
# Update game state
|
1036
|
+
@game_state = :lobby
|
1037
|
+
else
|
1038
|
+
log_message("Can only reset after a game has ended", :yellow)
|
1039
|
+
end
|
1040
|
+
when 'exit'
|
1041
|
+
# Clean up before exiting
|
1042
|
+
log_message("Shutting down server...", :yellow)
|
1043
|
+
print @cursor.show # Make sure cursor is visible
|
1044
|
+
print @cursor.clear_screen
|
1045
|
+
EM.stop_event_loop
|
1046
|
+
break
|
1047
|
+
else
|
1048
|
+
log_message("Unknown command: #{command}. Type 'help' for available commands.", :red)
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
# Small delay to process any display changes
|
1052
|
+
sleep 0.1
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
def print_players_list
|
1058
|
+
puts "\nCurrent players:"
|
1059
|
+
if @players.empty?
|
1060
|
+
puts "No players have joined yet".colorize(:yellow)
|
1061
|
+
else
|
1062
|
+
@players.keys.each_with_index do |name, i|
|
1063
|
+
puts "#{i+1}. #{name}"
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
def refresh_host_ui
|
1069
|
+
# Only clear the screen for the first draw
|
1070
|
+
if !@ui_drawn
|
1071
|
+
system("clear") || system("cls")
|
1072
|
+
|
1073
|
+
# Header - only show on first draw
|
1074
|
+
puts <<-HEADER.colorize(:green)
|
1075
|
+
██████╗ ██╗████████╗ ██████╗ █████╗ ███╗ ███╗███████╗
|
1076
|
+
██╔════╝ ██║╚══██╔══╝ ██╔════╝ ██╔══██╗████╗ ████║██╔════╝
|
1077
|
+
██║ ███╗██║ ██║ ██║ ███╗███████║██╔████╔██║█████╗
|
1078
|
+
██║ ██║██║ ██║ ██║ ██║██╔══██║██║╚██╔╝██║██╔══╝
|
1079
|
+
╚██████╔╝██║ ██║ ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗
|
1080
|
+
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
|
1081
|
+
HEADER
|
1082
|
+
else
|
1083
|
+
# Just print a separator for subsequent updates
|
1084
|
+
puts "\n\n" + ("=" * 60)
|
1085
|
+
puts "GIT GAME SHOW - STATUS UPDATE".center(60).colorize(:green)
|
1086
|
+
puts ("=" * 60)
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
# Server info
|
1090
|
+
puts "\n==================== SERVER INFO ====================".colorize(:cyan)
|
1091
|
+
puts "Status: #{game_state_text}".colorize(game_state_color)
|
1092
|
+
puts "Rounds: #{@current_round}/#{rounds}".colorize(:light_blue)
|
1093
|
+
puts "Repository: #{repo.dir.path}".colorize(:light_blue)
|
1094
|
+
|
1095
|
+
# Display join link prominently
|
1096
|
+
puts "\n==================== JOIN LINK =====================".colorize(:green)
|
1097
|
+
puts @join_link.to_s.colorize(:yellow)
|
1098
|
+
|
1099
|
+
# Player list
|
1100
|
+
puts "\n==================== PLAYERS =======================".colorize(:cyan)
|
1101
|
+
if @players.empty?
|
1102
|
+
puts "No players have joined yet".colorize(:yellow)
|
1103
|
+
else
|
1104
|
+
@players.keys.each_with_index do |name, i|
|
1105
|
+
puts "#{i+1}. #{name}"
|
1106
|
+
end
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
# Current game state info
|
1110
|
+
case @game_state
|
1111
|
+
when :lobby
|
1112
|
+
puts "\nWaiting for players to join. Type 'start' when ready.".colorize(:yellow)
|
1113
|
+
puts "Players can join using the link above.".colorize(:yellow)
|
1114
|
+
puts "Type 'players' to see the current list of players.".colorize(:yellow)
|
1115
|
+
when :playing
|
1116
|
+
puts "\n==================== GAME INFO =======================".colorize(:cyan)
|
1117
|
+
puts "Current round: #{@current_round}/#{rounds}".colorize(:light_blue)
|
1118
|
+
puts "Current mini-game: #{@current_mini_game&.class&.name || 'N/A'}".colorize(:light_blue)
|
1119
|
+
puts "Question: #{@current_question_index + 1}/#{@round_questions.size}".colorize(:light_blue) if @round_questions&.any?
|
1120
|
+
|
1121
|
+
# Show scoreboard
|
1122
|
+
puts "\n=================== SCOREBOARD ======================".colorize(:cyan)
|
1123
|
+
if @scores.empty?
|
1124
|
+
puts "No scores yet".colorize(:yellow)
|
1125
|
+
else
|
1126
|
+
@scores.sort_by { |_, score| -score }.each_with_index do |(name, score), i|
|
1127
|
+
case i
|
1128
|
+
when 0
|
1129
|
+
puts "🥇 #{name}: #{score} points".colorize(:light_yellow)
|
1130
|
+
when 1
|
1131
|
+
puts "🥈 #{name}: #{score} points".colorize(:light_blue)
|
1132
|
+
when 2
|
1133
|
+
puts "🥉 #{name}: #{score} points".colorize(:light_magenta)
|
1134
|
+
else
|
1135
|
+
puts "#{i+1}. #{name}: #{score} points"
|
1136
|
+
end
|
1137
|
+
end
|
1138
|
+
end
|
1139
|
+
when :ended
|
1140
|
+
puts "\nGame has ended. Type 'exit' to quit or 'start' to begin a new game.".colorize(:green)
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
|
1144
|
+
# Only print command help on first draw to avoid cluttering output
|
1145
|
+
if !@ui_drawn
|
1146
|
+
puts "\nAvailable commands: help, start, players, status, end, exit".colorize(:light_black)
|
1147
|
+
puts "Type a command and press Enter".colorize(:light_black)
|
1148
|
+
end
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
def print_help_message
|
1152
|
+
puts "\n==================== HELP =========================="
|
1153
|
+
puts "Available commands:"
|
1154
|
+
puts " help - Show this help message"
|
1155
|
+
puts " start - Start the game with current players"
|
1156
|
+
puts " players - Show list of connected players"
|
1157
|
+
puts " status - Refresh the status display"
|
1158
|
+
puts " end - End the current game"
|
1159
|
+
puts " exit - Shut down the server and exit"
|
1160
|
+
puts "=================================================="
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
def print_status_message(message, status)
|
1164
|
+
color = case status
|
1165
|
+
when :success then :green
|
1166
|
+
when :error then :red
|
1167
|
+
when :warning then :yellow
|
1168
|
+
else :white
|
1169
|
+
end
|
1170
|
+
puts "\n> #{message}".colorize(color)
|
1171
|
+
end
|
1172
|
+
|
1173
|
+
def game_state_text
|
1174
|
+
case @game_state
|
1175
|
+
when :lobby then "Waiting for players"
|
1176
|
+
when :playing then "Game in progress"
|
1177
|
+
when :ended then "Game over"
|
1178
|
+
end
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
def game_state_color
|
1182
|
+
case @game_state
|
1183
|
+
when :lobby then :yellow
|
1184
|
+
when :playing then :green
|
1185
|
+
when :ended then :light_blue
|
1186
|
+
end
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
# Select the next mini-game to ensure variety and avoid repetition
|
1190
|
+
def select_next_mini_game
|
1191
|
+
# If we have no more available mini-games, reset the cycle
|
1192
|
+
if @available_mini_games.empty?
|
1193
|
+
# Repopulate with all mini-games except the last one used
|
1194
|
+
@available_mini_games = @mini_games.reject { |game| game == @used_mini_games.last }
|
1195
|
+
|
1196
|
+
# Log that we're starting a new cycle
|
1197
|
+
log_message("Starting a new cycle of mini-games", :light_black)
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
# Select a random game from the available ones
|
1201
|
+
selected_game = @available_mini_games.sample
|
1202
|
+
|
1203
|
+
# Remove the selected game from available and add to used
|
1204
|
+
@available_mini_games.delete(selected_game)
|
1205
|
+
@used_mini_games << selected_game
|
1206
|
+
|
1207
|
+
# Log which mini-game was selected
|
1208
|
+
log_message("Selected #{selected_game.name} for this round", :light_black)
|
1209
|
+
|
1210
|
+
# Return the selected game class
|
1211
|
+
selected_game
|
1212
|
+
end
|
1213
|
+
|
1214
|
+
def load_mini_games
|
1215
|
+
# Enable all mini-games
|
1216
|
+
[
|
1217
|
+
GitGameShow::AuthorQuiz,
|
1218
|
+
GitGameShow::CommitMessageQuiz,
|
1219
|
+
GitGameShow::CommitMessageCompletion,
|
1220
|
+
GitGameShow::DateOrderingQuiz
|
1221
|
+
]
|
1222
|
+
end
|
1223
|
+
end
|
1224
|
+
end
|