diff --git a/backend/models.py b/backend/models.py index 866abed..77fe378 100644 --- a/backend/models.py +++ b/backend/models.py @@ -79,6 +79,9 @@ class Question(db.Model): start_time = db.Column(db.Integer, nullable=True) # Start time in seconds end_time = db.Column(db.Integer, nullable=True) # End time in seconds + times_correct = db.Column(db.Integer, default=0, nullable=False) + times_incorrect = db.Column(db.Integer, default=0, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) @@ -109,6 +112,8 @@ class Question(db.Model): 'start_time': self.start_time, 'end_time': self.end_time, 'category': self.category, + 'times_correct': self.times_correct, + 'times_incorrect': self.times_incorrect, 'created_at': self.created_at.isoformat() if self.created_at else None, 'created_by': self.created_by, 'creator_name': (self.creator.name or self.creator.preferred_username) if self.creator else None, @@ -157,6 +162,7 @@ class Game(db.Model): current_question_index = db.Column(db.Integer, default=0) # Track current question is_template = db.Column(db.Boolean, default=False) # Mark as reusable template current_turn_team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True) # Track whose turn it is + steal_active = db.Column(db.Boolean, default=False) # True when a team got it wrong and next team can steal completed_at = db.Column(db.DateTime, nullable=True) # When game ended winners = db.Column(db.JSON, nullable=True) # [{"team_name": str, "score": int}, ...] @@ -192,6 +198,7 @@ class Game(db.Model): 'is_template': self.is_template, 'current_turn_team_id': self.current_turn_team_id, 'current_turn_team_name': self.current_turn_team.name if self.current_turn_team else None, + 'steal_active': self.steal_active, 'completed_at': self.completed_at.isoformat() if self.completed_at else None, 'winners': self.winners } diff --git a/backend/routes/admin.py b/backend/routes/admin.py index f69ed9a..0a1da38 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -304,6 +304,43 @@ def seek_audio(game_id): return jsonify({'error': str(e)}), 500 +@bp.route('/game//mark-correct', methods=['POST']) +@require_auth +def mark_correct(game_id): + """Mark current question as answered correctly by the current turn team""" + game = Game.query.get_or_404(game_id) + + try: + team = game_service.mark_correct(game, socketio) + if team: + return jsonify({'message': 'Marked correct', 'team': team.to_dict()}), 200 + else: + return jsonify({'error': 'No current question or team'}), 400 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@bp.route('/game//mark-wrong', methods=['POST']) +@require_auth +def mark_wrong(game_id): + """Mark current question as answered incorrectly by the current turn team""" + game = Game.query.get_or_404(game_id) + + try: + result = game_service.mark_wrong(game, socketio) + return jsonify({ + 'message': 'Marked wrong', + 'steal_active': result['steal_active'], + 'both_failed': result['both_failed'] + }), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + @bp.route('/game//advance-turn', methods=['POST']) @require_auth def advance_turn(game_id): diff --git a/backend/services/game_service.py b/backend/services/game_service.py index 34a387c..8a1d245 100644 --- a/backend/services/game_service.py +++ b/backend/services/game_service.py @@ -19,7 +19,8 @@ def get_game_state(game): 'is_active': game.is_active, 'teams': [team.to_dict() for team in game.teams], 'current_turn_team_id': game.current_turn_team_id, - 'current_turn_team_name': game.current_turn_team.name if game.current_turn_team else None + 'current_turn_team_name': game.current_turn_team.name if game.current_turn_team else None, + 'steal_active': game.steal_active } if current_question: @@ -49,6 +50,7 @@ def start_game(game, socketio_instance): game.is_active = True game.current_question_index = 0 + game.steal_active = False # Set initial turn to the first team (by ID order) ordered_teams = get_ordered_teams(game) @@ -83,8 +85,10 @@ def next_question(game, socketio_instance): """Move to next question""" if game.current_question_index < len(game.game_questions) - 1: game.current_question_index += 1 + game.steal_active = False db.session.commit() broadcast_question_change(game, socketio_instance) + broadcast_steal_state(game, socketio_instance) return True return False @@ -93,8 +97,10 @@ def previous_question(game, socketio_instance): """Move to previous question""" if game.current_question_index > 0: game.current_question_index -= 1 + game.steal_active = False db.session.commit() broadcast_question_change(game, socketio_instance) + broadcast_steal_state(game, socketio_instance) return True return False @@ -254,6 +260,7 @@ def restart_game(game, socketio_instance): game.is_active = False game.current_question_index = 0 game.current_turn_team_id = None # Reset turn + game.steal_active = False # Reset phone-a-friend lifelines for all teams for team in game.teams: @@ -365,6 +372,76 @@ def advance_turn(game, socketio_instance): return next_team +def mark_correct(game, socketio_instance): + """Mark the current question as answered correctly by the current turn team. + Awards 1 point, increments question stats, resets steal state.""" + current_question = game.get_current_question() + if not current_question: + return None + + team = game.current_turn_team + if not team: + return None + + # Increment question stats + current_question.times_correct += 1 + + # Reset steal state + game.steal_active = False + + db.session.commit() + + # Award 1 point + award_points(game, team, 1, socketio_instance) + + # Broadcast steal state reset + broadcast_steal_state(game, socketio_instance) + + return team + + +def mark_wrong(game, socketio_instance): + """Mark the current question as answered incorrectly. + If steal not active: enters steal mode, advances turn. + If steal active: both teams failed, resets steal.""" + current_question = game.get_current_question() + if not current_question: + return {'steal_active': False, 'both_failed': True} + + # Increment question stats + current_question.times_incorrect += 1 + + if not game.steal_active: + # First wrong answer - enter steal mode, advance turn + game.steal_active = True + db.session.commit() + + advance_turn(game, socketio_instance) + broadcast_steal_state(game, socketio_instance) + + return {'steal_active': True, 'both_failed': False} + else: + # Second wrong answer - both teams failed + game.steal_active = False + db.session.commit() + + broadcast_steal_state(game, socketio_instance) + + return {'steal_active': False, 'both_failed': True} + + +def broadcast_steal_state(game, socketio_instance): + """Broadcast steal state to all connected clients""" + steal_data = { + 'steal_active': game.steal_active, + 'current_turn_team_id': game.current_turn_team_id, + 'current_turn_team_name': game.current_turn_team.name if game.current_turn_team else None, + } + + socketio_instance.emit('steal_state_changed', steal_data, room=f'game_{game.id}_contestant') + socketio_instance.emit('steal_state_changed', steal_data, room=f'game_{game.id}_admin') + + def broadcast_turn_change(game, socketio_instance): """Broadcast turn change to all connected clients""" ordered_teams = get_ordered_teams(game) diff --git a/frontend/frontend/src/components/admin/GameAdminView.jsx b/frontend/frontend/src/components/admin/GameAdminView.jsx index cd8b29b..98abddc 100644 --- a/frontend/frontend/src/components/admin/GameAdminView.jsx +++ b/frontend/frontend/src/components/admin/GameAdminView.jsx @@ -23,6 +23,7 @@ export default function GameAdminView() { const [newTeamName, setNewTeamName] = useState(""); const [currentTurnTeamId, setCurrentTurnTeamId] = useState(null); const [currentTurnTeamName, setCurrentTurnTeamName] = useState(null); + const [stealActive, setStealActive] = useState(false); useEffect(() => { loadGameState(); @@ -41,6 +42,7 @@ export default function GameAdminView() { setTotalQuestions(response.data.total_questions); setCurrentTurnTeamId(response.data.current_turn_team_id); setCurrentTurnTeamName(response.data.current_turn_team_name); + setStealActive(response.data.steal_active || false); } catch (error) { console.error("Error loading game state:", error); } @@ -87,6 +89,13 @@ export default function GameAdminView() { setCurrentTurnTeamName(data.current_turn_team_name); }); + socket.on("steal_state_changed", (data) => { + console.log("Steal state changed:", data); + setStealActive(data.steal_active); + setCurrentTurnTeamId(data.current_turn_team_id); + setCurrentTurnTeamName(data.current_turn_team_name); + }); + return () => { socket.off("question_with_answer"); socket.off("score_updated"); @@ -94,6 +103,7 @@ export default function GameAdminView() { socket.off("lifeline_updated"); socket.off("timer_reset"); socket.off("turn_changed"); + socket.off("steal_state_changed"); }; }, [socket]); @@ -220,6 +230,24 @@ export default function GameAdminView() { } }; + const handleMarkCorrect = async () => { + try { + await adminAPI.markCorrect(gameId); + } catch (error) { + console.error("Error marking correct:", error); + alert("Error marking correct"); + } + }; + + const handleMarkWrong = async () => { + try { + await adminAPI.markWrong(gameId); + } catch (error) { + console.error("Error marking wrong:", error); + alert("Error marking wrong"); + } + }; + const handleAdvanceTurn = async () => { try { await adminAPI.advanceTurn(gameId); @@ -399,6 +427,20 @@ export default function GameAdminView() {
Answer: {currentQuestion.answer}
+ {stealActive && ( +
+ STEAL OPPORTUNITY — {currentTurnTeamName}'s turn to steal +
+ )} ) : (

No question selected. Click "Start Game" to begin.

@@ -559,21 +601,57 @@ export default function GameAdminView() { {/* Button grid - 2 columns on desktop, 1 on mobile */}
{gameState?.is_active && teams.length > 0 && ( - + <> + + {currentQuestion && ( + <> + + + + )} + )} {currentQuestion && (