oops
This commit is contained in:
@@ -113,13 +113,16 @@ class Game(db.Model):
|
|||||||
is_active = db.Column(db.Boolean, default=False) # Only one game active at a time
|
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
|
current_question_index = db.Column(db.Integer, default=0) # Track current question
|
||||||
is_template = db.Column(db.Boolean, default=False) # Mark as reusable template
|
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
|
# 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',
|
game_questions = db.relationship('GameQuestion', back_populates='game',
|
||||||
cascade='all, delete-orphan',
|
cascade='all, delete-orphan',
|
||||||
order_by='GameQuestion.order')
|
order_by='GameQuestion.order')
|
||||||
scores = db.relationship('Score', back_populates='game', cascade='all, delete-orphan')
|
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
|
@classmethod
|
||||||
def get_active(cls):
|
def get_active(cls):
|
||||||
@@ -141,7 +144,9 @@ class Game(db.Model):
|
|||||||
'is_active': self.is_active,
|
'is_active': self.is_active,
|
||||||
'current_question_index': self.current_question_index,
|
'current_question_index': self.current_question_index,
|
||||||
'total_questions': len(self.game_questions),
|
'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:
|
if include_questions:
|
||||||
|
|||||||
@@ -302,3 +302,25 @@ def seek_audio(game_id):
|
|||||||
return jsonify({'message': f'Audio seeked to {position}s'}), 200
|
return jsonify({'message': f'Audio seeked to {position}s'}), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/game/<int:game_id>/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
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
from backend.models import db, Game, Score
|
from backend.models import db, Game, Score, Team
|
||||||
from flask_socketio import emit
|
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):
|
def get_game_state(game):
|
||||||
"""Get current game state with all necessary information"""
|
"""Get current game state with all necessary information"""
|
||||||
current_question = game.get_current_question()
|
current_question = game.get_current_question()
|
||||||
@@ -12,7 +17,9 @@ def get_game_state(game):
|
|||||||
'current_question_index': game.current_question_index,
|
'current_question_index': game.current_question_index,
|
||||||
'total_questions': len(game.game_questions),
|
'total_questions': len(game.game_questions),
|
||||||
'is_active': game.is_active,
|
'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:
|
if current_question:
|
||||||
@@ -42,6 +49,14 @@ def start_game(game, socketio_instance):
|
|||||||
|
|
||||||
game.is_active = True
|
game.is_active = True
|
||||||
game.current_question_index = 0
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
# Emit game_started event
|
# Emit game_started event
|
||||||
@@ -60,6 +75,9 @@ def start_game(game, socketio_instance):
|
|||||||
# Emit first question
|
# Emit first question
|
||||||
broadcast_question_change(game, socketio_instance)
|
broadcast_question_change(game, socketio_instance)
|
||||||
|
|
||||||
|
# Emit initial turn
|
||||||
|
broadcast_turn_change(game, socketio_instance)
|
||||||
|
|
||||||
|
|
||||||
def next_question(game, socketio_instance):
|
def next_question(game, socketio_instance):
|
||||||
"""Move to next question"""
|
"""Move to next question"""
|
||||||
@@ -209,6 +227,7 @@ def restart_game(game, socketio_instance):
|
|||||||
# Reset game state
|
# Reset game state
|
||||||
game.is_active = False
|
game.is_active = False
|
||||||
game.current_question_index = 0
|
game.current_question_index = 0
|
||||||
|
game.current_turn_team_id = None # Reset turn
|
||||||
|
|
||||||
# Reset phone-a-friend lifelines for all teams
|
# Reset phone-a-friend lifelines for all teams
|
||||||
for team in game.teams:
|
for team in game.teams:
|
||||||
@@ -283,3 +302,52 @@ def broadcast_audio_seek(game, position, socketio_instance):
|
|||||||
'game_id': game.id,
|
'game_id': game.id,
|
||||||
'position': position
|
'position': position
|
||||||
}, room=f'game_{game.id}_contestant')
|
}, 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')
|
||||||
|
|||||||
@@ -111,6 +111,17 @@
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: white;
|
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 {
|
.team-card-header {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export default function GameAdminView() {
|
|||||||
const [timerExpired, setTimerExpired] = useState(false);
|
const [timerExpired, setTimerExpired] = useState(false);
|
||||||
const [timerPaused, setTimerPaused] = useState(false);
|
const [timerPaused, setTimerPaused] = useState(false);
|
||||||
const [newTeamName, setNewTeamName] = useState("");
|
const [newTeamName, setNewTeamName] = useState("");
|
||||||
|
const [currentTurnTeamId, setCurrentTurnTeamId] = useState(null);
|
||||||
|
const [currentTurnTeamName, setCurrentTurnTeamName] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadGameState();
|
loadGameState();
|
||||||
@@ -37,6 +39,8 @@ export default function GameAdminView() {
|
|||||||
setCurrentQuestion(response.data.current_question);
|
setCurrentQuestion(response.data.current_question);
|
||||||
setQuestionIndex(response.data.current_question_index);
|
setQuestionIndex(response.data.current_question_index);
|
||||||
setTotalQuestions(response.data.total_questions);
|
setTotalQuestions(response.data.total_questions);
|
||||||
|
setCurrentTurnTeamId(response.data.current_turn_team_id);
|
||||||
|
setCurrentTurnTeamName(response.data.current_turn_team_name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading game state:", error);
|
console.error("Error loading game state:", error);
|
||||||
}
|
}
|
||||||
@@ -77,12 +81,19 @@ export default function GameAdminView() {
|
|||||||
setTimerPaused(false);
|
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 () => {
|
return () => {
|
||||||
socket.off("question_with_answer");
|
socket.off("question_with_answer");
|
||||||
socket.off("score_updated");
|
socket.off("score_updated");
|
||||||
socket.off("timer_paused");
|
socket.off("timer_paused");
|
||||||
socket.off("lifeline_updated");
|
socket.off("lifeline_updated");
|
||||||
socket.off("timer_reset");
|
socket.off("timer_reset");
|
||||||
|
socket.off("turn_changed");
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [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 handleToggleAnswer = async () => {
|
||||||
const newShowAnswer = !showAnswer;
|
const newShowAnswer = !showAnswer;
|
||||||
try {
|
try {
|
||||||
@@ -417,7 +437,7 @@ export default function GameAdminView() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="teams-list">
|
<div className="teams-list">
|
||||||
{teams.map((team) => (
|
{teams.map((team) => (
|
||||||
<div key={team.id} className="team-card">
|
<div key={team.id} className={`team-card ${team.id === currentTurnTeamId ? 'team-card-active-turn' : ''}`}>
|
||||||
<div className="team-card-header">
|
<div className="team-card-header">
|
||||||
<div className="team-name-section">
|
<div className="team-name-section">
|
||||||
<strong className="team-name">{team.name}</strong>
|
<strong className="team-name">{team.name}</strong>
|
||||||
@@ -538,6 +558,23 @@ export default function GameAdminView() {
|
|||||||
|
|
||||||
{/* Button grid - 2 columns on desktop, 1 on mobile */}
|
{/* Button grid - 2 columns on desktop, 1 on mobile */}
|
||||||
<div className="controls-button-grid">
|
<div className="controls-button-grid">
|
||||||
|
{gameState?.is_active && teams.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleAdvanceTurn}
|
||||||
|
className="btn-next-turn"
|
||||||
|
style={{
|
||||||
|
padding: "0.75rem 1.5rem",
|
||||||
|
background: "#673AB7",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next Turn {currentTurnTeamName ? `(${currentTurnTeamName})` : ""}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{currentQuestion && (
|
{currentQuestion && (
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleAnswer}
|
onClick={handleToggleAnswer}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export default function ContestantView() {
|
|||||||
const [timerActive, setTimerActive] = useState(false);
|
const [timerActive, setTimerActive] = useState(false);
|
||||||
const [timerPaused, setTimerPaused] = useState(false);
|
const [timerPaused, setTimerPaused] = useState(false);
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
|
const [currentTurnTeamId, setCurrentTurnTeamId] = useState(null);
|
||||||
|
const [currentTurnTeamName, setCurrentTurnTeamName] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load initial game state
|
// Load initial game state
|
||||||
@@ -140,6 +142,8 @@ export default function ContestantView() {
|
|||||||
setTimerSeconds(30);
|
setTimerSeconds(30);
|
||||||
setTimerActive(false);
|
setTimerActive(false);
|
||||||
setTimerPaused(false);
|
setTimerPaused(false);
|
||||||
|
setCurrentTurnTeamId(null);
|
||||||
|
setCurrentTurnTeamName(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("lifeline_updated", (data) => {
|
socket.on("lifeline_updated", (data) => {
|
||||||
@@ -154,6 +158,12 @@ export default function ContestantView() {
|
|||||||
setTimerPaused(false);
|
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 () => {
|
return () => {
|
||||||
socket.off("game_started");
|
socket.off("game_started");
|
||||||
socket.off("question_changed");
|
socket.off("question_changed");
|
||||||
@@ -163,6 +173,7 @@ export default function ContestantView() {
|
|||||||
socket.off("game_ended");
|
socket.off("game_ended");
|
||||||
socket.off("lifeline_updated");
|
socket.off("lifeline_updated");
|
||||||
socket.off("timer_reset");
|
socket.off("timer_reset");
|
||||||
|
socket.off("turn_changed");
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
@@ -283,6 +294,25 @@ export default function ContestantView() {
|
|||||||
>
|
>
|
||||||
{currentQuestion ? (
|
{currentQuestion ? (
|
||||||
<div style={{ width: "100%", textAlign: "center" }}>
|
<div style={{ width: "100%", textAlign: "center" }}>
|
||||||
|
{/* Turn indicator */}
|
||||||
|
{currentTurnTeamName && (
|
||||||
|
<div
|
||||||
|
className="turn-indicator"
|
||||||
|
style={{
|
||||||
|
fontSize: "2rem",
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginBottom: "1.5rem",
|
||||||
|
padding: "1rem 2rem",
|
||||||
|
background: "#673AB7",
|
||||||
|
color: "white",
|
||||||
|
borderRadius: "8px",
|
||||||
|
display: "inline-block",
|
||||||
|
animation: "pulse 2s infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentTurnTeamName}'s Turn
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Timer progress bar */}
|
{/* Timer progress bar */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -476,6 +506,11 @@ export default function ContestantView() {
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "baseline",
|
alignItems: "baseline",
|
||||||
fontSize: "1.8rem",
|
fontSize: "1.8rem",
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
borderRadius: "8px",
|
||||||
|
background: (team.team_id || team.id) === currentTurnTeamId ? "#673AB7" : "transparent",
|
||||||
|
color: (team.team_id || team.id) === currentTurnTeamId ? "white" : "inherit",
|
||||||
|
transition: "all 0.3s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -54,3 +54,20 @@ button:focus,
|
|||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Turn indicator pulse animation */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(103, 58, 183, 0.4);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 15px rgba(103, 58, 183, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(103, 58, 183, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.turn-indicator {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export const adminAPI = {
|
|||||||
api.post(`/admin/game/${gameId}/team/${teamId}/use-lifeline`),
|
api.post(`/admin/game/${gameId}/team/${teamId}/use-lifeline`),
|
||||||
addLifeline: (gameId, teamId) =>
|
addLifeline: (gameId, teamId) =>
|
||||||
api.post(`/admin/game/${gameId}/team/${teamId}/add-lifeline`),
|
api.post(`/admin/game/${gameId}/team/${teamId}/add-lifeline`),
|
||||||
|
advanceTurn: (id) => api.post(`/admin/game/${id}/advance-turn`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Categories API
|
// Categories API
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Add current_turn_team_id to games
|
||||||
|
|
||||||
|
Revision ID: a1b2c3d4e5f6
|
||||||
|
Revises: 1252454a2589
|
||||||
|
Create Date: 2026-01-18 15:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a1b2c3d4e5f6'
|
||||||
|
down_revision = '90b81e097444'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('games', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('current_turn_team_id', sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_foreign_key('fk_games_current_turn_team_id', 'teams', ['current_turn_team_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('games', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('fk_games_current_turn_team_id', type_='foreignkey')
|
||||||
|
batch_op.drop_column('current_turn_team_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user