git_game_show 0.2.1 → 0.2.2

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.
@@ -0,0 +1,18 @@
1
+ module GitGameShow
2
+ # Message types for client-server communication
3
+ module MessageType
4
+ JOIN_REQUEST = 'join_request'
5
+ JOIN_RESPONSE = 'join_response'
6
+ PLAYER_JOINED = 'player_joined'
7
+ PLAYER_LEFT = 'player_left'
8
+ GAME_START = 'game_start'
9
+ GAME_END = 'game_end'
10
+ GAME_RESET = 'game_reset'
11
+ QUESTION = 'question'
12
+ ANSWER = 'answer'
13
+ ANSWER_FEEDBACK = 'answer_feedback'
14
+ ROUND_RESULT = 'round_result'
15
+ SCOREBOARD = 'scoreboard'
16
+ CHAT = 'chat'
17
+ end
18
+ end
@@ -0,0 +1,241 @@
1
+ module GitGameShow
2
+ # Handles WebSocket messages
3
+ class MessageHandler
4
+ def initialize(player_manager, game_state, renderer, server_handler)
5
+ @player_manager = player_manager
6
+ @game_state = game_state
7
+ @renderer = renderer
8
+ @server_handler = server_handler
9
+ @password = nil # Will be set through setter
10
+ end
11
+
12
+ def set_password(password)
13
+ @password = password
14
+ end
15
+
16
+ def handle_message(ws, msg)
17
+ begin
18
+ data = JSON.parse(msg)
19
+ case data['type']
20
+ when MessageType::JOIN_REQUEST
21
+ handle_join_request(ws, data)
22
+ when MessageType::ANSWER
23
+ handle_answer(data)
24
+ when MessageType::CHAT
25
+ @server_handler.broadcast_message(data)
26
+ else
27
+ @renderer.log_message("Unknown message type: #{data['type']}", :red)
28
+ end
29
+ rescue JSON::ParserError => e
30
+ @renderer.log_message("Invalid message format: #{e.message}", :red)
31
+ rescue => e
32
+ @renderer.log_message("Error processing message: #{e.message}", :red)
33
+ end
34
+ end
35
+
36
+ def handle_player_disconnect(ws)
37
+ # Find the player who disconnected
38
+ player_name = @player_manager.find_player_by_ws(ws)
39
+ return unless player_name
40
+
41
+ # Remove the player
42
+ @player_manager.remove_player(player_name)
43
+
44
+ # Update the sidebar to reflect the player leaving
45
+ @server_handler.instance_variable_get(:@sidebar)&.update_player_list(@player_manager.player_names, @player_manager.scores)
46
+
47
+ # Log message for player leaving
48
+ @renderer.log_message("🔴 #{player_name} has left the game", :yellow)
49
+
50
+ # Notify other players
51
+ @server_handler.broadcast_message({
52
+ type: 'player_left',
53
+ name: player_name,
54
+ players: @player_manager.player_names
55
+ })
56
+ end
57
+
58
+ def broadcast(json_message, exclude = nil)
59
+ @player_manager.players.each do |player_name, ws|
60
+ # Skip excluded player if specified
61
+ next if exclude && player_name == exclude
62
+
63
+ # Skip nil websockets
64
+ next unless ws
65
+
66
+ # Send with error handling for each individual player
67
+ begin
68
+ ws.send(json_message)
69
+ rescue => e
70
+ @renderer.log_message("Error sending to #{player_name}: #{e.message}", :yellow)
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def handle_join_request(ws, data)
78
+ player_name = data['name']
79
+ sent_password = data['password']
80
+
81
+ response = {
82
+ type: MessageType::JOIN_RESPONSE
83
+ }
84
+
85
+ # Check if game is already in progress
86
+ if @game_state.playing?
87
+ response.merge!(success: false, message: "Game is already in progress")
88
+ # Validate password
89
+ elsif sent_password != @password
90
+ response.merge!(success: false, message: "Incorrect password")
91
+ # Check for duplicate names
92
+ elsif @player_manager.player_exists?(player_name)
93
+ response.merge!(success: false, message: "Player name already taken")
94
+ else
95
+ # Add player to the game
96
+ @player_manager.add_player(player_name, ws)
97
+
98
+ # Update the sidebar to show the new player
99
+ @server_handler.instance_variable_get(:@sidebar)&.update_player_list(@player_manager.player_names, @player_manager.scores)
100
+
101
+ # Include current player list in the response
102
+ response.merge!(
103
+ success: true,
104
+ message: "Successfully joined the game",
105
+ players: @player_manager.player_names
106
+ )
107
+
108
+ # Notify all existing players about the new player
109
+ @server_handler.broadcast_message({
110
+ type: 'player_joined',
111
+ name: player_name,
112
+ players: @player_manager.player_names
113
+ }, exclude: player_name)
114
+
115
+ # Log message for player joining
116
+ @renderer.log_message("🟢 #{player_name} has joined the game", :green)
117
+ end
118
+
119
+ ws.send(response.to_json)
120
+ end
121
+
122
+ def handle_answer(data)
123
+ return unless @game_state.playing?
124
+
125
+ player_name = data['name']
126
+ answer = data['answer']
127
+ question_id = data['question_id']
128
+
129
+ # Make sure the answer is for the current question
130
+ return unless question_id == @game_state.current_question_id
131
+
132
+ # Don't allow duplicate answers
133
+ return if @game_state.player_answers.dig(player_name, :answered)
134
+
135
+ # Calculate time taken to answer
136
+ time_taken = Time.now - @game_state.question_start_time
137
+
138
+ # Get current question
139
+ current_question = @game_state.current_question
140
+
141
+ # Handle nil answer (timeout) differently
142
+ points = 0
143
+
144
+ if answer.nil?
145
+ # For timeouts, set a special "TIMEOUT" answer with 0 points
146
+ @game_state.record_player_answer(player_name, "TIMEOUT", time_taken, false, 0)
147
+
148
+ # Send timeout feedback to the player
149
+ feedback = {
150
+ type: MessageType::ANSWER_FEEDBACK,
151
+ answer: "TIMEOUT",
152
+ correct: false,
153
+ correct_answer: current_question[:correct_answer],
154
+ points: points
155
+ }
156
+ @player_manager.get_ws(player_name)&.send(feedback.to_json)
157
+
158
+ # Log the timeout
159
+ truncated_name = player_name.length > 15 ? "#{player_name[0...12]}..." : player_name
160
+ @renderer.log_message("#{truncated_name} timed out after #{time_taken.round(2)}s ⏰", :yellow)
161
+ else
162
+ # Regular answer processing
163
+ # For ordering quizzes, we'll calculate points in evaluate_answers
164
+ if current_question[:question_type] == 'ordering'
165
+ # Just store the answer and time, points will be calculated in evaluate_answers
166
+ correct = false # Will be properly set during evaluation
167
+
168
+ # Get the mini-game to evaluate this answer for points
169
+ mini_game = @game_state.current_mini_game
170
+ points = mini_game.evaluate_answers(
171
+ current_question,
172
+ {player_name => {answer: answer, time_taken: time_taken}}
173
+ ).values.first[:points]
174
+ else
175
+ # For regular quizzes, calculate points immediately
176
+ correct = answer == current_question[:correct_answer]
177
+ points = 0
178
+
179
+ if correct
180
+ points = 10 # Base points for correct answer
181
+
182
+ # Bonus points for fast answers
183
+ if time_taken < 5
184
+ points += 5
185
+ elsif time_taken < 10
186
+ points += 3
187
+ end
188
+ end
189
+ end
190
+
191
+ # Store the answer
192
+ @game_state.record_player_answer(player_name, answer, time_taken, correct, points)
193
+
194
+ # Send immediate feedback to this player only
195
+ send_answer_feedback(player_name, answer, correct, current_question, points)
196
+
197
+ # Log this answer - ensure the name is not too long
198
+ truncated_name = player_name.length > 15 ? "#{player_name[0...12]}..." : player_name
199
+ if current_question[:question_type] == 'ordering'
200
+ @renderer.log_message("#{truncated_name} submitted ordering in #{time_taken.round(2)}s ⏱️", :cyan)
201
+ else
202
+ @renderer.log_message("#{truncated_name} answered in #{time_taken.round(2)}s: #{correct ? "Correct ✓" : "Wrong ✗"}", correct ? :green : :red)
203
+ end
204
+ end
205
+
206
+ # Check if all players have answered
207
+ check_all_answered
208
+ end
209
+
210
+ def send_answer_feedback(player_name, answer, correct, question, points=0)
211
+ # Send feedback only to the player who answered
212
+ ws = @player_manager.get_ws(player_name)
213
+ return unless ws
214
+
215
+ feedback = {
216
+ type: MessageType::ANSWER_FEEDBACK,
217
+ answer: answer,
218
+ correct: correct,
219
+ correct_answer: question[:correct_answer],
220
+ points: points # Include points in the feedback
221
+ }
222
+
223
+ # For ordering quizzes, we can't determine correctness immediately
224
+ if question[:question_type] == 'ordering'
225
+ feedback[:correct] = nil # nil means "scoring in progress"
226
+ # Keep the points value that was calculated earlier
227
+ feedback[:message] = "Ordering submitted. Points calculated based on your ordering."
228
+ end
229
+
230
+ ws.send(feedback.to_json)
231
+ end
232
+
233
+ def check_all_answered
234
+ # If all players have answered, log it but WAIT for the full timeout
235
+ if @game_state.player_answers.keys.size == @player_manager.player_count
236
+ timeout_sec = GitGameShow::DEFAULT_CONFIG[:question_timeout]
237
+ @renderer.log_message("All players have answered - waiting for timeout (#{timeout_sec}s)", :cyan)
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,52 @@
1
+ module GitGameShow
2
+ # WebSocket server management
3
+ class Server
4
+ attr_reader :port
5
+
6
+ def initialize(port, message_handler)
7
+ @port = port
8
+ @message_handler = message_handler
9
+ end
10
+
11
+ def start
12
+ WebSocket::EventMachine::Server.start(host: '0.0.0.0', port: @port) do |ws|
13
+ ws.onopen do
14
+ # Connection is logged when a player successfully joins
15
+ end
16
+
17
+ ws.onmessage do |msg|
18
+ @message_handler.handle_message(ws, msg)
19
+ end
20
+
21
+ ws.onclose do
22
+ @message_handler.handle_player_disconnect(ws)
23
+ end
24
+ end
25
+ end
26
+
27
+ def broadcast_message(message, exclude: nil)
28
+ return if message.nil?
29
+
30
+ begin
31
+ # Convert message to JSON safely
32
+ json_message = nil
33
+ begin
34
+ json_message = message.to_json
35
+ rescue => e
36
+ # Try to simplify the message to make it JSON-compatible
37
+ simplified_message = {
38
+ type: message[:type] || "unknown",
39
+ message: "Error processing full message"
40
+ }
41
+ json_message = simplified_message.to_json
42
+ end
43
+
44
+ return unless json_message
45
+
46
+ @message_handler.broadcast(json_message, exclude)
47
+ rescue => e
48
+ # Silently fail for now
49
+ end
50
+ end
51
+ end
52
+ end
@@ -147,15 +147,15 @@ module GitGameShow
147
147
  # Display instructions and welcome information
148
148
  puts "\n"
149
149
  puts " Welcome to Git Game Show!".colorize(:yellow)
150
- puts " Test your knowledge about Git and your team's commits through fun mini-games.".colorize(:light_white)
150
+ puts " Test your knowledge about Git and your team's commits through fun mini-games.".colorize(:light_black)
151
151
  puts "\n"
152
152
  puts " 🔹 Instructions:".colorize(:light_blue)
153
- puts " • The game consists of multiple rounds with different question types".colorize(:light_white)
154
- puts " • Each round has a theme based on Git commit history".colorize(:light_white)
155
- puts " • Answer questions as quickly as possible for maximum points".colorize(:light_white)
156
- puts " • The player with the most points at the end wins!".colorize(:light_white)
153
+ puts " • The game consists of multiple rounds with different question types".colorize(:light_black)
154
+ puts " • Each round has a theme based on Git commit history".colorize(:light_black)
155
+ puts " • Answer questions as quickly as possible for maximum points".colorize(:light_black)
156
+ puts " • The player with the most points at the end wins!".colorize(:light_black)
157
157
  puts "\n"
158
- puts " 🔹 Status: Waiting for the host to start the game...".colorize(:light_yellow)
158
+ puts " 🔹 Status: Waiting for the host to start the game...".colorize(:yellow)
159
159
  puts "\n"
160
160
 
161
161
  # Draw player section in a box
@@ -207,7 +207,7 @@ module GitGameShow
207
207
 
208
208
  puts "\n"
209
209
  puts " When the game starts, you'll see questions appear automatically.".colorize(:light_black)
210
- puts " Get ready to test your Git knowledge!".colorize(:light_yellow)
210
+ puts " Get ready to test your Git knowledge!".colorize(:yellow)
211
211
  puts "\n"
212
212
  end
213
213
 
@@ -508,7 +508,7 @@ module GitGameShow
508
508
  color = if seconds <= 5
509
509
  :red
510
510
  elsif seconds <= 10
511
- :light_yellow
511
+ :yellow
512
512
  else
513
513
  :green
514
514
  end
@@ -860,7 +860,7 @@ module GitGameShow
860
860
  # Show bonus points details if applicable
861
861
  if data['points'] > 10 # More than base points
862
862
  bonus = data['points'] - 10
863
- puts " 🎉 SPEED BONUS: +#{bonus} points for fast answer!".colorize(:light_yellow)
863
+ puts " 🎉 SPEED BONUS: +#{bonus} points for fast answer!".colorize(:yellow)
864
864
  end
865
865
  else
866
866
  if data['correct_answer'].is_a?(Array)
@@ -993,7 +993,7 @@ module GitGameShow
993
993
  output = " #{rank_display} #{player_str.ljust(20)} #{score} points"
994
994
 
995
995
  if player == name
996
- puts output.colorize(:light_yellow)
996
+ puts output.colorize(:yellow)
997
997
  else
998
998
  puts output.colorize(:light_blue)
999
999
  end
@@ -1038,7 +1038,7 @@ module GitGameShow
1038
1038
  case position
1039
1039
  when 1
1040
1040
  position_str = "🥇 #{position_str}"
1041
- puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_yellow)
1041
+ puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:yellow)
1042
1042
  when 2
1043
1043
  position_str = "🥈 #{position_str}"
1044
1044
  puts " #{position_str.ljust(5)} #{player_str.ljust(25)} #{score_str}".colorize(:light_blue)
@@ -1095,9 +1095,9 @@ module GitGameShow
1095
1095
 
1096
1096
  winner_is_you = winner == name
1097
1097
  if winner_is_you
1098
- puts "🎉 Congratulations! You won! 🎉".center(@game_width).colorize(:light_yellow)
1098
+ puts "🎉 Congratulations! You won! 🎉".center(@game_width).colorize(:yellow)
1099
1099
  else
1100
- puts "Winner: #{winner}! 🏆".center(@game_width).colorize(:light_yellow)
1100
+ puts "Winner: #{winner}! 🏆".center(@game_width).colorize(:yellow)
1101
1101
  end
1102
1102
 
1103
1103
  puts ""
@@ -1126,7 +1126,7 @@ module GitGameShow
1126
1126
  when 1
1127
1127
  position_str = "🥇 #{position_str}"
1128
1128
  left_string = (position_str.rjust(5) + ' ' + player_str).ljust(scores_width - score_str.length)
1129
- puts "#{left_string}#{score_str}".center(@game_width).colorize(:light_yellow)
1129
+ puts "#{left_string}#{score_str}".center(@game_width).colorize(:yellow)
1130
1130
  when 2
1131
1131
  position_str = "🥈 #{position_str}"
1132
1132
  left_string = (position_str.rjust(5) + ' ' + player_str).ljust(scores_width - score_str.length)
@@ -0,0 +1,299 @@
1
+ module GitGameShow
2
+ # Coordinates the various components of the game server
3
+ class ServerHandler
4
+ attr_reader :game_state, :player_manager, :question_manager
5
+
6
+ def initialize(port:, password:, rounds:, repo:)
7
+ @port = port
8
+ @password = password
9
+ @rounds = rounds
10
+ @repo = repo
11
+
12
+ # Initialize core components
13
+ @game_state = GameState.new(rounds)
14
+ @player_manager = PlayerManager.new
15
+ @mini_game_loader = MiniGameLoader.new
16
+
17
+ # Initialize UI components
18
+ @renderer = Renderer.new
19
+ @sidebar = Sidebar.new(@renderer)
20
+ @console = Console.new(self, @renderer)
21
+
22
+ # Initialize network components - passing self allows circular reference
23
+ @message_handler = MessageHandler.new(@player_manager, @game_state, @renderer, self)
24
+ @message_handler.set_password(password)
25
+ @server = Server.new(port, @message_handler)
26
+
27
+ # Finally initialize the question manager
28
+ @question_manager = QuestionManager.new(@game_state, @player_manager)
29
+ end
30
+
31
+ def start_with_ui(join_link = nil)
32
+ # Store join link as instance variable so it's accessible throughout the class
33
+ @join_link = join_link
34
+
35
+ # Setup UI
36
+ @renderer.setup
37
+ @renderer.draw_welcome_banner
38
+ @renderer.draw_join_link(@join_link) if @join_link
39
+ @sidebar.draw_header
40
+ @sidebar.update_player_list(@player_manager.player_names, @player_manager.scores)
41
+ @renderer.draw_command_prompt
42
+
43
+ # Start event machine
44
+ EM.run do
45
+ # Start the server
46
+ @server.start
47
+ # Setup console commands
48
+ @console.setup_command_handler
49
+ end
50
+ end
51
+
52
+ # Game lifecycle methods
53
+ def handle_start_command
54
+ if @player_manager.player_count < 1
55
+ @renderer.log_message("Need at least one player to start", :red)
56
+ return
57
+ end
58
+
59
+ # If players are in an ended state, reset them first
60
+ if @game_state.ended?
61
+ @renderer.log_message("Resetting players from previous game...", :light_black)
62
+ broadcast_message({
63
+ type: MessageType::GAME_RESET,
64
+ message: "Get ready! The host is starting a new game..."
65
+ })
66
+ # Give players a moment to see the reset message
67
+ sleep(1)
68
+ end
69
+
70
+ # Start the game
71
+ if @game_state.start_game
72
+ broadcast_message({
73
+ type: MessageType::GAME_START,
74
+ rounds: @rounds,
75
+ players: @player_manager.player_names
76
+ })
77
+
78
+ @renderer.log_message("Game started with #{@player_manager.player_count} players", :green)
79
+ start_next_round
80
+ end
81
+ end
82
+
83
+ def handle_end_command
84
+ if @game_state.playing?
85
+ @renderer.log_message("Ending game early...", :yellow)
86
+ end_game
87
+ elsif @game_state.ended?
88
+ @renderer.log_message("Game already ended. Type 'start' to begin a new game.", :yellow)
89
+ else
90
+ @renderer.log_message("No game in progress to end", :yellow)
91
+ end
92
+ end
93
+
94
+ def handle_reset_command
95
+ if @game_state.ended?
96
+ @renderer.log_message("Manually resetting all players to waiting room state...", :yellow)
97
+
98
+ # Send a game reset message to all players
99
+ broadcast_message({
100
+ type: MessageType::GAME_RESET,
101
+ message: "Game has been reset by the host. Waiting for a new game to start."
102
+ })
103
+
104
+ # Update game state
105
+ @game_state.reset_game
106
+ else
107
+ @renderer.log_message("Can only reset after a game has ended", :yellow)
108
+ end
109
+ end
110
+
111
+ def start_next_round
112
+ @game_state.start_next_round(@mini_game_loader.select_next_mini_game)
113
+
114
+ # Check if we've completed all rounds
115
+ if @game_state.current_round > @rounds
116
+ @renderer.log_message("All rounds completed! Showing final scores...", :green)
117
+ EM.next_tick { end_game } # Use next_tick to ensure it runs after current operations
118
+ return
119
+ end
120
+
121
+ # Announce new round
122
+ broadcast_message({
123
+ type: 'round_start',
124
+ round: @game_state.current_round,
125
+ total_rounds: @rounds,
126
+ mini_game: @game_state.current_mini_game.class.name,
127
+ description: @game_state.current_mini_game.class.description,
128
+ example: @game_state.current_mini_game.class.example
129
+ })
130
+
131
+ # Generate questions for this round
132
+ @question_manager.generate_questions(@repo)
133
+
134
+ @renderer.log_message("Starting round #{@game_state.current_round}: #{@game_state.current_mini_game.class.name}", :cyan)
135
+
136
+ # Start the first question after a short delay
137
+ EM.add_timer(3) do
138
+ ask_next_question
139
+ end
140
+ end
141
+
142
+ def ask_next_question
143
+ return if @game_state.current_question_index >= @game_state.round_questions.size
144
+
145
+ # Log information for debugging
146
+ @renderer.log_message("Preparing question #{@game_state.current_question_index + 1} of #{@game_state.round_questions.size}", :cyan)
147
+
148
+ # Prepare the question
149
+ @game_state.prepare_next_question
150
+ current_question = @game_state.current_question
151
+
152
+ # Get the appropriate timeout value
153
+ timeout = @question_manager.question_timeout
154
+
155
+ # Prepare question data
156
+ begin
157
+ question_data = {
158
+ type: MessageType::QUESTION,
159
+ question_id: @game_state.current_question_id.to_s,
160
+ question: current_question[:question].to_s,
161
+ options: current_question[:options] || [],
162
+ timeout: timeout,
163
+ round: @game_state.current_round.to_i,
164
+ question_number: (@game_state.current_question_index + 1).to_i,
165
+ total_questions: @game_state.round_questions.size.to_i
166
+ }
167
+
168
+ # Add additional question data safely
169
+ # Add question_type if it's a special question type (like ordering)
170
+ if current_question && current_question[:question_type]
171
+ question_data[:question_type] = current_question[:question_type].to_s
172
+ end
173
+
174
+ # Add commit info if available (for AuthorQuiz)
175
+ if current_question && current_question[:commit_info]
176
+ # Make a safe copy to avoid potential issues with the original object
177
+ if current_question[:commit_info].is_a?(Hash)
178
+ safe_commit_info = {}
179
+ current_question[:commit_info].each do |key, value|
180
+ safe_commit_info[key.to_s] = value.to_s
181
+ end
182
+ question_data[:commit_info] = safe_commit_info
183
+ else
184
+ question_data[:commit_info] = current_question[:commit_info].to_s
185
+ end
186
+ end
187
+
188
+ # Add context if available (for BlameGame)
189
+ if current_question && current_question[:context]
190
+ question_data[:context] = current_question[:context].to_s
191
+ end
192
+ rescue => e
193
+ @renderer.log_message("Error preparing question data: #{e.message}", :red)
194
+ # Create a minimal fallback question
195
+ question_data = {
196
+ type: MessageType::QUESTION,
197
+ question_id: @game_state.current_question_id.to_s,
198
+ question: "Question #{@game_state.current_question_index + 1}",
199
+ options: ["Option 1", "Option 2", "Option 3", "Option 4"],
200
+ timeout: timeout,
201
+ round: @game_state.current_round.to_i,
202
+ question_number: (@game_state.current_question_index + 1).to_i,
203
+ total_questions: @game_state.round_questions.size.to_i
204
+ }
205
+ end
206
+
207
+ # Don't log detailed question info to prevent author lists from showing
208
+ @renderer.log_message("Question #{@game_state.current_question_index + 1}/#{@game_state.round_questions.size}", :cyan)
209
+ @renderer.log_message("Broadcasting question to players...", :cyan)
210
+ broadcast_message(question_data)
211
+
212
+ # Set a timer for question timeout
213
+ EM.add_timer(timeout) do
214
+ @renderer.log_message("Question timeout (#{timeout}s) - evaluating", :yellow)
215
+ evaluate_answers
216
+ end
217
+ end
218
+
219
+ def evaluate_answers
220
+ # Delegate to question manager
221
+ evaluation = @question_manager.evaluate_answers
222
+ return unless evaluation
223
+
224
+ # Update player list in sidebar to reflect new scores
225
+ @sidebar.update_player_list(@player_manager.player_names, @player_manager.scores)
226
+
227
+ # Broadcast results to all players
228
+ broadcast_message({
229
+ type: MessageType::ROUND_RESULT,
230
+ question: evaluation[:question],
231
+ results: evaluation[:results],
232
+ correct_answer: evaluation[:question][:formatted_correct_answer] || evaluation[:question][:correct_answer],
233
+ scores: @player_manager.sorted_scores
234
+ })
235
+
236
+ # Log current scores for the host
237
+ @renderer.log_message("Current scores:", :cyan)
238
+ @player_manager.sorted_scores.each do |player, score|
239
+ truncated_name = player.length > 15 ? "#{player[0...12]}..." : player
240
+ @renderer.log_message("#{truncated_name}: #{score} points", :light_blue)
241
+ end
242
+
243
+ # Move to next question or round
244
+ @game_state.move_to_next_question
245
+
246
+ if @game_state.current_question_index >= @game_state.round_questions.size
247
+ # End of round
248
+ EM.add_timer(GitGameShow::DEFAULT_CONFIG[:transition_delay]) do
249
+ start_next_round
250
+ end
251
+ else
252
+ # Next question - use mini-game specific timing if available
253
+ display_time = @question_manager.question_display_time
254
+
255
+ @renderer.log_message("Next question in #{display_time} seconds...", :cyan)
256
+ EM.add_timer(display_time) do
257
+ ask_next_question
258
+ end
259
+ end
260
+ end
261
+
262
+ def end_game
263
+ @game_state.end_game
264
+
265
+ # Get winner and scores
266
+ winner = @player_manager.top_player
267
+ scores = @player_manager.scores
268
+
269
+ # Notify all players
270
+ broadcast_message({
271
+ type: MessageType::GAME_END,
272
+ winner: winner ? winner[0].to_s : "",
273
+ scores: @player_manager.sorted_scores
274
+ })
275
+
276
+ # Display the final results
277
+ @renderer.draw_game_over(winner, scores)
278
+
279
+ # Reset for next game
280
+ @game_state.reset_game
281
+ @player_manager.reset_scores
282
+
283
+ # Update sidebar
284
+ @sidebar.update_player_list(@player_manager.player_names, @player_manager.scores)
285
+ @renderer.log_message("Game ended! Type 'start' to play again or 'exit' to quit.", :cyan)
286
+ end
287
+
288
+ def broadcast_message(message, exclude: nil)
289
+ @server.broadcast_message(message, exclude: exclude)
290
+ end
291
+
292
+ def broadcast_scoreboard
293
+ broadcast_message({
294
+ type: MessageType::SCOREBOARD,
295
+ scores: @player_manager.sorted_scores
296
+ })
297
+ end
298
+ end
299
+ end