feat(phase6): Add sounds, team chat, reactions, monitor, settings, and CSV import/export

Sound System:
- Add soundStore with volume/mute persistence
- Add useSound hook with Web Audio API fallback
- Add SoundControl component for in-game volume adjustment
- Play sounds for correct/incorrect, steal, timer, victory/defeat

Team Chat:
- Add TeamChat component with collapsible panel
- Add team_message WebSocket event (team-only visibility)
- Store up to 50 messages per session

Emoji Reactions:
- Add EmojiReactions bar with 8 emojis
- Add ReactionOverlay with floating animations (Framer Motion)
- Add rate limiting (1 reaction per 3 seconds)
- Broadcast reactions to all players in room

Admin Monitor:
- Add Monitor page showing active rooms from Redis
- Display player counts, team composition, status
- Add ability to close problematic rooms

Admin Settings:
- Add Settings page for game configuration
- Configure points/times by difficulty, steal penalty, max players
- Store config in JSON file with service helpers

CSV Import/Export:
- Add export endpoint with optional filters
- Add import endpoint with validation and error reporting
- Add UI buttons and import result modal in Questions page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 08:58:33 +00:00
parent 90fa220890
commit 720432702f
23 changed files with 2753 additions and 51 deletions

View File

@@ -1,4 +1,5 @@
import socketio
import time
from datetime import datetime
from app.services.room_manager import room_manager
from app.services.game_manager import game_manager
@@ -8,6 +9,11 @@ from app.schemas.achievement import PlayerStats
from app.models.base import get_async_session
# Rate limiting para reacciones: {room_code: {player_name: last_reaction_timestamp}}
reaction_rate_limits: dict[str, dict[str, float]] = {}
REACTION_COOLDOWN_SECONDS = 3
async def get_db_session():
"""Helper para obtener sesion de BD en contexto de socket."""
AsyncSessionLocal = get_async_session()
@@ -353,13 +359,56 @@ def register_socket_events(sio: socketio.AsyncServer):
)
@sio.event
async def emoji_reaction(sid, data):
"""Send an emoji reaction visible to all."""
async def team_message(sid, data):
"""Send a team chat message - only visible to teammates."""
room_code = data.get("room_code", "")
team = data.get("team", "")
player_name = data.get("player_name", "")
message = data.get("message", "")[:500] # Limit message length
if not all([room_code, team, player_name, message]):
return
# Validate player exists in room
player = await room_manager.get_player(sid)
if not player or player["room"] != room_code or player["team"] != team:
return
# Get room data to find team members
room = await room_manager.get_room(room_code)
if not room:
return
# Get socket IDs of team members
team_sockets = [
p["socket_id"] for p in room["teams"][team]
if p.get("socket_id")
]
# Emit only to team members
message_data = {
"player_name": player_name,
"team": team,
"message": message,
"timestamp": datetime.utcnow().isoformat()
}
for socket_id in team_sockets:
await sio.emit(
"receive_team_message",
message_data,
to=socket_id
)
@sio.event
async def send_reaction(sid, data):
"""Send an emoji reaction visible to all players in the room."""
player = await room_manager.get_player(sid)
if not player:
return
room_code = player["room"]
room_code = data.get("room_code", player["room"])
player_name = data.get("player_name", player["name"])
emoji = data.get("emoji", "")
# Validate emoji
@@ -367,16 +416,37 @@ def register_socket_events(sio: socketio.AsyncServer):
if emoji not in allowed_emojis:
return
# Rate limiting: max 1 reaction every 3 seconds per player
current_time = time.time()
if room_code not in reaction_rate_limits:
reaction_rate_limits[room_code] = {}
last_reaction = reaction_rate_limits[room_code].get(player_name, 0)
if current_time - last_reaction < REACTION_COOLDOWN_SECONDS:
# Player is rate limited, ignore the reaction
return
# Update last reaction time
reaction_rate_limits[room_code][player_name] = current_time
# Emit to ALL players in the room (both teams)
await sio.emit(
"emoji_reaction",
"receive_reaction",
{
"player_name": player["name"],
"player_name": player_name,
"team": player["team"],
"emoji": emoji
"emoji": emoji,
"timestamp": datetime.utcnow().isoformat()
},
room=room_code
)
# Keep old event name for backwards compatibility
@sio.event
async def emoji_reaction(sid, data):
"""Alias for send_reaction (backwards compatibility)."""
await send_reaction(sid, data)
@sio.event
async def timer_expired(sid, data):
"""Handle timer expiration."""