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
|
||||
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:
|
||||
|
||||
@@ -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/<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
|
||||
|
||||
|
||||
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')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
) : (
|
||||
<div className="teams-list">
|
||||
{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-name-section">
|
||||
<strong className="team-name">{team.name}</strong>
|
||||
@@ -538,6 +558,23 @@ export default function GameAdminView() {
|
||||
|
||||
{/* Button grid - 2 columns on desktop, 1 on mobile */}
|
||||
<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 && (
|
||||
<button
|
||||
onClick={handleToggleAnswer}
|
||||
|
||||
@@ -18,6 +18,8 @@ export default function ContestantView() {
|
||||
const [timerActive, setTimerActive] = useState(false);
|
||||
const [timerPaused, setTimerPaused] = useState(false);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [currentTurnTeamId, setCurrentTurnTeamId] = useState(null);
|
||||
const [currentTurnTeamName, setCurrentTurnTeamName] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Load initial game state
|
||||
@@ -140,6 +142,8 @@ export default function ContestantView() {
|
||||
setTimerSeconds(30);
|
||||
setTimerActive(false);
|
||||
setTimerPaused(false);
|
||||
setCurrentTurnTeamId(null);
|
||||
setCurrentTurnTeamName(null);
|
||||
});
|
||||
|
||||
socket.on("lifeline_updated", (data) => {
|
||||
@@ -154,6 +158,12 @@ export default function ContestantView() {
|
||||
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("game_started");
|
||||
socket.off("question_changed");
|
||||
@@ -163,6 +173,7 @@ export default function ContestantView() {
|
||||
socket.off("game_ended");
|
||||
socket.off("lifeline_updated");
|
||||
socket.off("timer_reset");
|
||||
socket.off("turn_changed");
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
@@ -283,6 +294,25 @@ export default function ContestantView() {
|
||||
>
|
||||
{currentQuestion ? (
|
||||
<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 */}
|
||||
<div
|
||||
style={{
|
||||
@@ -476,6 +506,11 @@ export default function ContestantView() {
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
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
|
||||
|
||||
@@ -54,3 +54,20 @@ button:focus,
|
||||
button:focus-visible {
|
||||
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`),
|
||||
addLifeline: (gameId, teamId) =>
|
||||
api.post(`/admin/game/${gameId}/team/${teamId}/add-lifeline`),
|
||||
advanceTurn: (id) => api.post(`/admin/game/${id}/advance-turn`),
|
||||
};
|
||||
|
||||
// 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