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
This commit is contained in:
196
SPEC.md
Normal file
196
SPEC.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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 = <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
|
||||
Reference in New Issue
Block a user