feat: reconexión de sesión + 6 nuevas categorías + corrección de bugs

- Añade sistema de reconexión tras refresh/cierre del navegador
  - Persistencia de sesión en localStorage (3h TTL)
  - Banner de reconexión en Home
  - Evento rejoin_room en backend

- Nuevas categorías: Series TV, Marvel/DC, Disney, Memes, Pokémon, Mitología

- Correcciones de bugs:
  - Fix: juego bloqueado al fallar robo (steal decision)
  - Fix: jugador duplicado al cambiar de equipo
  - Fix: rotación incorrecta de turno tras fallo

- Config: soporte para Cloudflare tunnel (allowedHosts)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 01:53:32 +00:00
parent 6248037b47
commit 112f489e40
9 changed files with 327 additions and 12 deletions

View File

@@ -141,9 +141,17 @@ class GameManager:
else:
# Original team failed - enable steal
failed_team = room["current_team"]
room["can_steal"] = True
# Advance failed team's player index (they had their turn)
team_players = room["teams"][failed_team]
room["current_player_index"][failed_team] = (
room["current_player_index"][failed_team] + 1
) % len(team_players)
# Switch to other team for potential steal
room["current_team"] = "B" if room["current_team"] == "A" else "A"
room["current_team"] = "B" if failed_team == "A" else "A"
# Check if game is over (all questions answered)
all_answered = all(

View File

@@ -168,6 +168,23 @@ class RoomManager:
return json.loads(data)
return None
async def update_player(self, socket_id: str, updates: dict) -> Optional[dict]:
"""Update player info."""
await self.connect()
data = await self.redis.get(f"player:{socket_id}")
if not data:
return None
player = json.loads(data)
player.update(updates)
await self.redis.setex(
f"player:{socket_id}",
3600 * 3,
json.dumps(player)
)
return player
async def get_player_stats(self, room_code: str, player_name: str) -> Optional[dict]:
"""Obtiene stats de un jugador."""
await self.connect()

View File

@@ -1,5 +1,6 @@
import socketio
import time
import json
from datetime import datetime
from app.services.room_manager import room_manager
from app.services.game_manager import game_manager
@@ -80,6 +81,105 @@ def register_socket_events(sio: socketio.AsyncServer):
# Notify all players
await sio.emit("player_joined", {"room": room}, room=room_code)
@sio.event
async def rejoin_room(sid, data):
"""Rejoin an existing room after disconnect/refresh."""
room_code = data.get("room_code", "").upper()
player_name = data.get("player_name", "")
team = data.get("team", "A")
if not room_code or not player_name:
await sio.emit(
"rejoin_failed",
{"message": "Missing room code or player name"},
to=sid
)
return
room = await room_manager.get_room(room_code)
if not room:
await sio.emit(
"rejoin_failed",
{"message": "Room not found or expired"},
to=sid
)
return
# Check if player was in this room (by name)
player_found = False
player_team = None
for t in ["A", "B"]:
for i, p in enumerate(room["teams"][t]):
if p["name"] == player_name:
# Update socket_id for this player
room["teams"][t][i]["socket_id"] = sid
player_found = True
player_team = t
break
if player_found:
break
if not player_found:
# Player not found, try to add them back to their preferred team
if len(room["teams"][team]) >= 4:
# Try other team
other_team = "B" if team == "A" else "A"
if len(room["teams"][other_team]) >= 4:
await sio.emit(
"rejoin_failed",
{"message": "Room is full"},
to=sid
)
return
team = other_team
room["teams"][team].append({
"name": player_name,
"team": team,
"position": len(room["teams"][team]),
"socket_id": sid
})
player_team = team
# Update room and player records
await room_manager.update_room(room_code, room)
await room_manager.update_player(sid, {
"name": player_name,
"room": room_code,
"team": player_team
})
# Also set new player record if it doesn't exist
existing = await room_manager.get_player(sid)
if not existing:
await room_manager.redis.setex(
f"player:{sid}",
3600 * 3,
json.dumps({"name": player_name, "room": room_code, "team": player_team})
)
# Join socket room
await sio.enter_room(sid, room_code)
# Send current game state to rejoining player
await sio.emit(
"rejoin_success",
{
"room": room,
"player_name": player_name,
"team": player_team
},
to=sid
)
# Notify others that player reconnected
await sio.emit(
"player_reconnected",
{"player_name": player_name, "team": player_team, "room": room},
room=room_code,
skip_sid=sid
)
@sio.event
async def change_team(sid, data):
"""Switch player to another team."""
@@ -89,6 +189,11 @@ def register_socket_events(sio: socketio.AsyncServer):
room_code = player["room"]
new_team = data.get("team")
current_team = player["team"]
# Don't do anything if already on that team
if current_team == new_team:
return
room = await room_manager.get_room(room_code)
if not room or len(room["teams"][new_team]) >= 4:
@@ -99,12 +204,16 @@ def register_socket_events(sio: socketio.AsyncServer):
)
return
# Remove from current team
current_team = player["team"]
# Remove from current team (by socket_id to be safe)
room["teams"][current_team] = [
p for p in room["teams"][current_team] if p["socket_id"] != sid
]
# Also remove from new team if somehow already there (prevent duplicates)
room["teams"][new_team] = [
p for p in room["teams"][new_team] if p["socket_id"] != sid
]
# Add to new team
room["teams"][new_team].append({
"name": player["name"],
@@ -113,7 +222,12 @@ def register_socket_events(sio: socketio.AsyncServer):
"socket_id": sid
})
# Update room state
await room_manager.update_room(room_code, room)
# Update player record with new team
await room_manager.update_player(sid, {"team": new_team})
await sio.emit("team_changed", {"room": room}, room=room_code)
@sio.event