git_game_show 0.1.3 → 0.1.4

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