From e431ba45e96b184ac5a454518c427227d3c0ebab Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 18 Jan 2026 16:22:43 -0500 Subject: [PATCH] oops --- backend/models.py | 9 ++- backend/routes/admin.py | 22 ++++++ backend/services/game_service.py | 72 ++++++++++++++++++- .../src/components/admin/GameAdminView.css | 11 +++ .../src/components/admin/GameAdminView.jsx | 39 +++++++++- .../components/contestant/ContestantView.jsx | 35 +++++++++ frontend/frontend/src/index.css | 17 +++++ frontend/frontend/src/services/api.js | 1 + ...4e5f6_add_current_turn_team_id_to_games.py | 34 +++++++++ 9 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 migrations/versions/a1b2c3d4e5f6_add_current_turn_team_id_to_games.py diff --git a/backend/models.py b/backend/models.py index db68f81..b1feaaf 100644 --- a/backend/models.py +++ b/backend/models.py @@ -113,13 +113,16 @@ class Game(db.Model): is_active = db.Column(db.Boolean, default=False) # Only one game active at a time 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 # Relationships - teams = db.relationship('Team', back_populates='game', cascade='all, delete-orphan') + teams = db.relationship('Team', back_populates='game', cascade='all, delete-orphan', + foreign_keys='Team.game_id') game_questions = db.relationship('GameQuestion', back_populates='game', cascade='all, delete-orphan', order_by='GameQuestion.order') scores = db.relationship('Score', back_populates='game', cascade='all, delete-orphan') + current_turn_team = db.relationship('Team', foreign_keys=[current_turn_team_id], post_update=True) @classmethod def get_active(cls): @@ -141,7 +144,9 @@ class Game(db.Model): 'is_active': self.is_active, 'current_question_index': self.current_question_index, 'total_questions': len(self.game_questions), - 'is_template': self.is_template + '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 } if include_questions: diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 3768ef9..f69ed9a 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -302,3 +302,25 @@ def seek_audio(game_id): return jsonify({'message': f'Audio seeked to {position}s'}), 200 except Exception as e: return jsonify({'error': str(e)}), 500 + + +@bp.route('/game//advance-turn', methods=['POST']) +@require_auth +def advance_turn(game_id): + """Advance to the next team's turn""" + game = Game.query.get_or_404(game_id) + + try: + next_team = game_service.advance_turn(game, socketio) + if next_team: + return jsonify({ + 'message': 'Turn advanced', + 'current_turn_team_id': next_team.id, + 'current_turn_team_name': next_team.name + }), 200 + else: + return jsonify({'error': 'No teams in game'}), 400 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 diff --git a/backend/services/game_service.py b/backend/services/game_service.py index ae1335e..dd19d11 100644 --- a/backend/services/game_service.py +++ b/backend/services/game_service.py @@ -1,7 +1,12 @@ -from backend.models import db, Game, Score +from backend.models import db, Game, Score, Team from flask_socketio import emit +def get_ordered_teams(game): + """Get teams sorted by ID for consistent turn ordering""" + return sorted(game.teams, key=lambda t: t.id) + + def get_game_state(game): """Get current game state with all necessary information""" current_question = game.get_current_question() @@ -12,7 +17,9 @@ def get_game_state(game): 'current_question_index': game.current_question_index, 'total_questions': len(game.game_questions), 'is_active': game.is_active, - 'teams': [team.to_dict() for team in game.teams] + '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 } if current_question: @@ -42,6 +49,14 @@ def start_game(game, socketio_instance): game.is_active = True game.current_question_index = 0 + + # Set initial turn to the first team (by ID order) + ordered_teams = get_ordered_teams(game) + if ordered_teams: + game.current_turn_team_id = ordered_teams[0].id + else: + game.current_turn_team_id = None + db.session.commit() # Emit game_started event @@ -60,6 +75,9 @@ def start_game(game, socketio_instance): # Emit first question broadcast_question_change(game, socketio_instance) + # Emit initial turn + broadcast_turn_change(game, socketio_instance) + def next_question(game, socketio_instance): """Move to next question""" @@ -209,6 +227,7 @@ def restart_game(game, socketio_instance): # Reset game state game.is_active = False game.current_question_index = 0 + game.current_turn_team_id = None # Reset turn # Reset phone-a-friend lifelines for all teams for team in game.teams: @@ -283,3 +302,52 @@ def broadcast_audio_seek(game, position, socketio_instance): 'game_id': game.id, 'position': position }, room=f'game_{game.id}_contestant') + + +def advance_turn(game, socketio_instance): + """Advance to the next team's turn (cycles through teams by ID order)""" + ordered_teams = get_ordered_teams(game) + + if not ordered_teams: + return None + + if game.current_turn_team_id is None: + # No current turn, set to first team + next_team = ordered_teams[0] + else: + # Find current team index and advance to next + current_index = None + for i, team in enumerate(ordered_teams): + if team.id == game.current_turn_team_id: + current_index = i + break + + if current_index is None: + # Current team not found, set to first + next_team = ordered_teams[0] + else: + # Cycle to next team (modulo for wrap-around) + next_index = (current_index + 1) % len(ordered_teams) + next_team = ordered_teams[next_index] + + game.current_turn_team_id = next_team.id + db.session.commit() + + # Broadcast turn change to both rooms + broadcast_turn_change(game, socketio_instance) + + return next_team + + +def broadcast_turn_change(game, socketio_instance): + """Broadcast turn change to all connected clients""" + ordered_teams = get_ordered_teams(game) + + turn_data = { + '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, + 'all_teams': [{'id': t.id, 'name': t.name} for t in ordered_teams] + } + + socketio_instance.emit('turn_changed', turn_data, room=f'game_{game.id}_contestant') + socketio_instance.emit('turn_changed', turn_data, room=f'game_{game.id}_admin') diff --git a/frontend/frontend/src/components/admin/GameAdminView.css b/frontend/frontend/src/components/admin/GameAdminView.css index 299e3a6..6f8ef09 100644 --- a/frontend/frontend/src/components/admin/GameAdminView.css +++ b/frontend/frontend/src/components/admin/GameAdminView.css @@ -111,6 +111,17 @@ border: 1px solid #ccc; border-radius: 8px; background: white; + transition: all 0.3s ease; +} + +.team-card-active-turn { + border: 3px solid #673AB7; + background: #EDE7F6; + box-shadow: 0 0 10px rgba(103, 58, 183, 0.3); +} + +.team-card-active-turn .team-name { + color: #673AB7; } .team-card-header { diff --git a/frontend/frontend/src/components/admin/GameAdminView.jsx b/frontend/frontend/src/components/admin/GameAdminView.jsx index 51d362d..cd8b29b 100644 --- a/frontend/frontend/src/components/admin/GameAdminView.jsx +++ b/frontend/frontend/src/components/admin/GameAdminView.jsx @@ -21,6 +21,8 @@ export default function GameAdminView() { const [timerExpired, setTimerExpired] = useState(false); const [timerPaused, setTimerPaused] = useState(false); const [newTeamName, setNewTeamName] = useState(""); + const [currentTurnTeamId, setCurrentTurnTeamId] = useState(null); + const [currentTurnTeamName, setCurrentTurnTeamName] = useState(null); useEffect(() => { loadGameState(); @@ -37,6 +39,8 @@ export default function GameAdminView() { setCurrentQuestion(response.data.current_question); setQuestionIndex(response.data.current_question_index); setTotalQuestions(response.data.total_questions); + setCurrentTurnTeamId(response.data.current_turn_team_id); + setCurrentTurnTeamName(response.data.current_turn_team_name); } catch (error) { console.error("Error loading game state:", error); } @@ -77,12 +81,19 @@ export default function GameAdminView() { setTimerPaused(false); }); + socket.on("turn_changed", (data) => { + console.log("Turn changed:", data); + setCurrentTurnTeamId(data.current_turn_team_id); + setCurrentTurnTeamName(data.current_turn_team_name); + }); + return () => { socket.off("question_with_answer"); socket.off("score_updated"); socket.off("timer_paused"); socket.off("lifeline_updated"); socket.off("timer_reset"); + socket.off("turn_changed"); }; }, [socket]); @@ -209,6 +220,15 @@ export default function GameAdminView() { } }; + const handleAdvanceTurn = async () => { + try { + await adminAPI.advanceTurn(gameId); + } catch (error) { + console.error("Error advancing turn:", error); + alert("Error advancing turn"); + } + }; + const handleToggleAnswer = async () => { const newShowAnswer = !showAnswer; try { @@ -417,7 +437,7 @@ export default function GameAdminView() { ) : (
{teams.map((team) => ( -
+
{team.name} @@ -538,6 +558,23 @@ export default function GameAdminView() { {/* Button grid - 2 columns on desktop, 1 on mobile */}
+ {gameState?.is_active && teams.length > 0 && ( + + )} {currentQuestion && (