Files
farolero/SPEC.md
ShanaiaBot eb2662f561
Some checks failed
Build & Deploy Farolero / Análisis de código (push) Has been cancelled
Build & Deploy Farolero / Build APK + AAB release (push) Has been cancelled
fix: multidispositivo - Random seguro + gestor host + reacción clientes
- 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
2026-04-15 02:09:05 +02:00

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":
    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:

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:

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

// 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() 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