Spaces:
Runtime error
Runtime error
| # player_interface.py | |
| import gradio as gr | |
| import pandas as pd | |
| from quiz_manager import quiz_manager # Assuming quiz_manager exists and is accessible | |
| class PlayerInterface: | |
| def __init__(self): | |
| self.current_game_pin = None | |
| self.current_question_index = -1 | |
| self.current_quiz_data = None | |
| self.player_name = None | |
| self.total_questions = 0 | |
| # Modified join_game to return updates for the Group visibility | |
| def join_game(self, game_pin, player_name): | |
| """Validate the game pin and allow player to join.""" | |
| print(f"Attempting to join game: Pin={game_pin}, Player={player_name}") | |
| if not game_pin or not game_pin.strip() or not player_name or not player_name.strip(): | |
| print("JOIN_GAME: Empty pin or name") | |
| # Return outputs for staying on the join screen | |
| return ( | |
| "Please enter both Game Pin and your Name.", # Join Status | |
| gr.update(visible=True), # Show Join Group | |
| gr.update(visible=False), # Hide Game Area | |
| gr.update(visible=False), # Hide Leaderboard Area | |
| gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown | |
| gr.update(visible=False), # Hide Next Button Row | |
| gr.update(value="") # Clear Question Display | |
| ) | |
| try: | |
| # Check if quiz exists | |
| quiz = quiz_manager.get_quiz(game_pin.strip()) | |
| if not quiz: | |
| print(f"JOIN_GAME: Invalid pin: {game_pin}") | |
| # Return outputs for staying on the join screen with error | |
| return ( | |
| f"Invalid game pin: {game_pin.strip()}. Please try again.", | |
| gr.update(visible=True), # Show Join Group | |
| gr.update(visible=False), # Hide Game Area | |
| gr.update(visible=False), # Hide Leaderboard Area | |
| gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown | |
| gr.update(visible=False), # Hide Next Button Row | |
| gr.update(value="") # Clear Question Display | |
| ) | |
| self.current_game_pin = game_pin.strip() | |
| self.player_name = player_name.strip() | |
| self.current_quiz_data = quiz_manager.quizzes.get(self.current_game_pin) | |
| # Initialize player score if they are new to this quiz | |
| quiz_manager.initialize_player_score(self.current_game_pin, self.player_name) | |
| self.total_questions = len(self.current_quiz_data.get('questions', [])) | |
| question_numbers = list(range(1, self.total_questions + 1)) | |
| print(f"JOIN_GAME: Success for {self.player_name} in {self.current_game_pin}. Total questions: {self.total_questions}") | |
| # Return updates to hide Join Group, show Game Area, show Leaderboard Area, and update dropdown | |
| return ( | |
| f"Joined game {self.current_game_pin} successfully, {self.player_name}!", # Join Status | |
| gr.update(visible=False), # Hide Join Group | |
| gr.update(visible=True), # Show Game Area | |
| gr.update(visible=True), # Show Leaderboard Area | |
| # Update choices, make visible if questions exist, reset value | |
| gr.update(choices=question_numbers, visible=self.total_questions > 0, value=None), | |
| gr.update(visible=self.total_questions > 0), # Show Next Button Row only if questions exist | |
| gr.update(value="Click 'Start Quiz' to begin.") # Set initial text for Question Display | |
| ) | |
| except Exception as e: | |
| print(f"JOIN_GAME: Error - {str(e)}") | |
| # Return updates for all relevant components on error, stay on join screen | |
| return ( | |
| f"Error joining game: {str(e)}", | |
| gr.update(visible=True), # Show Join Group | |
| gr.update(visible=False), # Hide Game Area | |
| gr.update(visible=False), # Hide Leaderboard Area | |
| gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown | |
| gr.update(visible=False), # Hide Next Button Row | |
| gr.update(value="") # Clear Question Display | |
| ) | |
| def show_question(self): | |
| """Prepare updates for displaying the current question.""" | |
| print("SHOW_QUESTION: Called") | |
| # Prepare default updates for components | |
| default_options_update = gr.update(choices=[], value=None, visible=False) | |
| default_prompt_label_update = gr.update(value="", visible=False) | |
| default_question_area_update = gr.update(visible=True) # Assume area itself is visible when showing a question | |
| default_submit_btn_update = gr.update(visible=True) | |
| default_answer_output_update = gr.update(value="") | |
| if not self.current_quiz_data or self.current_question_index == -1: | |
| print("SHOW_QUESTION: No quiz data or invalid index") | |
| return ( | |
| "Error: Could not load question.", | |
| default_options_update, | |
| default_prompt_label_update, | |
| gr.update(visible=False), # Hide question area if no data | |
| gr.update(visible=False), # Hide submit button if no data | |
| gr.update(value="") | |
| ) | |
| questions = self.current_quiz_data.get('questions', []) | |
| if not questions or self.current_question_index < 0 or self.current_question_index >= len(questions): | |
| print("SHOW_QUESTION: Invalid question data or index out of bounds") | |
| return ( | |
| "Error loading question data.", | |
| default_options_update, | |
| default_prompt_label_update, | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(value="") | |
| ) | |
| question_data = questions[self.current_question_index] | |
| options = question_data.get('options', []) | |
| question_text = question_data.get('text', 'Error loading question text') | |
| print(f"SHOW_QUESTION: Displaying question - index: {self.current_question_index}, text: {question_text}") | |
| # Return specific updates for the current question | |
| return ( | |
| f"**Question {self.current_question_index + 1}/{self.total_questions}:** {question_text}", # Update question_display value with bolding | |
| gr.update(choices=options, value=None, label="Answer Choices", visible=True), # Update options_radio, make visible | |
| gr.update(value="Select your answer:", visible=True), # Update prompt label, make visible | |
| gr.update(visible=True), # Make question_area visible | |
| gr.update(visible=True), # Make submit_btn visible | |
| gr.update(value="") # Clear answer_output value | |
| ) | |
| # Modified methods to return updates for dropdown and next button row visibility | |
| # next_step returns 8 outputs | |
| def next_step(self): | |
| """Starts the quiz, showing the first question.""" | |
| print("NEXT_STEP: Called (Start Quiz)") | |
| if self.current_quiz_data and self.total_questions > 0: | |
| self.current_question_index = 0 | |
| question_updates = self.show_question() # 6 outputs | |
| print(f"NEXT_STEP: Showing question - index: {self.current_question_index}") | |
| return ( | |
| *question_updates, # Unpack the 6 updates | |
| gr.update(visible=False), # Hide Next Button Row | |
| gr.update(value=self.current_question_index + 1) # Update dropdown value | |
| ) | |
| else: | |
| print("NEXT_STEP: No questions available") | |
| # Return default updates if no questions, hide game elements | |
| return ( | |
| "No questions available for this quiz.", # Question Display (1) | |
| gr.update(choices=[], value=None, label="Answer Choices", visible=False), # Options Radio (2) | |
| gr.update(value="", visible=False), # Prompt Label (3) | |
| gr.update(visible=False), # Question Area (4) | |
| gr.update(visible=False), # Submit Button (5) | |
| gr.update(value=""), # Answer Output (6) | |
| gr.update(visible=False), # Hide Next Button Row (7) | |
| gr.update(value=None) # Clear dropdown value (8) | |
| ) | |
| # next_question returns 8 outputs | |
| def next_question(self): | |
| """Move to the next question.""" | |
| print("NEXT_QUESTION: Called") | |
| if not self.current_quiz_data or not self.current_game_pin or not self.player_name: | |
| print("NEXT_QUESTION: No quiz data or player context") | |
| return ( | |
| "Please join a game first.", # Question Display (1) | |
| gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) | |
| gr.update(value="", visible=False), # (3) | |
| gr.update(visible=False), # (4) | |
| gr.update(visible=False), # (5) | |
| gr.update(value=""), # (6) | |
| gr.update(visible=True), # Keep Next Button Row visible if no quiz data (7) | |
| gr.update(value=None) # Clear dropdown value (8) | |
| ) | |
| questions = self.current_quiz_data.get('questions', []) | |
| if self.current_question_index < len(questions) - 1: | |
| self.current_question_index += 1 | |
| question_updates = self.show_question() # 6 outputs | |
| print(f"NEXT_QUESTION: Showing question - index: {self.current_question_index}") | |
| return ( | |
| *question_updates, | |
| gr.update(visible=False), # Keep Next Button Row hidden (7) | |
| gr.update(value=self.current_question_index + 1) # Update dropdown value (8) | |
| ) | |
| else: | |
| print("NEXT_QUESTION: Quiz finished") | |
| # Reset instance variables | |
| self.current_question_index = -1 | |
| self.current_quiz_data = None | |
| self.current_game_pin = None | |
| self.player_name = None | |
| self.total_questions = 0 | |
| # Return updates for quiz finished state, hide game elements, show join box needs separate trigger | |
| return ( | |
| "Quiz finished! You can join another game.", # Question Display (1) | |
| gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) | |
| gr.update(value="", visible=False), # (3) | |
| gr.update(visible=False), # (4) | |
| gr.update(visible=False), # (5) | |
| gr.update(value=""), # (6) | |
| gr.update(visible=False), # Hide Next Button Row (7) | |
| gr.update(value=None) # Clear dropdown value (8) | |
| # Note: Join Box, Game Area, Leaderboard Area visibility needs to be handled by a separate action or logic | |
| # A 'Play Again' or 'Join New Game' button in the finished state might be better UI. | |
| ) | |
| # previous_question returns 8 outputs | |
| def previous_question(self): | |
| """Move to the previous question.""" | |
| print("PREVIOUS_QUESTION: Called") | |
| if not self.current_quiz_data or not self.current_game_pin or not self.player_name: | |
| print("PREVIOUS_QUESTION: No quiz data or player context") | |
| return ( | |
| "Please join a game first.", # Question Display (1) | |
| gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) | |
| gr.update(value="", visible=False), # (3) | |
| gr.update(visible=False), # (4) | |
| gr.update(visible=False), # (5) | |
| gr.update(value=""), # (6) | |
| gr.update(visible=True), # Keep Next Button Row visible if no quiz data (7) | |
| gr.update(value=None) # Clear dropdown value (8) | |
| ) | |
| if self.current_question_index > 0: | |
| self.current_question_index -= 1 | |
| question_updates = self.show_question() # 6 outputs | |
| print(f"PREVIOUS_QUESTION: Showing question - index: {self.current_question_index}") | |
| return ( | |
| *question_updates, | |
| gr.update(visible=False), # Keep Next Button Row hidden (7) | |
| gr.update(value=self.current_question_index + 1) # Update dropdown value (8) | |
| ) | |
| else: | |
| print("PREVIOUS_QUESTION: First question") | |
| # show_question already sets visibility for elements in question_area | |
| question_updates = self.show_question() # 6 outputs for current question (index 0) | |
| return ( | |
| *question_updates, # Includes showing question area elements | |
| gr.update(visible=False), # Keep Next Button Row hidden (7) | |
| gr.update(value=self.current_question_index + 1) # Keep dropdown value at 1 (8) | |
| ) | |
| # select_question_by_number returns 8 outputs | |
| def select_question_by_number(self, question_number): | |
| """Select a specific question by its number.""" | |
| print(f"SELECT_QUESTION_BY_NUMBER: Called with {question_number}") | |
| # Handle case where question_number might be None from dropdown reset or initial state | |
| if question_number is None: | |
| print("SELECT_QUESTION_BY_NUMBER: question_number is None") | |
| # Return outputs to clear question area, hide game elements | |
| return ( | |
| "", # Clear question text (1) | |
| gr.update(choices=[], value=None, label="Answer Choices", visible=False), # Clear options (2) | |
| gr.update(value="", visible=False), # Clear prompt label (3) | |
| gr.update(visible=False), # Hide question area (4) | |
| gr.update(visible=False), # Hide submit button (5) | |
| gr.update(value=""), # Clear answer output (6) | |
| gr.update(visible=False), # Hide Next Button Row (7) | |
| gr.update(value=None) # Clear dropdown value (8) | |
| ) | |
| if not self.current_quiz_data or not self.current_game_pin or not self.player_name: | |
| print("SELECT_QUESTION_BY_NUMBER: No quiz data or player context") | |
| # Return default updates, hide game elements | |
| return ( | |
| "Please join a game first.", # (1) | |
| gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) | |
| gr.update(value="", visible=False), # (3) | |
| gr.update(visible=False), # (4) | |
| gr.update(visible=False), # (5) | |
| gr.update(value=""), # (6) | |
| gr.update(visible=False), # (7) | |
| gr.update(value=None) # (8) | |
| ) | |
| questions = self.current_quiz_data.get('questions', []) | |
| # Convert question_number to integer index | |
| try: | |
| index = int(question_number) - 1 | |
| except (ValueError, TypeError): | |
| print("SELECT_QUESTION_BY_NUMBER: Invalid number input") | |
| return ( | |
| "Invalid input for question number.", # (1) | |
| gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) | |
| gr.update(value="", visible=False), # (3) | |
| gr.update(visible=False), # (4) | |
| gr.update(visible=False), # (5) | |
| gr.update(value=""), # (6) | |
| gr.update(visible=False), # (7) | |
| gr.update(value=None) # (8) | |
| ) | |
| if 0 <= index < len(questions): | |
| self.current_question_index = index | |
| question_updates = self.show_question() # 6 outputs | |
| print(f"SELECT_QUESTION_BY_NUMBER: Showing question - index: {self.current_question_index}") | |
| # Append updates for the dropdown (keep value) and keep next button row hidden | |
| return ( | |
| *question_updates, | |
| gr.update(visible=False), # Keep Next Button Row hidden (7) | |
| gr.update(value=self.current_question_index + 1) # Keep dropdown value (8) | |
| ) | |
| else: | |
| print("SELECT_QUESTION_BY_NUMBER: Invalid question number") | |
| # Return updates for invalid number, hide game elements | |
| return ( | |
| "Invalid question number.", # (1) | |
| gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) | |
| gr.update(value="", visible=False), # (3) | |
| gr.update(visible=False), # (4) | |
| gr.update(visible=False), # (5) | |
| gr.update(value=""), # (6) | |
| gr.update(visible=False), # (7) | |
| gr.update(value=None) # (8) | |
| ) | |
| def submit_answer(self, selected_option): | |
| """Submit the player's answer for the current question.""" | |
| if not self.current_quiz_data or self.current_question_index == -1 or not self.current_game_pin or not self.player_name: | |
| return "Please join a game and select a question first." | |
| # Ensure selected_option is not None if the user submits without selecting | |
| if selected_option is None: | |
| return "Please select an answer." | |
| # Fetch quiz data again to ensure it's fresh before checking answer | |
| quiz = quiz_manager.get_quiz(self.current_game_pin) | |
| if not quiz or 'questions' not in quiz or self.current_question_index < 0 or self.current_question_index >= len(quiz['questions']): | |
| return "Error submitting answer: Could not retrieve current question data." | |
| current_question = quiz['questions'][self.current_question_index] | |
| correct_answer = current_question.get('correct_answer') | |
| # Handle cases where correct_answer might be missing | |
| if correct_answer is None: | |
| return "Error submitting answer: Correct answer not defined for this question." | |
| is_correct = selected_option.strip() == correct_answer.strip() | |
| try: | |
| quiz_manager.record_answer(self.current_game_pin, self.player_name, is_correct) | |
| except Exception as e: | |
| return f"Error recording score: {str(e)}" | |
| return "Correct!" if is_correct else f"Incorrect. The correct answer was: {correct_answer}" | |
| def get_leaderboard(self): | |
| """Retrieve and display the leaderboard.""" | |
| print("GET_LEADERBOARD: Called") | |
| # This method is called by the Refresh button *inside* the leaderboard_area. | |
| # It relies on self.current_game_pin being set by the join_game method. | |
| if not self.current_game_pin: | |
| # This case should ideally not happen if UI flow is correct, but for safety | |
| return "Please join a game first to view the leaderboard." | |
| try: | |
| leaderboard = quiz_manager.get_leaderboard(self.current_game_pin) | |
| if not leaderboard: | |
| print("GET_LEADERBOARD: No scores recorded") | |
| return "No scores recorded yet." | |
| leaderboard_str = "### Leaderboard:\n" | |
| # Sort leaderboard by score in descending order | |
| try: | |
| # get_leaderboard in quiz_manager returns a sorted list of (name, data) tuples | |
| sorted_leaderboard = leaderboard # It's already sorted by quiz_manager | |
| for i, (name, data) in enumerate(sorted_leaderboard, 1): | |
| score = data.get('score', 0) # Safely get score from the dict | |
| leaderboard_str += f"{i}. {name}: {score} points\n" | |
| print("GET_LEADERBOARD: Leaderboard generated") | |
| return leaderboard_str | |
| except (TypeError, IndexError, AttributeError) as e: | |
| # Catch potential errors if the list elements are not in the expected format | |
| print(f"GET_LEADERBOARD: Error processing leaderboard data - {str(e)}") | |
| # Display raw data for debugging | |
| return f"Error displaying leaderboard data: {str(e)}.\nRaw data: {leaderboard}" | |
| except Exception as e: | |
| return f"Error retrieving leaderboard: {str(e)}" | |
| def create_player_interface(self): | |
| """Create the Gradio interface for players with a step-by-step layout.""" | |
| with gr.Blocks() as player_interface: | |
| gr.Markdown("## Quiz Player Dashboard") | |
| # --- Define all Components First --- | |
| # Join Game Section (Visible initially) | |
| # Replaced gr.Box with gr.Group | |
| join_group = gr.Group(visible=True) | |
| with join_group: | |
| gr.Markdown("### Join Game") | |
| with gr.Row(): | |
| game_pin_input = gr.Textbox(label="Enter Game Pin", scale=1) | |
| player_name_input = gr.Textbox(label="Your Name", scale=1) | |
| join_btn = gr.Button("Join Game", scale=1) | |
| join_output = gr.Textbox(label="Join Status", interactive=False) | |
| # Game Area (Hidden initially, shown after joining) | |
| game_area = gr.Column(visible=False) | |
| with game_area: | |
| gr.Markdown("### Play Quiz") | |
| # Initial Next Button (to start the quiz) - part of game_area but controlled separately | |
| next_button_row = gr.Row(visible=False) # Controlled visible/hidden | |
| with next_button_row: | |
| next_btn = gr.Button("Start Quiz") | |
| # Question Dropdown (visibility controlled dynamically) | |
| question_dropdown = gr.Dropdown(label="Select Question", choices=[], visible=False, interactive=True) | |
| # Question Display Area (Hidden initially, shown after starting/selecting question) | |
| question_area = gr.Column(visible=False) # Controlled visible/hidden | |
| with question_area: | |
| question_display = gr.Markdown(label="Question Text") | |
| options_prompt_label = gr.Label(label="", visible=False) | |
| options_radio = gr.Radio(label="Answer Choices", visible=False) | |
| submit_btn = gr.Button("Submit Answer", visible=False) | |
| answer_output = gr.Textbox(label="Answer Result", interactive=False) | |
| # Navigation Buttons | |
| with gr.Row(): | |
| prev_question_btn = gr.Button("Previous Question") | |
| next_question_btn = gr.Button("Next Question") | |
| # Leaderboard Area (Hidden initially, shown after joining) | |
| leaderboard_area = gr.Column(visible=False) # Controlled visible/hidden | |
| with leaderboard_area: | |
| gr.Markdown("### Current Leaderboard") | |
| leaderboard_output = gr.Markdown(label="Leaderboard Results") | |
| get_leaderboard_btn = gr.Button("Refresh Leaderboard") | |
| # --- Define all Interactions After Components --- | |
| # Join Game interaction | |
| # join_game returns 7 outputs: status, join_group_vis, game_area_vis, leaderboard_area_vis, dropdown_update, next_btn_row_vis, question_display_initial_text | |
| join_btn.click( | |
| fn=self.join_game, | |
| inputs=[game_pin_input, player_name_input], | |
| outputs=[ | |
| join_output, | |
| join_group, # Update visibility of join group | |
| game_area, # Update visibility of game area | |
| leaderboard_area, # Update visibility of leaderboard area | |
| question_dropdown, # Update dropdown choices/visibility | |
| next_button_row, # Update visibility of next button row | |
| question_display # Update initial text of question display | |
| ] | |
| ) | |
| # Start Quiz button interaction (uses next_step to show the first question) | |
| # next_step returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value | |
| next_btn.click( | |
| fn=self.next_step, | |
| inputs=None, # next_step uses instance variables | |
| outputs=[ | |
| question_display, | |
| options_radio, | |
| options_prompt_label, | |
| question_area, | |
| submit_btn, | |
| answer_output, | |
| next_button_row, | |
| question_dropdown # Update dropdown value | |
| ] | |
| ) | |
| # Question Dropdown interaction | |
| # select_question_by_number returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value | |
| question_dropdown.change( | |
| fn=self.select_question_by_number, | |
| inputs=question_dropdown, # Pass the selected value (the question number) | |
| outputs=[ | |
| question_display, | |
| options_radio, | |
| options_prompt_label, | |
| question_area, | |
| submit_btn, | |
| answer_output, | |
| next_button_row, # Ensure next button row is hidden after selection | |
| question_dropdown # Keep dropdown value updated | |
| ] | |
| ) | |
| # Next Question button interaction | |
| # next_question returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value | |
| next_question_btn.click( | |
| fn=self.next_question, | |
| inputs=None, # Uses instance variables | |
| outputs=[ | |
| question_display, | |
| options_radio, | |
| options_prompt_label, | |
| question_area, | |
| submit_btn, | |
| answer_output, | |
| next_button_row, | |
| question_dropdown # Update dropdown value | |
| ] | |
| ) | |
| # Previous Question button interaction | |
| # previous_question returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value | |
| prev_question_btn.click( | |
| fn=self.previous_question, | |
| inputs=None, # Uses instance variables | |
| outputs=[ | |
| question_display, | |
| options_radio, | |
| options_prompt_label, | |
| question_area, | |
| submit_btn, | |
| answer_output, | |
| next_button_row, | |
| question_dropdown # Update dropdown value | |
| ] | |
| ) | |
| # Submit Answer button interaction | |
| submit_btn.click( | |
| fn=self.submit_answer, | |
| inputs=options_radio, # Input is the selected value from the radio button | |
| outputs=answer_output, # Output is the result of the submission | |
| ) | |
| # Refresh Leaderboard button interaction | |
| get_leaderboard_btn.click( | |
| fn=self.get_leaderboard, | |
| inputs=None, # Uses instance variables | |
| outputs=leaderboard_output, # Output is the formatted leaderboard string | |
| ) | |
| # No specific load interaction needed here, join_game handles initial visibility. | |
| return player_interface | |
| # --- How to use this class --- | |
| # Create the PlayerInterface instance | |
| player_interface_instance = PlayerInterface() | |
| # To access the Gradio interface object: | |
| # The app.py file will call this method to get the interface block | |
| player_gradio_interface = player_interface_instance.create_player_interface() | |
| # You can now use player_gradio_interface in your main app.py file. |