# 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": 1. Crea la partida localmente ✅ 2. Envía palabras a clientes via Nearby ✅ 3. Navega a `PantallaVerPalabra` ❌ - `PantallaVerPalabra` está 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** - `ServicioNearby` procesa `partidaInicio` y actualiza `_palabraRecibida` / `_soyImpostor` ✅ - Llama a `notifyListeners()` ✅ - Pero `PantallaUnirse` → `_buildPantallaEspera` no escucha cambios de `ServicioNearby` - Se queda infinamente en "Esperando al host..." aunque la partida ya haya started **Bug 3: Random de impostores predecible** - `EstadoJuego.crearPartida()` usa `Random()` 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 menos `Random.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:** ```dart 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 voted - `resultado`: muestra quién fue eliminado y si era impostor - `adivinanza`: el impostor剩余 intenta adivinar - `finPartida`: muestra ganador ### 3.2 PantallaPalabraCliente **Props:** ```dart 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 `listo` al 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 ```dart // 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 = {}; while (impostoresElegidos.length < numImpostores) { impostoresElegidos.add(rng.nextInt(jugadores.length)); } ``` --- ## 4. Tasks ### Epic 1: Impostor seguro - [ ] **Task 1.1**: Cambiar `Random()` por `Random.secure()` en `EstadoJuego.crearPartida()` ### Epic 2: PantallaGestorHost - [ ] **Task 2.1**: Crear `pantalla_gestor_host.dart` con estructura de fases - [ ] **Task 2.2**: Implementar transición `lobby → gestor` en `pantalla_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 `PantallaUnirse` para `partidaInicio` - [ ] **Task 3.2**: Crear `pantalla_palabra_cliente.dart` (similar a `_PantallaRevelarPalabra` pero StatelessWidget) - [ ] **Task 3.3**: Enviar mensaje `listo` al 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: votacion` y muestra pantalla de voto - [ ] **Task 4.4**: Cliente reacciona a `fase: resultado` y 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