diff --git a/backend/app/services/question_service.py b/backend/app/services/question_service.py
index ddab07d..ccef0d1 100644
--- a/backend/app/services/question_service.py
+++ b/backend/app/services/question_service.py
@@ -1,11 +1,15 @@
from typing import Optional, List, Dict
from datetime import date
-from sqlalchemy import select, and_
+import random
+from sqlalchemy import select, and_, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.question import Question
from app.models.category import Category
+# Number of categories per game
+CATEGORIES_PER_GAME = 5
+
class QuestionService:
async def get_daily_questions(
@@ -64,16 +68,49 @@ class QuestionService:
target_date: Optional[date] = None
) -> Dict[str, List[dict]]:
"""
- Genera el tablero 8×5 para el juego.
- Si no hay suficientes preguntas, retorna lo disponible.
+ Genera el tablero 5×5 para el juego.
+ Selecciona 5 categorías aleatorias y 1 pregunta por dificultad.
Returns:
Dict con category_id como string (para JSON) -> lista de preguntas
"""
- board = await self.get_daily_questions(db, target_date)
+ full_board = await self.get_daily_questions(db, target_date)
- # Convertir keys a string para JSON
- return {str(k): v for k, v in board.items()}
+ if not full_board:
+ return {}
+
+ # Get available category IDs that have questions
+ available_categories = list(full_board.keys())
+
+ # Select random categories (up to CATEGORIES_PER_GAME)
+ num_categories = min(CATEGORIES_PER_GAME, len(available_categories))
+ selected_categories = random.sample(available_categories, num_categories)
+
+ # Build the game board with selected categories
+ game_board: Dict[str, List[dict]] = {}
+
+ for cat_id in selected_categories:
+ questions_by_difficulty: Dict[int, List[dict]] = {}
+
+ # Group questions by difficulty
+ for q in full_board[cat_id]:
+ diff = q["difficulty"]
+ if diff not in questions_by_difficulty:
+ questions_by_difficulty[diff] = []
+ questions_by_difficulty[diff].append(q)
+
+ # Select one random question per difficulty
+ selected_questions = []
+ for difficulty in range(1, 6): # 1-5
+ if difficulty in questions_by_difficulty:
+ questions = questions_by_difficulty[difficulty]
+ selected_q = random.choice(questions)
+ selected_questions.append(selected_q)
+
+ if selected_questions:
+ game_board[str(cat_id)] = selected_questions
+
+ return game_board
async def get_question_by_id(
self,
diff --git a/backend/app/services/room_manager.py b/backend/app/services/room_manager.py
index f68266a..97e6e56 100644
--- a/backend/app/services/room_manager.py
+++ b/backend/app/services/room_manager.py
@@ -59,9 +59,9 @@ class RoomManager:
)
# Add player to room
- await self.add_player(room_code, player_name, "A", socket_id)
+ room = await self.add_player(room_code, player_name, "A", socket_id)
- return room_state
+ return room
async def get_room(self, room_code: str) -> Optional[dict]:
"""Get room state by code."""
diff --git a/backend/app/sockets/game_events.py b/backend/app/sockets/game_events.py
index db512d1..e8e6f41 100644
--- a/backend/app/sockets/game_events.py
+++ b/backend/app/sockets/game_events.py
@@ -50,7 +50,7 @@ def register_socket_events(sio: socketio.AsyncServer):
await room_manager.init_player_stats(room["code"], player_name)
# Join socket room
- sio.enter_room(sid, room["code"])
+ await sio.enter_room(sid, room["code"])
await sio.emit("room_created", {"room": room}, to=sid)
@@ -75,7 +75,7 @@ def register_socket_events(sio: socketio.AsyncServer):
await room_manager.init_player_stats(room_code, player_name)
# Join socket room
- sio.enter_room(sid, room_code)
+ await sio.enter_room(sid, room_code)
# Notify all players
await sio.emit("player_joined", {"room": room}, room=room_code)
@@ -147,13 +147,18 @@ def register_socket_events(sio: socketio.AsyncServer):
)
return
- # Get board from data or generate
- board = data.get("board", {})
-
- updated_room = await game_manager.start_game(room_code, board)
+ # Load board from database and start game
+ async with await get_db_session() as db:
+ updated_room = await game_manager.start_game_with_db(db, room_code)
if updated_room:
await sio.emit("game_started", {"room": updated_room}, room=room_code)
+ else:
+ await sio.emit(
+ "error",
+ {"message": "No hay preguntas disponibles para hoy. Contacta al administrador."},
+ to=sid
+ )
@sio.event
async def select_question(sid, data):
diff --git a/frontend/src/components/ui/SoundControl.tsx b/frontend/src/components/ui/SoundControl.tsx
index 6a59195..df05905 100644
--- a/frontend/src/components/ui/SoundControl.tsx
+++ b/frontend/src/components/ui/SoundControl.tsx
@@ -7,15 +7,12 @@ import { useThemeStyles } from '../../themes/ThemeProvider'
interface SoundControlProps {
/** Compact mode shows just the icon, expanded shows slider */
compact?: boolean
- /** Position for the popup menu when in compact mode */
- popupPosition?: 'top' | 'bottom' | 'left' | 'right'
/** Custom class name */
className?: string
}
export default function SoundControl({
compact = false,
- popupPosition = 'top',
className = '',
}: SoundControlProps) {
const { volume, muted, setVolume, toggleMute } = useSoundStore()
@@ -84,19 +81,6 @@ export default function SoundControl({
)
}
- const getPopupStyles = () => {
- switch (popupPosition) {
- case 'top':
- return 'bottom-full mb-2 left-1/2 -translate-x-1/2'
- case 'bottom':
- return 'top-full mt-2 left-1/2 -translate-x-1/2'
- case 'left':
- return 'right-full mr-2 top-1/2 -translate-y-1/2'
- case 'right':
- return 'left-full ml-2 top-1/2 -translate-y-1/2'
- }
- }
-
if (!compact) {
return (
-
+
-
handleVolumeChange(parseFloat(e.target.value))}
- className="w-full h-2 rounded-lg appearance-none cursor-pointer"
- style={{
- background: `linear-gradient(to right, ${config.colors.primary} 0%, ${config.colors.primary} ${(muted ? 0 : volume) * 100}%, ${config.colors.text}30 ${(muted ? 0 : volume) * 100}%, ${config.colors.text}30 100%)`,
- }}
- />
+ {/* Vertical slider container */}
+
+ handleVolumeChange(parseFloat(e.target.value))}
+ className="h-20 w-2 rounded-lg appearance-none cursor-pointer"
+ style={{
+ writingMode: 'vertical-lr',
+ direction: 'rtl',
+ background: `linear-gradient(to top, ${config.colors.primary} 0%, ${config.colors.primary} ${(muted ? 0 : volume) * 100}%, ${config.colors.text}30 ${(muted ? 0 : volume) * 100}%, ${config.colors.text}30 100%)`,
+ }}
+ />
+
(null)
- const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd, setGameResult, addReaction, addTeamMessage } =
+ const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setGameResult, addReaction, addTeamMessage } =
useGameStore()
// Initialize sound player with current theme
@@ -27,23 +24,16 @@ export function useSocket() {
soundPlayer.loadTheme(currentTheme)
useEffect(() => {
- // Create socket connection
- socketRef.current = io(SOCKET_URL, {
- transports: ['websocket', 'polling'],
- autoConnect: true,
- })
+ // Get singleton socket connection
+ const socket = socketService.connect()
- const socket = socketRef.current
-
- // Connection events
- socket.on('connect', () => {
- console.log('Connected to server')
- })
-
- socket.on('disconnect', () => {
- console.log('Disconnected from server')
- })
+ // Only set up listeners once globally
+ if (socketService.isInitialized) {
+ return // No cleanup - socket persists
+ }
+ socketService.setInitialized()
+ // Error handler
socket.on('error', (data: { message: string }) => {
console.error('Socket error:', data.message)
})
@@ -55,6 +45,9 @@ export function useSocket() {
socket.on('player_joined', (data: { room: GameRoom }) => {
setRoom(data.room)
+ // Play sound when a player joins
+ const volume = useSoundStore.getState().volume
+ soundPlayer.play('player_join', volume)
})
socket.on('player_left', (data: { room: GameRoom }) => {
@@ -68,11 +61,22 @@ export function useSocket() {
// Game events
socket.on('game_started', (data: { room: GameRoom }) => {
setRoom(data.room)
+ // Play game start sound
+ const volume = useSoundStore.getState().volume
+ soundPlayer.play('game_start', volume)
})
socket.on('question_selected', (data: { room: GameRoom; question_id: number }) => {
setRoom(data.room)
- // Fetch full question details
+ // Find the question in the board and set it as current
+ const questionId = data.question_id
+ for (const categoryQuestions of Object.values(data.room.board || {})) {
+ const question = (categoryQuestions as Array<{ id: number }>).find(q => q.id === questionId)
+ if (question) {
+ setCurrentQuestion(question as unknown as import('../types').Question)
+ break
+ }
+ }
})
socket.on('answer_result', (data: AnswerResult) => {
@@ -82,12 +86,17 @@ export function useSocket() {
const volume = useSoundStore.getState().volume
if (data.valid) {
soundPlayer.play('correct', volume)
+ // Clear current question after correct answer
+ setCurrentQuestion(null)
} else {
soundPlayer.play('incorrect', volume)
}
if (!data.valid && !data.was_steal && data.room.can_steal) {
setShowStealPrompt(true)
+ } else if (data.was_steal) {
+ // Clear question after steal attempt (success or fail)
+ setCurrentQuestion(null)
}
})
@@ -103,6 +112,7 @@ export function useSocket() {
socket.on('steal_passed', (data: { room: GameRoom }) => {
setRoom(data.room)
setShowStealPrompt(false)
+ setCurrentQuestion(null)
})
socket.on('time_up', (data: { room: GameRoom; was_steal: boolean }) => {
@@ -111,6 +121,7 @@ export function useSocket() {
setShowStealPrompt(true)
} else {
setShowStealPrompt(false)
+ setCurrentQuestion(null)
}
})
@@ -175,18 +186,16 @@ export function useSocket() {
})
})
- return () => {
- socket.disconnect()
- }
- }, [setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd, setGameResult, addReaction, addTeamMessage])
+ // No cleanup - socket connection persists across components
+ }, [])
- // Socket methods
+ // Socket methods - use singleton service
const createRoom = useCallback((playerName: string) => {
- socketRef.current?.emit('create_room', { player_name: playerName })
+ socketService.emit('create_room', { player_name: playerName })
}, [])
const joinRoom = useCallback((roomCode: string, playerName: string, team: 'A' | 'B') => {
- socketRef.current?.emit('join_room', {
+ socketService.emit('join_room', {
room_code: roomCode,
player_name: playerName,
team,
@@ -194,15 +203,15 @@ export function useSocket() {
}, [])
const changeTeam = useCallback((team: 'A' | 'B') => {
- socketRef.current?.emit('change_team', { team })
+ socketService.emit('change_team', { team })
}, [])
const startGame = useCallback((board: Record) => {
- socketRef.current?.emit('start_game', { board })
+ socketService.emit('start_game', { board })
}, [])
const selectQuestion = useCallback((questionId: number, categoryId: number) => {
- socketRef.current?.emit('select_question', {
+ socketService.emit('select_question', {
question_id: questionId,
category_id: categoryId,
})
@@ -210,7 +219,7 @@ export function useSocket() {
const submitAnswer = useCallback(
(answer: string, question: Record, isSteal: boolean = false) => {
- socketRef.current?.emit('submit_answer', {
+ socketService.emit('submit_answer', {
answer,
question,
is_steal: isSteal,
@@ -220,7 +229,7 @@ export function useSocket() {
)
const stealDecision = useCallback((attempt: boolean, questionId: number, answer?: string) => {
- socketRef.current?.emit('steal_decision', {
+ socketService.emit('steal_decision', {
attempt,
question_id: questionId,
answer,
@@ -228,15 +237,15 @@ export function useSocket() {
}, [])
const sendChatMessage = useCallback((message: string) => {
- socketRef.current?.emit('chat_message', { message })
+ socketService.emit('chat_message', { message })
}, [])
const sendEmojiReaction = useCallback((emoji: string) => {
- socketRef.current?.emit('emoji_reaction', { emoji })
+ socketService.emit('emoji_reaction', { emoji })
}, [])
const sendReaction = useCallback((emoji: string, roomCode?: string, playerName?: string) => {
- socketRef.current?.emit('send_reaction', {
+ socketService.emit('send_reaction', {
emoji,
room_code: roomCode,
player_name: playerName,
@@ -245,7 +254,7 @@ export function useSocket() {
const sendTeamMessage = useCallback(
(message: string, roomCode: string, team: 'A' | 'B', playerName: string) => {
- socketRef.current?.emit('team_message', {
+ socketService.emit('team_message', {
room_code: roomCode,
team,
player_name: playerName,
@@ -256,11 +265,11 @@ export function useSocket() {
)
const notifyTimerExpired = useCallback(() => {
- socketRef.current?.emit('timer_expired', {})
+ socketService.emit('timer_expired', {})
}, [])
return {
- socket: socketRef.current,
+ socket: socketService.connect(),
createRoom,
joinRoom,
changeTeam,
diff --git a/frontend/src/pages/Game.tsx b/frontend/src/pages/Game.tsx
index f468527..fd7ddfb 100644
--- a/frontend/src/pages/Game.tsx
+++ b/frontend/src/pages/Game.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useState, useCallback, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import { useSocket } from '../hooks/useSocket'
@@ -11,16 +11,17 @@ import TeamChat from '../components/chat/TeamChat'
import SoundControl from '../components/ui/SoundControl'
import type { Question } from '../types'
-const categories = [
- { id: 1, name: 'Nintendo', icon: '🍄', color: '#E60012' },
- { id: 2, name: 'Xbox', icon: '🎮', color: '#107C10' },
- { id: 3, name: 'PlayStation', icon: '🎯', color: '#003791' },
- { id: 4, name: 'Anime', icon: '⛩️', color: '#FF6B9D' },
- { id: 5, name: 'Música', icon: '🎵', color: '#1DB954' },
- { id: 6, name: 'Películas', icon: '🎬', color: '#F5C518' },
- { id: 7, name: 'Libros', icon: '📚', color: '#8B4513' },
- { id: 8, name: 'Historia-Cultura', icon: '🏛️', color: '#6B5B95' },
-]
+// All available categories with their styling
+const allCategories: Record = {
+ 1: { name: 'Nintendo', icon: '🍄', color: '#E60012' },
+ 2: { name: 'Xbox', icon: '🎮', color: '#107C10' },
+ 3: { name: 'PlayStation', icon: '🎯', color: '#003791' },
+ 4: { name: 'Anime', icon: '⛩️', color: '#FF6B9D' },
+ 5: { name: 'Música', icon: '🎵', color: '#1DB954' },
+ 6: { name: 'Películas', icon: '🎬', color: '#F5C518' },
+ 7: { name: 'Libros', icon: '📚', color: '#8B4513' },
+ 8: { name: 'Historia', icon: '🏛️', color: '#6B5B95' },
+}
export default function Game() {
useParams<{ roomCode: string }>()
@@ -32,7 +33,7 @@ export default function Game() {
const [answer, setAnswer] = useState('')
const [timeLeft, setTimeLeft] = useState(0)
- const [showingQuestion, setShowingQuestion] = useState(false)
+ const [hoveredCell, setHoveredCell] = useState(null)
// Redirect if game finished
useEffect(() => {
@@ -41,9 +42,16 @@ export default function Game() {
}
}, [room?.status, room?.code, navigate])
+ // Play sound when question is revealed
+ useEffect(() => {
+ if (currentQuestion) {
+ play('question_reveal')
+ }
+ }, [currentQuestion, play])
+
// Timer logic with sound effects
useEffect(() => {
- if (!currentQuestion || !showingQuestion) return
+ if (!currentQuestion) return
setTimeLeft(currentQuestion.time_seconds)
const interval = setInterval(() => {
@@ -52,11 +60,11 @@ export default function Game() {
clearInterval(interval)
return 0
}
- // Play urgent sound when time is running low (5 seconds or less)
- if (prev <= 6 && prev > 1) {
+ if (prev <= 4 && prev > 1) {
+ play('countdown')
+ } else if (prev <= 6 && prev > 4) {
play('timer_urgent')
} else if (prev > 6) {
- // Play tick sound for normal countdown
play('timer_tick')
}
return prev - 1
@@ -64,12 +72,25 @@ export default function Game() {
}, 1000)
return () => clearInterval(interval)
- }, [currentQuestion, showingQuestion, play])
+ }, [currentQuestion, play])
+
+ // Hover sound handler
+ const handleCellHover = useCallback((cellId: string, canSelect: boolean) => {
+ if (canSelect && hoveredCell !== cellId) {
+ setHoveredCell(cellId)
+ play('hover')
+ }
+ }, [hoveredCell, play])
if (!room) {
return (
)
}
@@ -85,164 +106,379 @@ export default function Game() {
if (!amICurrentPlayer || question.answered) return
play('select')
selectQuestion(question.id, categoryId)
- setShowingQuestion(true)
}
const handleSubmitAnswer = () => {
if (!currentQuestion || !answer.trim()) return
submitAnswer(answer, currentQuestion as unknown as Record, room.can_steal)
setAnswer('')
- setShowingQuestion(false)
}
const handleStealDecision = (attempt: boolean) => {
if (!currentQuestion) return
- if (attempt) {
- setShowingQuestion(true)
- } else {
+ if (!attempt) {
stealDecision(false, currentQuestion.id)
}
setShowStealPrompt(false)
}
- // Handler for sending team messages
const handleSendTeamMessage = (message: string) => {
if (room && playerName && myTeam) {
sendTeamMessage(message, room.code, myTeam, playerName)
}
}
- // Determine if the game is active (playing status)
const isGameActive = room.status === 'playing'
+ const timerProgress = currentQuestion ? (timeLeft / currentQuestion.time_seconds) * 100 : 100
+
+ // Get active categories from the board (dynamic based on what backend sends)
+ const activeCategories = useMemo(() => {
+ if (!room.board) return []
+ return Object.keys(room.board).map(id => ({
+ id: parseInt(id),
+ ...allCategories[parseInt(id)] || { name: `Cat ${id}`, icon: '❓', color: '#666' }
+ }))
+ }, [room.board])
+
+ const numCategories = activeCategories.length || 5
return (
-
-
- {/* Scoreboard */}
-
-
+
+ {/* Header with Room Code */}
+
+
- Equipo A
-
- {room.scores.A}
-
-
-
-
-
- Turno de {room.current_team === 'A' ? 'Equipo A' : 'Equipo B'}
-
- {amICurrentPlayer && (
-
- ¡Tu turno!
-
- )}
-
-
-
-
Equipo B
-
- {room.scores.B}
-
+ TRIVY
+
+
+ Sala: {room.code}
- {/* Game Board */}
-
- {/* Category Headers */}
- {categories.map((cat) => (
-
-
{cat.icon}
-
{cat.name}
+ {/* Scoreboard */}
+
+ {/* Team A Score */}
+
+ {room.current_team === 'A' && (
+
+ )}
+
+
+ EQUIPO A
+
+
+ {room.scores.A}
+
+
+ {room.teams.A.map(p => p.name).join(', ')}
+
- ))}
+
+
+ {/* Turn Indicator */}
+
+
+ TURNO
+
+ {room.current_team}
+
+ {amICurrentPlayer && (
+
+ ¡TU TURNO!
+
+ )}
+
+
+
+ {/* Team B Score */}
+
+ {room.current_team === 'B' && (
+
+ )}
+
+
+ EQUIPO B
+
+
+ {room.scores.B}
+
+
+ {room.teams.B.map(p => p.name).join(', ')}
+
+
+
+
+
+ {/* Game Board - Jeopardy Style */}
+
+ {/* Category Headers */}
+
+ {activeCategories.map((cat, index) => (
+
+ {cat.icon}
+
+ {cat.name}
+
+
+ ))}
+
{/* Questions Grid */}
- {[1, 2, 3, 4, 5].map((difficulty) =>
- categories.map((cat) => {
- const questions = room.board[String(cat.id)] || []
- const question = questions.find(q => q.difficulty === difficulty)
- const isAnswered = question?.answered
+ {[1, 2, 3, 4, 5].map((difficulty, rowIndex) => (
+
+ {activeCategories.map((cat, colIndex) => {
+ const questions = room.board[String(cat.id)] || []
+ const question = questions.find(q => q.difficulty === difficulty)
+ const isAnswered = question?.answered
+ const cellId = `${cat.id}-${difficulty}`
+ const canSelect = !isAnswered && amICurrentPlayer && question
- return (
- question && handleSelectQuestion(question, cat.id)}
- disabled={isAnswered || !amICurrentPlayer}
- className={`p-4 rounded transition-all ${
- isAnswered ? 'opacity-30' : amICurrentPlayer ? 'cursor-pointer' : 'cursor-not-allowed opacity-70'
- }`}
- style={{
- backgroundColor: isAnswered ? config.colors.bg : cat.color + '40',
- border: `2px solid ${cat.color}`,
- }}
- >
-
- {difficulty * 100}
-
-
- )
- })
- )}
-
+ return (
+ question && handleSelectQuestion(question, cat.id)}
+ onMouseEnter={() => handleCellHover(cellId, !!canSelect)}
+ onMouseLeave={() => setHoveredCell(null)}
+ disabled={isAnswered || !amICurrentPlayer}
+ className={`relative aspect-[4/3] md:aspect-square flex items-center justify-center transition-all duration-200 ${
+ isAnswered
+ ? 'cursor-default'
+ : canSelect
+ ? 'cursor-pointer'
+ : 'cursor-not-allowed'
+ }`}
+ style={{
+ background: isAnswered
+ ? `linear-gradient(135deg, ${config.colors.bg} 0%, ${config.colors.bg}90 100%)`
+ : `linear-gradient(135deg, ${cat.color}50 0%, ${cat.color}30 100%)`,
+ borderRight: colIndex < numCategories - 1 ? '1px solid rgba(255,255,255,0.1)' : 'none',
+ borderBottom: '1px solid rgba(255,255,255,0.1)',
+ opacity: isAnswered ? 0.3 : (!amICurrentPlayer ? 0.6 : 1),
+ }}
+ >
+ {/* Glow effect on hover */}
+ {canSelect && hoveredCell === cellId && (
+
+ )}
+
+ {/* Points display */}
+
+ ${difficulty * 100}
+
+
+ {/* Answered checkmark */}
+ {isAnswered && (
+
+ ✓
+
+ )}
+
+ )
+ })}
+
+ ))}
+
{/* Question Modal */}
- {showingQuestion && currentQuestion && (
+ {currentQuestion && (
- {/* Timer */}
-
-
- {currentQuestion.points} puntos
-
-
5 ? config.colors.primary : undefined }}
+ {/* Timer Bar */}
+
+ 30 ? config.colors.primary : timerProgress > 15 ? '#FFA500' : '#FF4444',
+ boxShadow: `0 0 10px ${timerProgress > 30 ? config.colors.primary : '#FF4444'}`
+ }}
+ />
+
+
+ {/* Points & Timer */}
+
+
- {timeLeft}s
-
+ ${currentQuestion.points}
+
+
5 ? config.colors.primary : undefined,
+ textShadow: timeLeft <= 5 ? '0 0 20px #FF0000' : `0 0 20px ${config.colors.primary}50`
+ }}
+ >
+ {timeLeft}
+
{/* Question */}
-
- {currentQuestion.question_text || 'Pregunta de ejemplo: ¿En qué año se lanzó la NES?'}
-
+
+ {currentQuestion.question_text}
+
{/* Answer Input */}
{amICurrentPlayer && (
-
+
e.key === 'Enter' && handleSubmitAnswer()}
placeholder="Escribe tu respuesta..."
autoFocus
- className="w-full px-4 py-3 rounded-lg bg-transparent outline-none text-lg"
+ className="w-full px-6 py-4 rounded-xl bg-black/30 outline-none text-xl text-center transition-all focus:ring-2"
style={{
- border: `2px solid ${config.colors.primary}`,
+ border: `3px solid ${config.colors.primary}50`,
color: config.colors.text,
- }}
+ '--tw-ring-color': config.colors.primary,
+ } as React.CSSProperties}
/>
-
-
+ RESPONDER
+
+
)}
{!amICurrentPlayer && (
-
- Esperando respuesta de {currentPlayer?.name}...
-
+
+
+
+
+ Esperando respuesta de {currentPlayer?.name}
+
+
+
)}
@@ -287,68 +542,82 @@ export default function Game() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
- className="fixed inset-0 bg-black/80 flex items-center justify-center p-4 z-50"
+ className="fixed inset-0 bg-black/90 flex items-center justify-center p-4 z-50"
>
-
- ¡Oportunidad de Robo!
+
+ 🎯
+
+
+ ¡OPORTUNIDAD DE ROBO!
-
- El equipo contrario falló. ¿Quieres intentar robar los puntos?
-
- Advertencia: Si fallas, perderás puntos
+
+ El equipo contrario falló
+
+
+ ⚠️ Si fallas, perderás puntos
-
-
+
)}
- {/* Emoji Reactions Bar - Fixed at bottom */}
+ {/* Emoji Reactions Bar */}
- {/* Sound Control - Fixed at top right */}
+ {/* Sound Control */}
-
+
- {/* Reaction Overlay - Full screen overlay for floating reactions */}
- {/* Team Chat - Only visible during the game */}
{isGameActive && (
void>> = new Map()
+ private initialized = false
connect(): Socket {
if (!this.socket) {
+ console.log('Creating new socket connection to:', SOCKET_URL)
this.socket = io(SOCKET_URL, {
transports: ['websocket', 'polling'],
autoConnect: true,
reconnection: true,
- reconnectionAttempts: 5,
+ reconnectionAttempts: 10,
reconnectionDelay: 1000,
})
@@ -24,9 +25,12 @@ class SocketService {
console.log('Socket disconnected:', reason)
})
- this.socket.on('error', (error) => {
- console.error('Socket error:', error)
+ this.socket.on('connect_error', (error) => {
+ console.error('Socket connection error:', error)
})
+ } else if (!this.socket.connected) {
+ console.log('Reconnecting socket...')
+ this.socket.connect()
}
return this.socket
@@ -36,25 +40,35 @@ class SocketService {
if (this.socket) {
this.socket.disconnect()
this.socket = null
+ this.initialized = false
}
}
- on(event: string, callback: (data: unknown) => void): void {
- if (!this.listeners.has(event)) {
- this.listeners.set(event, new Set())
- }
- this.listeners.get(event)!.add(callback)
-
- this.socket?.on(event, callback)
+ on(event: string, callback: (...args: unknown[]) => void): void {
+ const socket = this.connect()
+ // Remove existing listener to prevent duplicates
+ socket.off(event, callback)
+ socket.on(event, callback)
}
- off(event: string, callback: (data: unknown) => void): void {
- this.listeners.get(event)?.delete(callback)
- this.socket?.off(event, callback)
+ off(event: string, callback?: (...args: unknown[]) => void): void {
+ if (callback) {
+ this.socket?.off(event, callback)
+ } else {
+ this.socket?.off(event)
+ }
}
emit(event: string, data?: unknown): void {
- this.socket?.emit(event, data)
+ const socket = this.connect()
+ if (socket.connected) {
+ socket.emit(event, data)
+ } else {
+ // Wait for connection and then emit
+ socket.once('connect', () => {
+ socket.emit(event, data)
+ })
+ }
}
get connected(): boolean {
@@ -64,6 +78,15 @@ class SocketService {
get id(): string | undefined {
return this.socket?.id
}
+
+ // Check if listeners are initialized
+ get isInitialized(): boolean {
+ return this.initialized
+ }
+
+ setInitialized(): void {
+ this.initialized = true
+ }
}
export const socketService = new SocketService()
diff --git a/frontend/src/stores/soundStore.ts b/frontend/src/stores/soundStore.ts
index 66687b6..4c10cf8 100644
--- a/frontend/src/stores/soundStore.ts
+++ b/frontend/src/stores/soundStore.ts
@@ -11,6 +11,11 @@ export type SoundEffect =
| 'victory'
| 'defeat'
| 'select'
+ | 'game_start'
+ | 'player_join'
+ | 'question_reveal'
+ | 'hover'
+ | 'countdown'
interface SoundState {
volume: number
@@ -56,6 +61,11 @@ export const soundPaths: Record> = {
victory: '/sounds/drrr/victory.mp3',
defeat: '/sounds/drrr/defeat.mp3',
select: '/sounds/drrr/select.mp3',
+ game_start: '/sounds/drrr/game_start.mp3',
+ player_join: '/sounds/drrr/player_join.mp3',
+ question_reveal: '/sounds/drrr/question_reveal.mp3',
+ hover: '/sounds/drrr/hover.mp3',
+ countdown: '/sounds/drrr/countdown.mp3',
},
retro: {
correct: '/sounds/retro/correct.mp3',
@@ -66,6 +76,11 @@ export const soundPaths: Record> = {
victory: '/sounds/retro/victory.mp3',
defeat: '/sounds/retro/defeat.mp3',
select: '/sounds/retro/select.mp3',
+ game_start: '/sounds/retro/game_start.mp3',
+ player_join: '/sounds/retro/player_join.mp3',
+ question_reveal: '/sounds/retro/question_reveal.mp3',
+ hover: '/sounds/retro/hover.mp3',
+ countdown: '/sounds/retro/countdown.mp3',
},
minimal: {
correct: '/sounds/minimal/correct.mp3',
@@ -76,6 +91,11 @@ export const soundPaths: Record> = {
victory: '/sounds/minimal/victory.mp3',
defeat: '/sounds/minimal/defeat.mp3',
select: '/sounds/minimal/select.mp3',
+ game_start: '/sounds/minimal/game_start.mp3',
+ player_join: '/sounds/minimal/player_join.mp3',
+ question_reveal: '/sounds/minimal/question_reveal.mp3',
+ hover: '/sounds/minimal/hover.mp3',
+ countdown: '/sounds/minimal/countdown.mp3',
},
rgb: {
correct: '/sounds/rgb/correct.mp3',
@@ -86,6 +106,11 @@ export const soundPaths: Record> = {
victory: '/sounds/rgb/victory.mp3',
defeat: '/sounds/rgb/defeat.mp3',
select: '/sounds/rgb/select.mp3',
+ game_start: '/sounds/rgb/game_start.mp3',
+ player_join: '/sounds/rgb/player_join.mp3',
+ question_reveal: '/sounds/rgb/question_reveal.mp3',
+ hover: '/sounds/rgb/hover.mp3',
+ countdown: '/sounds/rgb/countdown.mp3',
},
anime: {
correct: '/sounds/anime/correct.mp3',
@@ -96,6 +121,11 @@ export const soundPaths: Record> = {
victory: '/sounds/anime/victory.mp3',
defeat: '/sounds/anime/defeat.mp3',
select: '/sounds/anime/select.mp3',
+ game_start: '/sounds/anime/game_start.mp3',
+ player_join: '/sounds/anime/player_join.mp3',
+ question_reveal: '/sounds/anime/question_reveal.mp3',
+ hover: '/sounds/anime/hover.mp3',
+ countdown: '/sounds/anime/countdown.mp3',
},
}
@@ -110,4 +140,9 @@ export const fallbackSoundConfigs: Record