- Random.secure() para selección de impostores (no predecible) - Random.secure() también en desempate de votación - Nueva PantallaGestorHost para coordinación multi-device - Navegación: host va a gestor tras iniciar, no a pantalla de palabra - PantallaPalabraCliente: cada jugador ve su palabra en su móvil - PantallaDebateCliente: debate con timer y botón solicitar votación - PantallaVotacionCliente: voto desde el móvil del cliente - PantallaUnirse: listener que reacciona a partidaInicio y cambia de fase - Protocolo: listo/voto/solicitoVotacion via Nearby hacia el host - Nuevas cadenas l10n ES
7.0 KiB
7.0 KiB
Farolero — SDD: Multidispositivo completo + Impostor seguro
1. Explore
Hallazgos
Bug 1: El host funciona como juego en un solo móvil
- En
pantalla_crear_partida.dart→_iniciarPartidaMulti(), al pulsar "Iniciar":- Crea la partida localmente ✅
- Envía palabras a clientes via Nearby ✅
- Navega a
PantallaVerPalabra❌
PantallaVerPalabraestá diseñada para juego en un solo móvil (muestra TODOS los jugadores en lista)- El host no necesita ver palabras — es el gestor, no un jugador
Bug 2: Clientes no se enteran de que el juego ha started
ServicioNearbyprocesapartidaInicioy actualiza_palabraRecibida/_soyImpostor✅- Llama a
notifyListeners()✅ - Pero
PantallaUnirse→_buildPantallaEsperano escucha cambios deServicioNearby - Se queda infinamente en "Esperando al host..." aunque la partida ya haya started
Bug 3: Random de impostores predecible
EstadoJuego.crearPartida()usaRandom()con seed del sistema (determinista)indices.shuffle(rng)mezcla un array [0,1,2,...,n-1] — la mezcla depende del estado inicial- Si un atacante conoce el número de jugadores y el momento de creación, puede reconstruir la seed
- Solución: usar
Random.secure()(CSPRNG) o al menosRandom.fromSecureRandom()
Restricciones
- Nearby Connections tiene límite de payloads (4KB por mensaje)
- Google Nearby Connections requiere Bluetooth + Location permissions en Android/iOS
- La palabra secreta NO debe enviarse en texto plano a la red ( Nearby es inseguro en teoría — cualquier dispositivo puede sniffear)
- Compatibilidad hacia atrás: modo un solo móvil no debe romperse
2. Propose
Cambio 1: Pantalla Gestor Host (multidispositivo)
El host en modo multi-device necesita una pantalla que:
- Muestre la lista de jugadores conectados y sus estados (listo para debate/votación)
- Permita avanzar de fase (debate → votación → resultado)
- NO muestre palabras a nadie
- Gestione la coordinación de la partida completa
Pantallas afectadas/creadas:
- Nueva:
pantalla_gestor_host.dart - Modificar:
pantalla_crear_partida.dart(onIniciar → navegar a gestor) - Modificar:
pantalla_lobby_host.dart(el QR se queda ahí)
Cambio 2: Reacción de clientes al inicio de partida
Los clientes deben:
- Escuchar cambios en
ServicioNearby(registrar listener) - Cuando llega
partidaInicio, navegar automáticamente a la pantalla de ver su palabra - Crear pantalla dedicada para cliente:
pantalla_palabra_cliente.dart
Pantallas afectadas/creadas:
- Nueva:
pantalla_palabra_cliente.dart - Nueva:
pantalla_debate_cliente.dart - Nueva:
pantalla_votacion_cliente.dart - Modificar:
pantalla_unirse.dart(registrar listener y reaccionar)
Cambio 3: Impostor con Random seguro
Reemplazar Random() por Random.secure() en EstadoJuego.crearPartida().
Archivos afectados:
lib/estado/estado_juego.dart
3. Spec
3.1 PantallaGestorHost
Props:
class PantallaGestorHost extends StatelessWidget {
final VoidCallback onPartidaFin;
}
Responsabilidades:
- Escuchar cambios de
EstadoJuego(fase actual) - Escuchar cambios de
ServicioNearby(desconexiones) - Botones para avanzar de fase
- Mostrar estado de cada jugador (en debate, votando, eliminado)
- NO mostrar palabras a nadie
Estados:
debate: muestra timer si hay tiempo configurado, botón "Iniciar votación"votacion: muestra quién ha votado, botón "Ver resultados" cuando todos votedresultado: muestra quién fue eliminado y si era impostoradivinanza: el impostor剩余 intenta adivinarfinPartida: muestra ganador
3.2 PantallaPalabraCliente
Props:
class PantallaPalabraCliente extends StatelessWidget {
final String palabra;
final bool esImpostor;
final String? pistaCategoria;
final VoidCallback onVisto;
}
Responsabilidades:
- Mostrar la palabra solo al jugador correspondiente
- Si es impostor: mostrar pista de categoría (si está activa)
- Mantener pulsado para revelar (mismo UX que
_PantallaRevelarPalabra) - Al pulsar "Ya la he visto" → enviar
listoal host via Nearby - Esperar al host que inicie el debate
3.3 Sincronización de fases
Protocolo de mensajes Via Nearby:
HOST → CLIENTE:
partidaInicio → { palabra, esImpostor, categoria, numJugadores }
fase → { fase: "debate"|"votacion"|"resultado"|"adivinanza"|"fin" }
votacionResultado → { eliminadoId, eliminadoNombre, eraImpostor, votos }
impostorAdivina → { acierto: bool }
partidaFin → { ganador: "jugadores"|"impostores" }
CLIENTE → HOST:
listo → {} (el cliente ha visto su palabra y está listo)
voto → { votoporId }
3.4 Random seguro para impostores
// ANTES (predecible):
final rng = Random();
final indices = List.generate(jugadores.length, (i) => i);
indices.shuffle(rng);
// DESPUÉS (seguro):
final rng = Random.secure();
final impostoresElegidos = <int>{};
while (impostoresElegidos.length < numImpostores) {
impostoresElegidos.add(rng.nextInt(jugadores.length));
}
4. Tasks
Epic 1: Impostor seguro
- Task 1.1: Cambiar
Random()porRandom.secure()enEstadoJuego.crearPartida()
Epic 2: PantallaGestorHost
- Task 2.1: Crear
pantalla_gestor_host.dartcon estructura de fases - Task 2.2: Implementar transición
lobby → gestorenpantalla_crear_partida.dart - Task 2.3: Implementar envío de cambio de fase a clientes
- Task 2.4: Gestionar desconexiones de jugadores durante la partida
Epic 3: Cliente recibe inicio de partida
- Task 3.1: Registrar listener en
PantallaUnirseparapartidaInicio - Task 3.2: Crear
pantalla_palabra_cliente.dart(similar a_PantallaRevelarPalabrapero StatelessWidget) - Task 3.3: Enviar mensaje
listoal host cuando el cliente confirma que ha visto la palabra
Epic 4: Cliente en fases de juego
- Task 4.1: Crear
pantalla_debate_cliente.dart(muestra countdown, botón para solicitar votación) - Task 4.2: Crear
pantalla_votacion_cliente.dart(votar desde el móvil del cliente) - Task 4.3: Cliente reacciona a
fase: votaciony muestra pantalla de voto - Task 4.4: Cliente reacciona a
fase: resultadoy muestra quién fue eliminado - Task 4.5: Si el cliente es impostor剩余, mostrar pantalla de adivinanza
Epic 5: Testing
- Task 5.1: Probar flujo completo multi-device (3+ jugadores)
- Task 5.2: Probar desconexión de un jugador durante la partida
- Task 5.3: Probar que en modo un solo móvil no se rompe nada
5. Apply
(Ejecución por agente OpenCode)
6. Verify
- Flujo multi-device: host inicia → clientes ven sus palabras → debate → votan → resultado
- Modo un solo móvil sigue funcionando exactamente igual que antes
- Impostores se seleccionan con Random.secure() — no reproducibles
- Sin regresiones en localized strings