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,1145 @@
|
|
1
|
+
require 'websocket-client-simple'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
module GitGameShow
|
6
|
+
class PlayerClient
|
7
|
+
attr_reader :host, :port, :password, :name
|
8
|
+
|
9
|
+
def initialize(host:, port:, password:, name:)
|
10
|
+
@host = host
|
11
|
+
@port = port
|
12
|
+
@password = password
|
13
|
+
@name = name
|
14
|
+
@ws = nil
|
15
|
+
@prompt = TTY::Prompt.new
|
16
|
+
@players = []
|
17
|
+
@game_state = :lobby # :lobby, :playing, :ended
|
18
|
+
@current_timer_id = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def connect
|
22
|
+
begin
|
23
|
+
client = self # Store reference to the client instance
|
24
|
+
@ws = WebSocket::Client::Simple.connect("ws://#{host}:#{port}")
|
25
|
+
|
26
|
+
@ws.on :open do
|
27
|
+
puts "Connected to server".colorize(:green)
|
28
|
+
# Use the stored client reference
|
29
|
+
client.send_join_request
|
30
|
+
end
|
31
|
+
|
32
|
+
@ws.on :message do |msg|
|
33
|
+
client.handle_message(msg)
|
34
|
+
end
|
35
|
+
|
36
|
+
@ws.on :error do |e|
|
37
|
+
puts "Error: #{e.message}".colorize(:red)
|
38
|
+
end
|
39
|
+
|
40
|
+
@ws.on :close do |e|
|
41
|
+
puts "Connection closed (#{e.code}: #{e.reason})".colorize(:yellow)
|
42
|
+
exit(1)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Keep the client running
|
46
|
+
loop do
|
47
|
+
sleep(1)
|
48
|
+
|
49
|
+
# Check if connection is still alive
|
50
|
+
if @ws.nil? || @ws.closed?
|
51
|
+
puts "Connection lost. Exiting...".colorize(:red)
|
52
|
+
exit(1)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
rescue => e
|
56
|
+
puts "Failed to connect: #{e.message}".colorize(:red)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Make these methods public so they can be called from the WebSocket callbacks
|
61
|
+
def send_join_request
|
62
|
+
send_message({
|
63
|
+
type: MessageType::JOIN_REQUEST,
|
64
|
+
name: name,
|
65
|
+
password: password
|
66
|
+
})
|
67
|
+
end
|
68
|
+
|
69
|
+
# Make public for WebSocket callback
|
70
|
+
def handle_message(msg)
|
71
|
+
begin
|
72
|
+
data = JSON.parse(msg.data)
|
73
|
+
|
74
|
+
# Remove debug print to reduce console noise
|
75
|
+
|
76
|
+
case data['type']
|
77
|
+
when MessageType::JOIN_RESPONSE
|
78
|
+
handle_join_response(data)
|
79
|
+
when MessageType::GAME_START
|
80
|
+
handle_game_start(data)
|
81
|
+
when MessageType::GAME_RESET # New handler for game reset
|
82
|
+
handle_game_reset(data)
|
83
|
+
when 'player_joined', 'player_left'
|
84
|
+
handle_player_update(data)
|
85
|
+
when 'round_start'
|
86
|
+
handle_round_start(data)
|
87
|
+
when MessageType::QUESTION
|
88
|
+
handle_question(data)
|
89
|
+
when MessageType::ROUND_RESULT
|
90
|
+
handle_round_result(data)
|
91
|
+
when MessageType::SCOREBOARD
|
92
|
+
handle_scoreboard(data)
|
93
|
+
when MessageType::GAME_END
|
94
|
+
handle_game_end(data)
|
95
|
+
when MessageType::ANSWER_FEEDBACK
|
96
|
+
handle_answer_feedback(data)
|
97
|
+
when MessageType::CHAT
|
98
|
+
handle_chat(data)
|
99
|
+
else
|
100
|
+
puts "Unknown message type: #{data['type']}".colorize(:yellow)
|
101
|
+
end
|
102
|
+
rescue JSON::ParserError => e
|
103
|
+
puts "Invalid message format: #{e.message}".colorize(:red)
|
104
|
+
rescue => e
|
105
|
+
puts "Error processing message: #{e.message}".colorize(:red)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def handle_join_response(data)
|
110
|
+
if data['success']
|
111
|
+
@players = data['players'] # Get the full player list from server
|
112
|
+
display_waiting_room
|
113
|
+
else
|
114
|
+
puts "Failed to join: #{data['message']}".colorize(:red)
|
115
|
+
exit(1)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def display_waiting_room
|
120
|
+
clear_screen
|
121
|
+
|
122
|
+
# Draw header with fancy box
|
123
|
+
terminal_width = `tput cols`.to_i rescue 80
|
124
|
+
terminal_height = `tput lines`.to_i rescue 24
|
125
|
+
|
126
|
+
# Create title box
|
127
|
+
puts "┏#{"━" * (terminal_width - 2)}┓".colorize(:green)
|
128
|
+
puts "┃#{" GIT GAME SHOW - WAITING ROOM ".center(terminal_width - 2)}┃".colorize(:green)
|
129
|
+
puts "┗#{"━" * (terminal_width - 2)}┛".colorize(:green)
|
130
|
+
|
131
|
+
# Left column width (2/3 of terminal) for main content
|
132
|
+
left_width = (terminal_width * 0.65).to_i
|
133
|
+
|
134
|
+
# Display instructions and welcome information
|
135
|
+
puts "\n"
|
136
|
+
puts " Welcome to Git Game Show!".colorize(:yellow)
|
137
|
+
puts " Test your knowledge about Git and your team's commits through fun mini-games.".colorize(:light_white)
|
138
|
+
puts "\n"
|
139
|
+
puts " 🔹 INSTRUCTIONS:".colorize(:cyan)
|
140
|
+
puts " • The game consists of multiple rounds with different question types".colorize(:light_white)
|
141
|
+
puts " • Each round has a theme based on Git commit history".colorize(:light_white)
|
142
|
+
puts " • Answer questions as quickly as possible for maximum points".colorize(:light_white)
|
143
|
+
puts " • The player with the most points at the end wins!".colorize(:light_white)
|
144
|
+
puts "\n"
|
145
|
+
puts " 🔹 STATUS: Waiting for the host to start the game...".colorize(:light_yellow)
|
146
|
+
puts "\n"
|
147
|
+
|
148
|
+
# Draw player section in a box
|
149
|
+
player_box_width = terminal_width - 4
|
150
|
+
puts " ┏#{"━" * player_box_width}┓".colorize(:cyan)
|
151
|
+
puts " ┃#{" PLAYERS ".center(player_box_width)}┃".colorize(:cyan)
|
152
|
+
puts " ┗#{"━" * player_box_width}┛".colorize(:cyan)
|
153
|
+
|
154
|
+
# Display list of players in a nicer format
|
155
|
+
if @players.empty?
|
156
|
+
puts " (No other players yet)".colorize(:light_black)
|
157
|
+
else
|
158
|
+
# Calculate number of columns based on terminal width and name lengths
|
159
|
+
max_name_length = @players.map(&:length).max + 10 # Extra space for number and "(You)" text
|
160
|
+
|
161
|
+
# Add more spacing between players - increase padding from 4 to 10
|
162
|
+
column_width = max_name_length + 12 # More generous spacing
|
163
|
+
num_cols = [terminal_width / column_width, 3].min # Cap at 3 columns max
|
164
|
+
num_cols = 1 if num_cols < 1
|
165
|
+
|
166
|
+
# Use fewer columns for better spacing
|
167
|
+
if num_cols > 1 && @players.size > 6
|
168
|
+
# If we have many players, prefer fewer columns with more space
|
169
|
+
num_cols = [num_cols, 2].min
|
170
|
+
end
|
171
|
+
|
172
|
+
# Split players into rows for multi-column display
|
173
|
+
player_rows = @players.each_slice(((@players.size + num_cols - 1) / num_cols).ceil).to_a
|
174
|
+
|
175
|
+
puts "\n"
|
176
|
+
player_rows.each do |row_players|
|
177
|
+
row_str = " "
|
178
|
+
row_players.each_with_index do |player, idx|
|
179
|
+
col_idx = player_rows.index { |rp| rp.include?(player) }
|
180
|
+
player_num = col_idx * player_rows[0].length + idx + 1
|
181
|
+
|
182
|
+
# Apply different color for current player
|
183
|
+
if player == @name
|
184
|
+
row_str += "#{player_num}. #{player} (You)".colorize(:green).ljust(column_width)
|
185
|
+
else
|
186
|
+
row_str += "#{player_num}. #{player}".colorize(:light_blue).ljust(column_width)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
# Add a blank line between rows for vertical spacing too
|
190
|
+
puts row_str
|
191
|
+
puts ""
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
puts "\n"
|
196
|
+
puts " When the game starts, you'll see questions appear automatically.".colorize(:light_black)
|
197
|
+
puts " Get ready to test your Git knowledge!".colorize(:light_yellow)
|
198
|
+
puts "\n"
|
199
|
+
end
|
200
|
+
|
201
|
+
def clear_screen
|
202
|
+
# Reset cursor and clear entire screen
|
203
|
+
print "\033[H\033[2J" # Move to home position and clear screen
|
204
|
+
print "\033[3J" # Clear scrollback buffer
|
205
|
+
|
206
|
+
# Reserve bottom line for timer status
|
207
|
+
term_height = `tput lines`.to_i rescue 24
|
208
|
+
|
209
|
+
# Move to bottom of screen and clear status line
|
210
|
+
print "\e[#{term_height};1H"
|
211
|
+
print "\e[K"
|
212
|
+
print "\e[H" # Move cursor back to home position
|
213
|
+
|
214
|
+
STDOUT.flush
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
# Helper method to print a countdown timer status in the window title
|
219
|
+
# This doesn't interfere with the terminal content
|
220
|
+
def update_title_timer(seconds)
|
221
|
+
# Use terminal escape sequence to update window title
|
222
|
+
# This is widely supported and doesn't interfere with content
|
223
|
+
print "\033]0;Git Game Show - #{seconds} seconds remaining\007"
|
224
|
+
STDOUT.flush
|
225
|
+
end
|
226
|
+
|
227
|
+
# Super simple ordering implementation with minimal screen updates
|
228
|
+
def handle_ordering_question(options, question_text = nil)
|
229
|
+
# Create a copy of the options that we can modify
|
230
|
+
current_order = options.dup
|
231
|
+
cursor_index = 0
|
232
|
+
selected_index = nil
|
233
|
+
num_options = current_order.size
|
234
|
+
question_text ||= "Put these commits in chronological order (oldest to newest)"
|
235
|
+
|
236
|
+
# Extract question data if available
|
237
|
+
data = Thread.current[:question_data] || {}
|
238
|
+
question_number = data['question_number']
|
239
|
+
total_questions = data['total_questions']
|
240
|
+
|
241
|
+
# Draw the initial screen once
|
242
|
+
# system('clear') || system('cls')
|
243
|
+
|
244
|
+
# Draw question header once
|
245
|
+
if question_number && total_questions
|
246
|
+
puts "\n ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓".colorize(:cyan)
|
247
|
+
puts " ┃#{"QUESTION #{question_number} of #{total_questions}".center(45)}┃".colorize(:cyan)
|
248
|
+
puts " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛".colorize(:cyan)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Draw the main question text once
|
252
|
+
puts "\n #{question_text}".colorize(:light_blue)
|
253
|
+
puts " Put in order from oldest (1) to newest (#{num_options})".colorize(:light_blue)
|
254
|
+
|
255
|
+
# Draw instructions once
|
256
|
+
puts "\n INSTRUCTIONS:".colorize(:yellow)
|
257
|
+
puts " • Use ↑/↓ arrows to move cursor".colorize(:white)
|
258
|
+
puts " • Press ENTER to select/deselect an item to move".colorize(:white)
|
259
|
+
puts " • Selected items move with cursor when you press ↑/↓".colorize(:white)
|
260
|
+
puts " • Navigate to Submit and press ENTER when finished".colorize(:white)
|
261
|
+
|
262
|
+
# Draw the header for the list content once
|
263
|
+
puts "\n CURRENT ORDER:".colorize(:light_blue)
|
264
|
+
|
265
|
+
# Calculate where the list content starts on screen
|
266
|
+
content_start_line = question_number ? 15 : 12
|
267
|
+
|
268
|
+
# Draw the list content (this will be redrawn repeatedly)
|
269
|
+
draw_ordering_list(current_order, cursor_index, selected_index, content_start_line, num_options)
|
270
|
+
|
271
|
+
# Initialize variables
|
272
|
+
|
273
|
+
# Main interaction loop
|
274
|
+
loop do
|
275
|
+
# Read a single keypress
|
276
|
+
char = read_char
|
277
|
+
|
278
|
+
# Clear any message on this line
|
279
|
+
move_cursor_to(content_start_line + num_options + 2, 0)
|
280
|
+
print "\r\033[K"
|
281
|
+
|
282
|
+
# Check if the timer has expired
|
283
|
+
if @timer_expired
|
284
|
+
# If timer expired, just return the current ordering
|
285
|
+
return current_order
|
286
|
+
end
|
287
|
+
|
288
|
+
# Now char is an integer (ASCII code)
|
289
|
+
case char
|
290
|
+
when 13, 10 # Enter key (CR or LF)
|
291
|
+
if cursor_index == num_options
|
292
|
+
# Submit the answer
|
293
|
+
# Move to end of list and print a message
|
294
|
+
move_cursor_to(content_start_line + num_options + 3, 0)
|
295
|
+
print "\r\033[K"
|
296
|
+
print " Submitting your answer...".colorize(:green)
|
297
|
+
return current_order
|
298
|
+
elsif selected_index == cursor_index
|
299
|
+
# Deselect the currently selected item
|
300
|
+
selected_index = nil
|
301
|
+
else
|
302
|
+
# Select the item at cursor position
|
303
|
+
selected_index = cursor_index
|
304
|
+
end
|
305
|
+
when 65, 107, 119 # Up arrow (65='A'), k (107), w (119)
|
306
|
+
# Move cursor up
|
307
|
+
if selected_index == cursor_index && cursor_index > 0
|
308
|
+
# Move the selected item up in the order
|
309
|
+
current_order[cursor_index], current_order[cursor_index - 1] =
|
310
|
+
current_order[cursor_index - 1], current_order[cursor_index]
|
311
|
+
cursor_index -= 1
|
312
|
+
selected_index = cursor_index
|
313
|
+
elsif cursor_index > 0
|
314
|
+
# Just move the cursor up
|
315
|
+
cursor_index -= 1
|
316
|
+
end
|
317
|
+
when 66, 106, 115 # Down arrow (66='B'), j (106), s (115)
|
318
|
+
if selected_index == cursor_index && cursor_index < num_options - 1
|
319
|
+
# Move the selected item down in the order
|
320
|
+
current_order[cursor_index], current_order[cursor_index + 1] =
|
321
|
+
current_order[cursor_index + 1], current_order[cursor_index]
|
322
|
+
cursor_index += 1
|
323
|
+
selected_index = cursor_index
|
324
|
+
elsif cursor_index < num_options
|
325
|
+
# Just move the cursor down
|
326
|
+
cursor_index += 1
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# Redraw just the list portion of the screen
|
331
|
+
draw_ordering_list(current_order, cursor_index, selected_index, content_start_line, num_options)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Helper method to draw just the list portion of the ordering UI
|
336
|
+
def draw_ordering_list(items, cursor_index, selected_index, start_line, num_options)
|
337
|
+
# Clear the line above the list (was used for debugging)
|
338
|
+
debug_line = start_line - 1
|
339
|
+
move_cursor_to(debug_line, 0)
|
340
|
+
print "\r\033[K" # Clear debug line
|
341
|
+
|
342
|
+
# Move cursor to the start position for the list
|
343
|
+
move_cursor_to(start_line, 0)
|
344
|
+
|
345
|
+
# Clear all lines that will contain list items and the submit button
|
346
|
+
(num_options + 2).times do |i|
|
347
|
+
move_cursor_to(start_line + i, 0)
|
348
|
+
print "\r\033[K" # Clear current line without moving cursor
|
349
|
+
end
|
350
|
+
|
351
|
+
# Draw each item with appropriate highlighting
|
352
|
+
items.each_with_index do |item, idx|
|
353
|
+
# Calculate the line for this item
|
354
|
+
item_line = start_line + idx
|
355
|
+
move_cursor_to(item_line, 0)
|
356
|
+
|
357
|
+
if selected_index == idx
|
358
|
+
# Selected item (being moved)
|
359
|
+
print " → #{idx + 1}. #{item}".colorize(:light_green)
|
360
|
+
elsif cursor_index == idx
|
361
|
+
# Cursor is on this item
|
362
|
+
print " → #{idx + 1}. #{item}".colorize(:light_blue)
|
363
|
+
else
|
364
|
+
# Normal item
|
365
|
+
print " #{idx + 1}. #{item}".colorize(:white)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Add the Submit option at the bottom
|
370
|
+
move_cursor_to(start_line + num_options, 0)
|
371
|
+
if cursor_index == num_options
|
372
|
+
print " → Submit Answer".colorize(:yellow)
|
373
|
+
else
|
374
|
+
print " Submit Answer".colorize(:white)
|
375
|
+
end
|
376
|
+
|
377
|
+
# Move cursor after the list
|
378
|
+
move_cursor_to(start_line + num_options + 1, 0)
|
379
|
+
|
380
|
+
# Ensure output is visible
|
381
|
+
STDOUT.flush
|
382
|
+
end
|
383
|
+
|
384
|
+
# Helper to position cursor at a specific row/column
|
385
|
+
def move_cursor_to(row, col)
|
386
|
+
print "\033[#{row};#{col}H"
|
387
|
+
end
|
388
|
+
|
389
|
+
# Simplified key input reader that uses numbers for arrow keys
|
390
|
+
def read_char
|
391
|
+
begin
|
392
|
+
system("stty raw -echo")
|
393
|
+
|
394
|
+
# Read a character
|
395
|
+
c = STDIN.getc
|
396
|
+
|
397
|
+
# Special handling for escape sequences
|
398
|
+
if c == "\e"
|
399
|
+
# Could be an arrow key - read more
|
400
|
+
begin
|
401
|
+
# Check if there's more data to read
|
402
|
+
if IO.select([STDIN], [], [], 0.1)
|
403
|
+
c2 = STDIN.getc
|
404
|
+
if c2 == "["
|
405
|
+
# This is an arrow key or similar control sequence
|
406
|
+
if IO.select([STDIN], [], [], 0.1)
|
407
|
+
c3 = STDIN.getc
|
408
|
+
case c3
|
409
|
+
when 'A' then return 65 # Up arrow (ASCII 'A')
|
410
|
+
when 'B' then return 66 # Down arrow (ASCII 'B')
|
411
|
+
when 'C' then return 67 # Right arrow
|
412
|
+
when 'D' then return 68 # Left arrow
|
413
|
+
else
|
414
|
+
return c3.ord # Other control character
|
415
|
+
end
|
416
|
+
end
|
417
|
+
else
|
418
|
+
return c2.ord # ESC followed by another key
|
419
|
+
end
|
420
|
+
end
|
421
|
+
rescue => e
|
422
|
+
# Just return ESC if there's an error
|
423
|
+
return 27 # ESC key
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Just return the ASCII value for the key
|
428
|
+
return c.ord
|
429
|
+
ensure
|
430
|
+
system("stty -raw echo")
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Non-blocking key input reader that supports timeouts
|
435
|
+
def read_char_with_timeout
|
436
|
+
begin
|
437
|
+
# Check if there's input data available
|
438
|
+
if IO.select([STDIN], [], [], 0.1)
|
439
|
+
# Read a character
|
440
|
+
c = STDIN.getc
|
441
|
+
|
442
|
+
# Handle nil case (EOF)
|
443
|
+
return nil if c.nil?
|
444
|
+
|
445
|
+
# Special handling for escape sequences
|
446
|
+
if c == "\e"
|
447
|
+
# Could be an arrow key - read more
|
448
|
+
begin
|
449
|
+
# Check if there's more data to read
|
450
|
+
if IO.select([STDIN], [], [], 0.1)
|
451
|
+
c2 = STDIN.getc
|
452
|
+
if c2 == "["
|
453
|
+
# This is an arrow key or similar control sequence
|
454
|
+
if IO.select([STDIN], [], [], 0.1)
|
455
|
+
c3 = STDIN.getc
|
456
|
+
case c3
|
457
|
+
when 'A' then return 65 # Up arrow (ASCII 'A')
|
458
|
+
when 'B' then return 66 # Down arrow (ASCII 'B')
|
459
|
+
when 'C' then return 67 # Right arrow
|
460
|
+
when 'D' then return 68 # Left arrow
|
461
|
+
else
|
462
|
+
return c3.ord # Other control character
|
463
|
+
end
|
464
|
+
end
|
465
|
+
else
|
466
|
+
return c2.ord # ESC followed by another key
|
467
|
+
end
|
468
|
+
end
|
469
|
+
rescue => e
|
470
|
+
# Just return ESC if there's an error
|
471
|
+
return 27 # ESC key
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# Just return the ASCII value for the key
|
476
|
+
return c.ord
|
477
|
+
end
|
478
|
+
|
479
|
+
# No input available - return nil for timeout
|
480
|
+
return nil
|
481
|
+
rescue => e
|
482
|
+
# In case of error, return nil
|
483
|
+
return nil
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# Helper method to display countdown using a status bar at the bottom of the screen
|
488
|
+
def update_countdown_display(seconds, original_seconds)
|
489
|
+
# Get terminal dimensions
|
490
|
+
term_height = `tput lines`.to_i rescue 24
|
491
|
+
|
492
|
+
# Calculate a simple progress bar
|
493
|
+
total_width = 30
|
494
|
+
progress_width = ((seconds.to_f / original_seconds) * total_width).to_i
|
495
|
+
remaining_width = total_width - progress_width
|
496
|
+
|
497
|
+
# Choose color based on time remaining
|
498
|
+
color = if seconds <= 5
|
499
|
+
:red
|
500
|
+
elsif seconds <= 10
|
501
|
+
:light_yellow
|
502
|
+
else
|
503
|
+
:green
|
504
|
+
end
|
505
|
+
|
506
|
+
# Create status bar with progress indicator
|
507
|
+
bar = "[#{"█" * progress_width}#{" " * remaining_width}]"
|
508
|
+
status_text = " ⏱️ Time remaining: #{seconds.to_s.rjust(2)} seconds ".colorize(color) + bar
|
509
|
+
|
510
|
+
# Save cursor position
|
511
|
+
print "\e7"
|
512
|
+
|
513
|
+
# Move to bottom of screen (status line)
|
514
|
+
print "\e[#{term_height};1H"
|
515
|
+
|
516
|
+
# Clear the line
|
517
|
+
print "\e[K"
|
518
|
+
|
519
|
+
# Print status bar at bottom of screen
|
520
|
+
print status_text
|
521
|
+
|
522
|
+
# Restore cursor position
|
523
|
+
print "\e8"
|
524
|
+
STDOUT.flush
|
525
|
+
end
|
526
|
+
|
527
|
+
def handle_game_start(data)
|
528
|
+
@game_state = :playing
|
529
|
+
@players = data['players']
|
530
|
+
@total_rounds = data['rounds']
|
531
|
+
|
532
|
+
clear_screen
|
533
|
+
|
534
|
+
# Display a fun "Game Starting" animation
|
535
|
+
puts "\n\n"
|
536
|
+
puts " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓".colorize(:green)
|
537
|
+
puts " ┃ GAME STARTING... ┃".colorize(:green)
|
538
|
+
puts " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛".colorize(:green)
|
539
|
+
puts "\n\n"
|
540
|
+
|
541
|
+
puts " Total rounds: #{@total_rounds}".colorize(:cyan)
|
542
|
+
puts " Players: #{@players.join(', ')}".colorize(:cyan)
|
543
|
+
puts "\n\n"
|
544
|
+
puts " Get ready for the first round!".colorize(:yellow)
|
545
|
+
puts "\n\n"
|
546
|
+
end
|
547
|
+
|
548
|
+
def handle_player_update(data)
|
549
|
+
# Update the players list
|
550
|
+
@players = data['players']
|
551
|
+
|
552
|
+
if @game_state == :lobby
|
553
|
+
# If we're in the lobby, refresh the waiting room UI with updated player list
|
554
|
+
display_waiting_room
|
555
|
+
|
556
|
+
# Show notification at the bottom
|
557
|
+
if data['type'] == 'player_joined'
|
558
|
+
puts "\n 🟢 #{data['name']} has joined the game".colorize(:green)
|
559
|
+
else
|
560
|
+
puts "\n 🔴 #{data['name']} has left the game".colorize(:yellow)
|
561
|
+
end
|
562
|
+
else
|
563
|
+
# During gameplay, just show a notification without disrupting the game UI
|
564
|
+
terminal_width = `tput cols`.to_i rescue 80
|
565
|
+
|
566
|
+
# Create a notification box that won't interfere with ongoing gameplay
|
567
|
+
puts "\n┏#{"━" * (terminal_width - 2)}┓".colorize(:cyan)
|
568
|
+
|
569
|
+
if data['type'] == 'player_joined'
|
570
|
+
puts "┃#{" 🟢 #{data['name']} has joined the game ".center(terminal_width - 2)}┃".colorize(:green)
|
571
|
+
else
|
572
|
+
puts "┃#{" 🔴 #{data['name']} has left the game ".center(terminal_width - 2)}┃".colorize(:yellow)
|
573
|
+
end
|
574
|
+
|
575
|
+
# Don't show all players during gameplay - can be too disruptive
|
576
|
+
# Just show the total count
|
577
|
+
puts "┃#{" Total players: #{data['players'].size} ".center(terminal_width - 2)}┃".colorize(:cyan)
|
578
|
+
puts "┗#{"━" * (terminal_width - 2)}┛".colorize(:cyan)
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
def handle_round_start(data)
|
583
|
+
clear_screen
|
584
|
+
|
585
|
+
# Draw a fancy round header
|
586
|
+
round_num = data['round']
|
587
|
+
total_rounds = data['total_rounds']
|
588
|
+
mini_game = data['mini_game']
|
589
|
+
description = data['description']
|
590
|
+
|
591
|
+
puts "\n\n"
|
592
|
+
|
593
|
+
# Box is drawn with exactly 45 "━" characters for the top and bottom borders
|
594
|
+
# The top and bottom including borders are 48 characters wide
|
595
|
+
box_top = " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"
|
596
|
+
box_bottom = " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
|
597
|
+
|
598
|
+
# Get the text to center
|
599
|
+
round_text = "ROUND #{round_num} of #{total_rounds}"
|
600
|
+
|
601
|
+
# Find exact box width by measuring the top border
|
602
|
+
box_width = box_top.length # Should be 48 with Unicode characters
|
603
|
+
|
604
|
+
# The inner width is the box width minus the borders
|
605
|
+
inner_width = box_width - (" ┃".length + "┃".length)
|
606
|
+
|
607
|
+
# Simply use Ruby's built-in center method for reliable centering
|
608
|
+
box_middle = " ┃" + round_text.center(inner_width) + "┃"
|
609
|
+
|
610
|
+
# Output the box
|
611
|
+
puts box_top.colorize(:green)
|
612
|
+
puts box_middle.colorize(:green)
|
613
|
+
puts box_bottom.colorize(:green)
|
614
|
+
puts "\n"
|
615
|
+
puts " Mini-game: #{mini_game}".colorize(:cyan)
|
616
|
+
puts " #{description}".colorize(:light_blue)
|
617
|
+
puts "\n"
|
618
|
+
|
619
|
+
# Count down to the start - don't sleep here as we're waiting for the server
|
620
|
+
# to send us the questions after a fixed delay
|
621
|
+
puts " Get ready for the first question...".colorize(:yellow)
|
622
|
+
puts " Questions will appear automatically when the game begins.".colorize(:yellow)
|
623
|
+
puts " The host is controlling the timing of all questions.".colorize(:light_blue)
|
624
|
+
puts "\n\n"
|
625
|
+
end
|
626
|
+
|
627
|
+
def handle_question(data)
|
628
|
+
# Invalidate any previous timer
|
629
|
+
@current_timer_id = SecureRandom.uuid
|
630
|
+
|
631
|
+
# Clear the screen completely
|
632
|
+
clear_screen
|
633
|
+
|
634
|
+
question_num = data['question_number']
|
635
|
+
total_questions = data['total_questions']
|
636
|
+
question = data['question']
|
637
|
+
timeout = data['timeout']
|
638
|
+
|
639
|
+
# Store question data in thread-local storage for access in other methods
|
640
|
+
Thread.current[:question_data] = data
|
641
|
+
|
642
|
+
# No need to reserve space for timer - it will be at the bottom of the screen
|
643
|
+
|
644
|
+
# Display question header
|
645
|
+
puts "\n"
|
646
|
+
|
647
|
+
# Draw a simple box for the question header
|
648
|
+
box_top = " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"
|
649
|
+
box_bottom = " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
|
650
|
+
question_text = "QUESTION #{question_num} of #{total_questions}"
|
651
|
+
inner_width = box_top.length - (" ┃".length + "┃".length)
|
652
|
+
box_middle = " ┃" + question_text.center(inner_width) + "┃"
|
653
|
+
|
654
|
+
# Output the question box
|
655
|
+
puts box_top.colorize(:cyan)
|
656
|
+
puts box_middle.colorize(:cyan)
|
657
|
+
puts box_bottom.colorize(:cyan)
|
658
|
+
puts "\n"
|
659
|
+
|
660
|
+
# Display question
|
661
|
+
puts " #{question}".colorize(:light_blue)
|
662
|
+
|
663
|
+
# Display commit info if available
|
664
|
+
if data['commit_info']
|
665
|
+
puts "\n Commit: #{data['commit_info']}".colorize(:yellow)
|
666
|
+
end
|
667
|
+
puts "\n"
|
668
|
+
|
669
|
+
# Create a unique timer ID for this question
|
670
|
+
timer_id = SecureRandom.uuid
|
671
|
+
@current_timer_id = timer_id
|
672
|
+
start_time = Time.now
|
673
|
+
end_time = start_time + timeout
|
674
|
+
|
675
|
+
# Static deadline info
|
676
|
+
puts " Deadline: #{end_time.strftime('%I:%M:%S %p')}".colorize(:light_blue)
|
677
|
+
puts "\n"
|
678
|
+
|
679
|
+
# Initialize remaining time for scoring
|
680
|
+
@time_remaining = timeout
|
681
|
+
|
682
|
+
# Update the timer display immediately
|
683
|
+
update_countdown_display(timeout, timeout)
|
684
|
+
|
685
|
+
# Variable to track if the timer has expired
|
686
|
+
@timer_expired = false
|
687
|
+
|
688
|
+
# Start countdown in a background thread with new approach
|
689
|
+
countdown_thread = Thread.new do
|
690
|
+
begin
|
691
|
+
remaining = timeout
|
692
|
+
|
693
|
+
while remaining > 0 && @current_timer_id == timer_id
|
694
|
+
# Update both window title and fixed position display
|
695
|
+
update_title_timer(remaining)
|
696
|
+
update_countdown_display(remaining, timeout)
|
697
|
+
|
698
|
+
# Sound alert when time is almost up (< 5 seconds)
|
699
|
+
if remaining < 5 && remaining > 0
|
700
|
+
print "\a" if remaining % 2 == 0 # Beep on even seconds
|
701
|
+
end
|
702
|
+
|
703
|
+
# Store time for scoring
|
704
|
+
@time_remaining = remaining
|
705
|
+
|
706
|
+
# Wait one second
|
707
|
+
sleep 1
|
708
|
+
remaining -= 1
|
709
|
+
end
|
710
|
+
|
711
|
+
# Final update when timer reaches zero
|
712
|
+
if @current_timer_id == timer_id
|
713
|
+
update_countdown_display(0, timeout)
|
714
|
+
|
715
|
+
# IMPORTANT: Send a timeout answer when time expires
|
716
|
+
# without waiting for user input
|
717
|
+
@timer_expired = true
|
718
|
+
|
719
|
+
# Clear the screen to break out of any prompt/UI state
|
720
|
+
clear_screen
|
721
|
+
|
722
|
+
puts "\n ⏰ TIME'S UP! Timeout answer submitted.".colorize(:red)
|
723
|
+
puts " Waiting for the next question...".colorize(:yellow)
|
724
|
+
|
725
|
+
# Force terminal back to normal mode in case something is waiting for input
|
726
|
+
system("stty sane") rescue nil
|
727
|
+
system("tput cnorm") rescue nil # Re-enable cursor
|
728
|
+
|
729
|
+
# Send a timeout answer to the server
|
730
|
+
send_message({
|
731
|
+
type: MessageType::ANSWER,
|
732
|
+
name: name,
|
733
|
+
answer: nil, # nil indicates timeout
|
734
|
+
question_id: data['question_id']
|
735
|
+
})
|
736
|
+
|
737
|
+
# Force kill other input methods by returning directly from handle_question
|
738
|
+
# This breaks out of the entire method, bypassing any pending input operations
|
739
|
+
return
|
740
|
+
end
|
741
|
+
rescue => e
|
742
|
+
# Silent failure for robustness
|
743
|
+
end
|
744
|
+
end
|
745
|
+
|
746
|
+
# Handle different question types - but wrap in a separate thread
|
747
|
+
# so that timeouts can interrupt the UI
|
748
|
+
input_thread = Thread.new do
|
749
|
+
if data['question_type'] == 'ordering'
|
750
|
+
# Special UI for ordering questions
|
751
|
+
answer = handle_ordering_question(data['options'], data['question'])
|
752
|
+
elsif data['options'] && !data['options'].empty?
|
753
|
+
# Regular multiple choice question - with interrupt check
|
754
|
+
begin
|
755
|
+
# Configure prompt to be interruptible
|
756
|
+
answer = @prompt.select(" Choose your answer:", data['options'], per_page: 10) do |menu|
|
757
|
+
# Check for timeout periodically during menu interactions
|
758
|
+
menu.help ""
|
759
|
+
menu.default 1
|
760
|
+
end
|
761
|
+
rescue TTY::Reader::InputInterrupt
|
762
|
+
# If interrupted, just return nil
|
763
|
+
nil
|
764
|
+
end
|
765
|
+
else
|
766
|
+
# Free text answer - with interrupt check
|
767
|
+
begin
|
768
|
+
answer = @prompt.ask(" Your answer:") do |q|
|
769
|
+
# Check for timeout periodically
|
770
|
+
q.help ""
|
771
|
+
end
|
772
|
+
rescue TTY::Reader::InputInterrupt
|
773
|
+
# If interrupted, just return nil
|
774
|
+
nil
|
775
|
+
end
|
776
|
+
end
|
777
|
+
end
|
778
|
+
|
779
|
+
# Wait for input but with timeout
|
780
|
+
answer = nil
|
781
|
+
begin
|
782
|
+
# Try to join the thread but allow for interruption
|
783
|
+
Timeout.timeout(timeout + 0.5) do
|
784
|
+
answer = input_thread.value
|
785
|
+
end
|
786
|
+
rescue Timeout::Error
|
787
|
+
# If timeout occurs during join, kill the thread
|
788
|
+
input_thread.kill if input_thread.alive?
|
789
|
+
end
|
790
|
+
|
791
|
+
# Only send user answer if timer hasn't expired
|
792
|
+
unless @timer_expired
|
793
|
+
# Send answer back to server
|
794
|
+
send_message({
|
795
|
+
type: MessageType::ANSWER,
|
796
|
+
name: name,
|
797
|
+
answer: answer,
|
798
|
+
question_id: data['question_id']
|
799
|
+
})
|
800
|
+
|
801
|
+
puts "\n Answer submitted! Waiting for feedback...".colorize(:green)
|
802
|
+
end
|
803
|
+
|
804
|
+
# Stop the timer by invalidating its ID and terminating the thread
|
805
|
+
@current_timer_id = SecureRandom.uuid # Change timer ID to signal thread to stop
|
806
|
+
countdown_thread.kill if countdown_thread.alive? # Force kill the thread
|
807
|
+
|
808
|
+
# Reset window title
|
809
|
+
print "\033]0;Git Game Show\007"
|
810
|
+
|
811
|
+
# Clear the timer status line at bottom
|
812
|
+
term_height = `tput lines`.to_i rescue 24
|
813
|
+
print "\e7" # Save cursor position
|
814
|
+
print "\e[#{term_height};1H" # Move to bottom line
|
815
|
+
print "\e[K" # Clear line
|
816
|
+
print "\e8" # Restore cursor position
|
817
|
+
|
818
|
+
# The server will send ANSWER_FEEDBACK message right away, then we'll see feedback
|
819
|
+
end
|
820
|
+
|
821
|
+
# Handle immediate feedback after submitting an answer
|
822
|
+
def handle_answer_feedback(data)
|
823
|
+
# Invalidate any running timer and reset window title
|
824
|
+
@current_timer_id = SecureRandom.uuid
|
825
|
+
print "\033]0;Git Game Show\007" # Reset window title
|
826
|
+
|
827
|
+
# Clear the timer status line at bottom
|
828
|
+
term_height = `tput lines`.to_i rescue 24
|
829
|
+
print "\e7" # Save cursor position
|
830
|
+
print "\e[#{term_height};1H" # Move to bottom line
|
831
|
+
print "\e[K" # Clear line
|
832
|
+
print "\e8" # Restore cursor position
|
833
|
+
|
834
|
+
# Don't clear screen, just display the feedback under the question
|
835
|
+
# This keeps the context of the question while showing the result
|
836
|
+
|
837
|
+
# Add a visual separator
|
838
|
+
puts "\n #{"─" * 40}".colorize(:light_black)
|
839
|
+
puts "\n"
|
840
|
+
|
841
|
+
# Show immediate feedback
|
842
|
+
if data['answer'] == "TIMEOUT"
|
843
|
+
# Special handling for timeouts
|
844
|
+
puts " ⏰ TIME'S UP! You didn't answer in time.".colorize(:red)
|
845
|
+
puts " The correct answer was: #{data['correct_answer']}".colorize(:yellow)
|
846
|
+
puts " (0 points)".colorize(:light_black)
|
847
|
+
elsif data['correct']
|
848
|
+
# Correct answer
|
849
|
+
points_text = data['points'] > 0 ? " (+#{data['points']} points)" : ""
|
850
|
+
puts " ✅ CORRECT! Your answer was correct: #{data['answer']}#{points_text}".colorize(:green)
|
851
|
+
|
852
|
+
# Show bonus points details if applicable
|
853
|
+
if data['points'] > 10 # More than base points
|
854
|
+
bonus = data['points'] - 10
|
855
|
+
puts " 🎉 SPEED BONUS: +#{bonus} points for fast answer!".colorize(:light_yellow)
|
856
|
+
end
|
857
|
+
else
|
858
|
+
# Incorrect answer
|
859
|
+
puts " ❌ INCORRECT! The correct answer was: #{data['correct_answer']}".colorize(:red)
|
860
|
+
puts " You answered: #{data['answer']} (0 points)".colorize(:yellow)
|
861
|
+
end
|
862
|
+
|
863
|
+
puts "\n Waiting for the round to complete. Please wait for the next question...".colorize(:light_blue)
|
864
|
+
end
|
865
|
+
|
866
|
+
# Handle round results showing all players' answers
|
867
|
+
def handle_round_result(data)
|
868
|
+
# Invalidate any running timer and reset window title
|
869
|
+
@current_timer_id = SecureRandom.uuid
|
870
|
+
print "\033]0;Git Game Show - Round Results\007" # Reset window title with context
|
871
|
+
|
872
|
+
# Start with a clean screen
|
873
|
+
clear_screen
|
874
|
+
|
875
|
+
puts "\n"
|
876
|
+
|
877
|
+
# Box is drawn with exactly 45 "━" characters for the top and bottom borders
|
878
|
+
# The top and bottom including borders are 48 characters wide
|
879
|
+
box_top = " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"
|
880
|
+
box_bottom = " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
|
881
|
+
|
882
|
+
# Get the text to center
|
883
|
+
result_text = "ROUND RESULTS"
|
884
|
+
|
885
|
+
# Find exact box width by measuring the top border
|
886
|
+
box_width = box_top.length # Should be 48 with Unicode characters
|
887
|
+
|
888
|
+
# The inner width is the box width minus 2 characters for the borders
|
889
|
+
inner_width = box_width - (" ┃".length + "┃".length)
|
890
|
+
|
891
|
+
# Simply use Ruby's built-in center method for reliable centering
|
892
|
+
box_middle = " ┃" + result_text.center(inner_width) + "┃"
|
893
|
+
|
894
|
+
# Output the box
|
895
|
+
puts box_top.colorize(:cyan)
|
896
|
+
puts box_middle.colorize(:cyan)
|
897
|
+
puts box_bottom.colorize(:cyan)
|
898
|
+
puts "\n"
|
899
|
+
|
900
|
+
# Show question again
|
901
|
+
puts " Question: #{data['question'][:question]}".colorize(:light_blue)
|
902
|
+
puts " Correct answer: #{data['correct_answer']}".colorize(:green)
|
903
|
+
|
904
|
+
puts "\n All player results:".colorize(:cyan)
|
905
|
+
|
906
|
+
# Debug data temporarily removed
|
907
|
+
|
908
|
+
# Handle results based on structure
|
909
|
+
if data['results'].is_a?(Hash)
|
910
|
+
data['results'].each do |player, result|
|
911
|
+
# Ensure result is a hash with the expected keys
|
912
|
+
if result.is_a?(Hash)
|
913
|
+
# Check if 'correct' is a boolean or check string equality if it's a string
|
914
|
+
correct = result[:correct] || result['correct'] || false
|
915
|
+
answer = result[:answer] || result['answer'] || "No answer"
|
916
|
+
points = result[:points] || result['points'] || 0
|
917
|
+
|
918
|
+
status = correct ? "✓" : "✗"
|
919
|
+
points_str = "(+#{points} points)"
|
920
|
+
player_str = player == name ? "#{player} (You)" : player
|
921
|
+
|
922
|
+
player_output = " #{player_str.ljust(20)} #{points_str.ljust(15)} #{answer} #{status}"
|
923
|
+
if correct
|
924
|
+
puts player_output.colorize(:green)
|
925
|
+
else
|
926
|
+
puts player_output.colorize(:red)
|
927
|
+
end
|
928
|
+
else
|
929
|
+
# Fallback for unexpected result format
|
930
|
+
puts " #{player}: #{result.inspect}".colorize(:yellow)
|
931
|
+
end
|
932
|
+
end
|
933
|
+
else
|
934
|
+
# Fallback message if results isn't a hash
|
935
|
+
puts " No detailed results available".colorize(:yellow)
|
936
|
+
end
|
937
|
+
|
938
|
+
# Display current scoreboard
|
939
|
+
if data['scores']
|
940
|
+
puts "\n Current Standings:".colorize(:yellow)
|
941
|
+
data['scores'].each_with_index do |(player, score), index|
|
942
|
+
player_str = player == name ? "#{player} (You)" : player
|
943
|
+
rank = index + 1
|
944
|
+
|
945
|
+
# Add medal emoji for top 3
|
946
|
+
rank_display = case rank
|
947
|
+
when 1 then "🥇"
|
948
|
+
when 2 then "🥈"
|
949
|
+
when 3 then "🥉"
|
950
|
+
else "#{rank}."
|
951
|
+
end
|
952
|
+
|
953
|
+
output = " #{rank_display} #{player_str.ljust(20)} #{score} points"
|
954
|
+
|
955
|
+
if player == name
|
956
|
+
puts output.colorize(:light_yellow)
|
957
|
+
else
|
958
|
+
puts output.colorize(:light_blue)
|
959
|
+
end
|
960
|
+
end
|
961
|
+
end
|
962
|
+
|
963
|
+
puts "\n Next question coming up automatically...".colorize(:yellow)
|
964
|
+
end
|
965
|
+
|
966
|
+
def handle_scoreboard(data)
|
967
|
+
# Invalidate any running timer and reset window title
|
968
|
+
@current_timer_id = SecureRandom.uuid
|
969
|
+
print "\033]0;Git Game Show - Scoreboard\007" # Reset window title with context
|
970
|
+
|
971
|
+
# Always start with a clean screen for the scoreboard
|
972
|
+
clear_screen
|
973
|
+
|
974
|
+
puts "\n"
|
975
|
+
puts " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓".colorize(:yellow)
|
976
|
+
puts " ┃ SCOREBOARD ┃".colorize(:yellow)
|
977
|
+
puts " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛".colorize(:yellow)
|
978
|
+
puts "\n"
|
979
|
+
|
980
|
+
# Get player positions
|
981
|
+
position = 1
|
982
|
+
last_score = nil
|
983
|
+
|
984
|
+
data['scores'].each do |player, score|
|
985
|
+
# Determine position (handle ties)
|
986
|
+
position = data['scores'].values.index(score) + 1 if last_score != score
|
987
|
+
last_score = score
|
988
|
+
|
989
|
+
# Highlight current player
|
990
|
+
player_str = player == name ? "#{player} (You)" : player
|
991
|
+
|
992
|
+
# Format with position
|
993
|
+
position_str = "#{position}."
|
994
|
+
score_str = "#{score} points"
|
995
|
+
|
996
|
+
# Add emoji for top 3
|
997
|
+
case position
|
998
|
+
when 1
|
999
|
+
position_str = "🥇 #{position_str}"
|
1000
|
+
puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_yellow)
|
1001
|
+
when 2
|
1002
|
+
position_str = "🥈 #{position_str}"
|
1003
|
+
puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_blue)
|
1004
|
+
when 3
|
1005
|
+
position_str = "🥉 #{position_str}"
|
1006
|
+
puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_magenta)
|
1007
|
+
else
|
1008
|
+
puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}"
|
1009
|
+
end
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
puts "\n Next round coming up soon...".colorize(:cyan)
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def handle_game_end(data)
|
1016
|
+
# Invalidate any running timer and reset window title
|
1017
|
+
@current_timer_id = SecureRandom.uuid
|
1018
|
+
print "\033]0;Git Game Show - Game Over\007" # Reset window title with context
|
1019
|
+
|
1020
|
+
# Clear any timer status line at the bottom
|
1021
|
+
term_height = `tput lines`.to_i rescue 24
|
1022
|
+
print "\e7" # Save cursor position
|
1023
|
+
print "\e[#{term_height};1H" # Move to bottom line
|
1024
|
+
print "\e[K" # Clear line
|
1025
|
+
print "\e8" # Restore cursor position
|
1026
|
+
|
1027
|
+
# Completely clear the screen
|
1028
|
+
clear_screen
|
1029
|
+
@game_state = :ended
|
1030
|
+
|
1031
|
+
winner = data['winner']
|
1032
|
+
|
1033
|
+
# ASCII trophy art
|
1034
|
+
trophy = <<-TROPHY
|
1035
|
+
___________
|
1036
|
+
'._==_==_=_.'
|
1037
|
+
.-\\: /-.
|
1038
|
+
| (|:. |) |
|
1039
|
+
'-|:. |-'
|
1040
|
+
\\::. /
|
1041
|
+
'::. .'
|
1042
|
+
) (
|
1043
|
+
_.' '._
|
1044
|
+
TROPHY
|
1045
|
+
|
1046
|
+
puts "\n\n"
|
1047
|
+
puts trophy.colorize(:yellow)
|
1048
|
+
puts "\n"
|
1049
|
+
puts " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓".colorize(:green)
|
1050
|
+
puts " ┃ GAME OVER ┃".colorize(:green)
|
1051
|
+
puts " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛".colorize(:green)
|
1052
|
+
puts "\n"
|
1053
|
+
|
1054
|
+
winner_is_you = winner == name
|
1055
|
+
if winner_is_you
|
1056
|
+
puts " 🎉 Congratulations! You won! 🎉".colorize(:light_yellow)
|
1057
|
+
else
|
1058
|
+
puts " Winner: #{winner}! 🏆".colorize(:light_yellow)
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
puts "\n Final Scores:".colorize(:cyan)
|
1062
|
+
|
1063
|
+
# Get player positions
|
1064
|
+
position = 1
|
1065
|
+
last_score = nil
|
1066
|
+
|
1067
|
+
data['scores'].each do |player, score|
|
1068
|
+
# Determine position (handle ties)
|
1069
|
+
position = data['scores'].values.index(score) + 1 if last_score != score
|
1070
|
+
last_score = score
|
1071
|
+
|
1072
|
+
# Highlight current player
|
1073
|
+
player_str = player == name ? "#{player} (You)" : player
|
1074
|
+
|
1075
|
+
# Format with position
|
1076
|
+
position_str = "#{position}."
|
1077
|
+
score_str = "#{score} points"
|
1078
|
+
|
1079
|
+
# Add emoji for top 3
|
1080
|
+
case position
|
1081
|
+
when 1
|
1082
|
+
position_str = "🥇 #{position_str}"
|
1083
|
+
puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_yellow)
|
1084
|
+
when 2
|
1085
|
+
position_str = "🥈 #{position_str}"
|
1086
|
+
puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_blue)
|
1087
|
+
when 3
|
1088
|
+
position_str = "🥉 #{position_str}"
|
1089
|
+
puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_magenta)
|
1090
|
+
else
|
1091
|
+
puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}"
|
1092
|
+
end
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
puts "\n\n Thanks for playing Git Game Show!".colorize(:green)
|
1096
|
+
puts " Waiting for the host to start a new game...".colorize(:cyan)
|
1097
|
+
puts " Press Ctrl+C to exit, or wait for the next game".colorize(:light_black)
|
1098
|
+
|
1099
|
+
# Keep client ready to receive a new game start or reset message
|
1100
|
+
@game_over_timer = Thread.new do
|
1101
|
+
begin
|
1102
|
+
loop do
|
1103
|
+
# Just keep waiting for host to start a new game
|
1104
|
+
# The client will receive GAME_START or GAME_RESET when the host takes action
|
1105
|
+
sleep 1
|
1106
|
+
end
|
1107
|
+
rescue => e
|
1108
|
+
# Silence any errors in the waiting thread
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
end
|
1112
|
+
|
1113
|
+
# Add a special method to handle game reset notifications
|
1114
|
+
def handle_game_reset(data)
|
1115
|
+
# Stop the game over timer if it's running
|
1116
|
+
@game_over_timer&.kill if @game_over_timer&.alive?
|
1117
|
+
|
1118
|
+
# Reset game state
|
1119
|
+
@game_state = :lobby
|
1120
|
+
|
1121
|
+
# Clear any lingering state
|
1122
|
+
@players = @players || [] # Keep existing players list if we have one
|
1123
|
+
|
1124
|
+
# Show the waiting room again
|
1125
|
+
clear_screen
|
1126
|
+
display_waiting_room
|
1127
|
+
|
1128
|
+
# Show a prominent message that we're back in waiting room mode
|
1129
|
+
puts "\n 🔄 The game has been reset by the host. Waiting for a new game to start...".colorize(:cyan)
|
1130
|
+
puts " You can play again or press Ctrl+C to exit.".colorize(:cyan)
|
1131
|
+
end
|
1132
|
+
|
1133
|
+
def handle_chat(data)
|
1134
|
+
puts "[#{data['sender']}]: #{data['message']}".colorize(:light_blue)
|
1135
|
+
end
|
1136
|
+
|
1137
|
+
def send_message(message)
|
1138
|
+
begin
|
1139
|
+
@ws.send(message.to_json)
|
1140
|
+
rescue => e
|
1141
|
+
puts "Error sending message: #{e.message}".colorize(:red)
|
1142
|
+
end
|
1143
|
+
end
|
1144
|
+
end
|
1145
|
+
end
|