feat: sistema de 2 rondas con puntos dobles
Ronda 1: 5 categorías con puntos normales (100-500) Ronda 2: 5 categorías diferentes con puntos x2 (200-1000) Backend: - question_service: soporta excluir categorías y multiplicador de puntos - game_manager: trackea current_round, start_round_2() carga nuevo tablero - game_events: emite round_started al completar ronda 1 Frontend: - useSocket: escucha evento round_started - Game.tsx: muestra indicador de ronda actual - types: GameRoom incluye current_round Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,8 @@ class GameManager:
|
|||||||
room["current_player_index"] = {"A": 0, "B": 0}
|
room["current_player_index"] = {"A": 0, "B": 0}
|
||||||
room["board"] = board
|
room["board"] = board
|
||||||
room["scores"] = {"A": 0, "B": 0}
|
room["scores"] = {"A": 0, "B": 0}
|
||||||
|
room["current_round"] = 1
|
||||||
|
room["round1_categories"] = [int(cat_id) for cat_id in board.keys()]
|
||||||
|
|
||||||
await room_manager.update_room(room_code, room)
|
await room_manager.update_room(room_code, room)
|
||||||
return room
|
return room
|
||||||
@@ -153,13 +155,19 @@ class GameManager:
|
|||||||
# Switch to other team for potential steal
|
# Switch to other team for potential steal
|
||||||
room["current_team"] = "B" if failed_team == "A" else "A"
|
room["current_team"] = "B" if failed_team == "A" else "A"
|
||||||
|
|
||||||
# Check if game is over (all questions answered)
|
# Check if round is over (all questions answered)
|
||||||
all_answered = all(
|
all_answered = all(
|
||||||
q["answered"]
|
q["answered"]
|
||||||
for questions in room["board"].values()
|
for questions in room["board"].values()
|
||||||
for q in questions
|
for q in questions
|
||||||
)
|
)
|
||||||
if all_answered:
|
if all_answered:
|
||||||
|
current_round = room.get("current_round", 1)
|
||||||
|
if current_round == 1:
|
||||||
|
# Round 1 finished - need to start round 2
|
||||||
|
room["round_finished"] = True
|
||||||
|
else:
|
||||||
|
# Round 2 finished - game over
|
||||||
room["status"] = "finished"
|
room["status"] = "finished"
|
||||||
|
|
||||||
await room_manager.update_room(room_code, room)
|
await room_manager.update_room(room_code, room)
|
||||||
@@ -189,6 +197,19 @@ class GameManager:
|
|||||||
room["current_question"] = None
|
room["current_question"] = None
|
||||||
room["can_steal"] = False
|
room["can_steal"] = False
|
||||||
|
|
||||||
|
# Check if round is over
|
||||||
|
all_answered = all(
|
||||||
|
q["answered"]
|
||||||
|
for questions in room["board"].values()
|
||||||
|
for q in questions
|
||||||
|
)
|
||||||
|
if all_answered:
|
||||||
|
current_round = room.get("current_round", 1)
|
||||||
|
if current_round == 1:
|
||||||
|
room["round_finished"] = True
|
||||||
|
else:
|
||||||
|
room["status"] = "finished"
|
||||||
|
|
||||||
await room_manager.update_room(room_code, room)
|
await room_manager.update_room(room_code, room)
|
||||||
return room
|
return room
|
||||||
|
|
||||||
@@ -205,6 +226,45 @@ class GameManager:
|
|||||||
index = room["current_player_index"][team]
|
index = room["current_player_index"][team]
|
||||||
return players[index % len(players)]
|
return players[index % len(players)]
|
||||||
|
|
||||||
|
async def start_round_2(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
room_code: str
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Start round 2 with different categories and double points.
|
||||||
|
"""
|
||||||
|
room = await room_manager.get_room(room_code)
|
||||||
|
if not room:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get categories used in round 1
|
||||||
|
round1_categories = room.get("round1_categories", [])
|
||||||
|
|
||||||
|
# Get new board excluding round 1 categories, with 2x points
|
||||||
|
new_board = await question_service.get_board_for_game(
|
||||||
|
db,
|
||||||
|
exclude_categories=round1_categories,
|
||||||
|
point_multiplier=2
|
||||||
|
)
|
||||||
|
|
||||||
|
if not new_board:
|
||||||
|
# Not enough categories for round 2 - end game
|
||||||
|
room["status"] = "finished"
|
||||||
|
await room_manager.update_room(room_code, room)
|
||||||
|
return room
|
||||||
|
|
||||||
|
# Update room for round 2
|
||||||
|
room["board"] = new_board
|
||||||
|
room["current_round"] = 2
|
||||||
|
room["round_finished"] = False
|
||||||
|
room["current_question"] = None
|
||||||
|
room["can_steal"] = False
|
||||||
|
# Keep current_team - winner of last question picks first
|
||||||
|
|
||||||
|
await room_manager.update_room(room_code, room)
|
||||||
|
return room
|
||||||
|
|
||||||
def calculate_timer_end(self, time_seconds: int, is_steal: bool = False) -> datetime:
|
def calculate_timer_end(self, time_seconds: int, is_steal: bool = False) -> datetime:
|
||||||
"""Calculate when the timer should end."""
|
"""Calculate when the timer should end."""
|
||||||
if is_steal:
|
if is_steal:
|
||||||
|
|||||||
@@ -65,12 +65,20 @@ class QuestionService:
|
|||||||
async def get_board_for_game(
|
async def get_board_for_game(
|
||||||
self,
|
self,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
target_date: Optional[date] = None
|
target_date: Optional[date] = None,
|
||||||
|
exclude_categories: Optional[List[int]] = None,
|
||||||
|
point_multiplier: int = 1
|
||||||
) -> Dict[str, List[dict]]:
|
) -> Dict[str, List[dict]]:
|
||||||
"""
|
"""
|
||||||
Genera el tablero 5×5 para el juego.
|
Genera el tablero 5×5 para el juego.
|
||||||
Selecciona 5 categorías aleatorias y 1 pregunta por dificultad.
|
Selecciona 5 categorías aleatorias y 1 pregunta por dificultad.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Database session
|
||||||
|
target_date: Date for questions (default: today)
|
||||||
|
exclude_categories: Category IDs to exclude (for round 2)
|
||||||
|
point_multiplier: Multiply points by this value (for round 2)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict con category_id como string (para JSON) -> lista de preguntas
|
Dict con category_id como string (para JSON) -> lista de preguntas
|
||||||
"""
|
"""
|
||||||
@@ -82,6 +90,15 @@ class QuestionService:
|
|||||||
# Get available category IDs that have questions
|
# Get available category IDs that have questions
|
||||||
available_categories = list(full_board.keys())
|
available_categories = list(full_board.keys())
|
||||||
|
|
||||||
|
# Exclude categories from previous round
|
||||||
|
if exclude_categories:
|
||||||
|
available_categories = [
|
||||||
|
c for c in available_categories if c not in exclude_categories
|
||||||
|
]
|
||||||
|
|
||||||
|
if not available_categories:
|
||||||
|
return {}
|
||||||
|
|
||||||
# Select random categories (up to CATEGORIES_PER_GAME)
|
# Select random categories (up to CATEGORIES_PER_GAME)
|
||||||
num_categories = min(CATEGORIES_PER_GAME, len(available_categories))
|
num_categories = min(CATEGORIES_PER_GAME, len(available_categories))
|
||||||
selected_categories = random.sample(available_categories, num_categories)
|
selected_categories = random.sample(available_categories, num_categories)
|
||||||
@@ -104,7 +121,10 @@ class QuestionService:
|
|||||||
for difficulty in range(1, 6): # 1-5
|
for difficulty in range(1, 6): # 1-5
|
||||||
if difficulty in questions_by_difficulty:
|
if difficulty in questions_by_difficulty:
|
||||||
questions = questions_by_difficulty[difficulty]
|
questions = questions_by_difficulty[difficulty]
|
||||||
selected_q = random.choice(questions)
|
selected_q = random.choice(questions).copy()
|
||||||
|
# Apply point multiplier for round 2
|
||||||
|
if point_multiplier > 1:
|
||||||
|
selected_q["points"] = selected_q["points"] * point_multiplier
|
||||||
selected_questions.append(selected_q)
|
selected_questions.append(selected_q)
|
||||||
|
|
||||||
if selected_questions:
|
if selected_questions:
|
||||||
|
|||||||
@@ -358,9 +358,28 @@ def register_socket_events(sio: socketio.AsyncServer):
|
|||||||
points_earned=result["points_earned"]
|
points_earned=result["points_earned"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verificar si el juego termino (todas las preguntas respondidas)
|
# Verificar si terminó la ronda o el juego
|
||||||
if room_data.get("status") == "finished":
|
if room_data.get("round_finished"):
|
||||||
# Disparar finalizacion automatica
|
# Ronda 1 terminada - iniciar ronda 2
|
||||||
|
async with await get_db_session() as db:
|
||||||
|
new_room = await game_manager.start_round_2(db, room_code)
|
||||||
|
if new_room:
|
||||||
|
if new_room.get("status") == "finished":
|
||||||
|
# No hay suficientes categorías para ronda 2
|
||||||
|
await finish_game_internal(room_code)
|
||||||
|
else:
|
||||||
|
# Emitir evento de nueva ronda
|
||||||
|
await sio.emit(
|
||||||
|
"round_started",
|
||||||
|
{
|
||||||
|
"room": new_room,
|
||||||
|
"round": 2,
|
||||||
|
"message": "¡Ronda 2! Puntos dobles"
|
||||||
|
},
|
||||||
|
room=room_code
|
||||||
|
)
|
||||||
|
elif room_data.get("status") == "finished":
|
||||||
|
# Juego terminado
|
||||||
await finish_game_internal(room_code)
|
await finish_game_internal(room_code)
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
@@ -395,6 +414,26 @@ def register_socket_events(sio: socketio.AsyncServer):
|
|||||||
team=player["team"],
|
team=player["team"],
|
||||||
question_id=question_id
|
question_id=question_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Verificar si terminó la ronda o el juego
|
||||||
|
if room.get("round_finished"):
|
||||||
|
async with await get_db_session() as db:
|
||||||
|
new_room = await game_manager.start_round_2(db, room_code)
|
||||||
|
if new_room:
|
||||||
|
if new_room.get("status") == "finished":
|
||||||
|
await finish_game_internal(room_code)
|
||||||
|
else:
|
||||||
|
await sio.emit(
|
||||||
|
"round_started",
|
||||||
|
{
|
||||||
|
"room": new_room,
|
||||||
|
"round": 2,
|
||||||
|
"message": "¡Ronda 2! Puntos dobles"
|
||||||
|
},
|
||||||
|
room=room_code
|
||||||
|
)
|
||||||
|
elif room.get("status") == "finished":
|
||||||
|
await finish_game_internal(room_code)
|
||||||
else:
|
else:
|
||||||
# Will attempt steal - just notify, answer comes separately
|
# Will attempt steal - just notify, answer comes separately
|
||||||
room = await room_manager.get_room(room_code)
|
room = await room_manager.get_room(room_code)
|
||||||
|
|||||||
@@ -85,6 +85,14 @@ export function useSocket() {
|
|||||||
soundPlayer.play('game_start', volume)
|
soundPlayer.play('game_start', volume)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.on('round_started', (data: { room: GameRoom; round: number; message: string }) => {
|
||||||
|
setRoom(data.room)
|
||||||
|
setCurrentQuestion(null)
|
||||||
|
// Play sound for new round
|
||||||
|
const volume = useSoundStore.getState().volume
|
||||||
|
soundPlayer.play('game_start', volume)
|
||||||
|
})
|
||||||
|
|
||||||
socket.on('question_selected', (data: { room: GameRoom; question_id: number }) => {
|
socket.on('question_selected', (data: { room: GameRoom; question_id: number }) => {
|
||||||
setRoom(data.room)
|
setRoom(data.room)
|
||||||
// Find the question in the board and set it as current
|
// Find the question in the board and set it as current
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export default function Game() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen p-2 md:p-4 overflow-hidden" style={styles.bgPrimary}>
|
<div className="min-h-screen p-2 md:p-4 overflow-hidden" style={styles.bgPrimary}>
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header with Room Code */}
|
{/* Header with Room Code and Round */}
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial={{ y: -20, opacity: 0 }}
|
initial={{ y: -20, opacity: 0 }}
|
||||||
@@ -167,8 +167,21 @@ export default function Game() {
|
|||||||
>
|
>
|
||||||
TRIVY
|
TRIVY
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<div className="text-xs opacity-60" style={{ color: config.colors.textMuted }}>
|
<div className="flex items-center justify-center gap-3 text-xs" style={{ color: config.colors.textMuted }}>
|
||||||
Sala: {room.code}
|
<span className="opacity-60">Sala: {room.code}</span>
|
||||||
|
<span className="opacity-40">|</span>
|
||||||
|
<motion.span
|
||||||
|
key={room.current_round}
|
||||||
|
initial={{ scale: 1.5, color: config.colors.accent }}
|
||||||
|
animate={{ scale: 1, color: config.colors.textMuted }}
|
||||||
|
className="font-bold"
|
||||||
|
style={{
|
||||||
|
color: room.current_round === 2 ? config.colors.accent : config.colors.textMuted
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ronda {room.current_round || 1}
|
||||||
|
{room.current_round === 2 && ' (x2)'}
|
||||||
|
</motion.span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface GameRoom {
|
|||||||
can_steal: boolean
|
can_steal: boolean
|
||||||
scores: { A: number; B: number }
|
scores: { A: number; B: number }
|
||||||
board: Record<string, Question[]>
|
board: Record<string, Question[]>
|
||||||
|
current_round?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
|
|||||||
Reference in New Issue
Block a user