Files
farolero/lib/estado/estado_juego.dart
Javier Bautista Fernández a8d5b0f002
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
feat: Implement multiplayer game session management
- Add models for managing player assignments and game session initialization in `inicio_partida_multijugador.dart`.
- Create a multiplayer room state management system in `sala_multijugador.dart`, including user registration, selection, and session validation.
- Develop a UI screen for displaying player words sequentially in `pantalla_palabras_cliente.dart`.
- Implement unit tests for the multiplayer session management and player assignment logic in `inicio_partida_multijugador_test.dart` and `sala_multijugador_test.dart`.
2026-04-27 14:02:33 +02:00

293 lines
8.2 KiB
Dart

import 'dart:math';
import 'package:flutter/foundation.dart';
import '../modelos/jugador.dart';
import '../modelos/partida.dart';
import '../modelos/palabra.dart';
import '../modelos/sala_multijugador.dart';
import '../servicios/servicio_notas.dart';
/// Estado global del juego gestionado con Provider
class EstadoJuego extends ChangeNotifier {
BancoPalabras? _banco;
Partida? _partida;
final Map<String, String> _votos = {}; // votanteId -> votadoId
bool _cargando = false;
/// Jugador local del host en modo multi-dispositivo
Jugador? _hostLocal;
BancoPalabras? get banco => _banco;
Partida? get partida => _partida;
Map<String, String> get votos => Map.unmodifiable(_votos);
bool get cargando => _cargando;
/// Jugador local del host (para modo multi-dispositivo)
Jugador? get hostLocal => _hostLocal;
Future<void> cargarBanco() async {
_cargando = true;
notifyListeners();
_banco = await BancoPalabras.cargar();
_cargando = false;
notifyListeners();
}
/// Establece el jugador local del host para modo multi-dispositivo
void setHostJugador(String nombre) {
_hostLocal = Jugador(
id: 'host-local',
nombre: nombre,
endpointId: null, // El host local no tiene endpointId
);
notifyListeners();
}
/// Crea una nueva partida con la configuración dada y lista de jugadores
void crearPartida({
required ConfigPartida config,
required List<String> nombresJugadores,
}) {
if (_banco == null) return;
if (nombresJugadores.length < 3) return;
// Seleccionar palabra
final palabra = _banco!.palabraAleatoria(config.categoria);
final categoriaReal =
_banco!.categoriaDepalabra(palabra) ?? config.categoria;
// Crear jugadores
final jugadores = nombresJugadores.asMap().entries.map((e) {
return Jugador(id: 'j${e.key}', nombre: e.value);
}).toList();
// Asignar impostores usando Random seguro (no predecible)
final rng = Random.secure();
final numImpostores = config.numImpostores.clamp(1, jugadores.length ~/ 3);
final impostoresElegidos = <int>{};
while (impostoresElegidos.length < numImpostores) {
impostoresElegidos.add(rng.nextInt(jugadores.length));
}
for (final i in impostoresElegidos) {
jugadores[i].esImpostor = true;
}
// Asignar palabras
for (final j in jugadores) {
if (!j.esImpostor) {
j.palabra = palabra;
}
}
_partida = Partida(
config: config,
jugadores: jugadores,
palabraSecreta: palabra,
categoriaReal: categoriaReal,
);
_votos.clear();
ServicioNotas.limpiarNotas();
notifyListeners();
}
/// Crea una partida multi-dispositivo usando los usuarios seleccionados de la
/// sala como jugadores reales. La identidad de jugador se conserva por id y
/// cada jugador queda asociado al endpoint del cliente que lo controla.
void crearPartidaDesdeSala({
required ConfigPartida config,
required EstadoSalaMultijugador sala,
}) {
if (_banco == null) return;
final usuariosSeleccionados = sala.usuariosSeleccionados;
if (usuariosSeleccionados.length < 3) return;
final palabra = _banco!.palabraAleatoria(config.categoria);
final categoriaReal =
_banco!.categoriaDepalabra(palabra) ?? config.categoria;
final jugadores = usuariosSeleccionados.map((usuario) {
final clienteId = usuario.clienteIdSeleccionado;
final endpointId = clienteId == null
? null
: sala.clientes[clienteId]?.endpointId;
return Jugador(
id: usuario.id,
nombre: usuario.nombre,
endpointId: endpointId,
);
}).toList();
final rng = Random.secure();
final numImpostores = config.numImpostores.clamp(1, jugadores.length ~/ 3);
final impostoresElegidos = <int>{};
while (impostoresElegidos.length < numImpostores) {
impostoresElegidos.add(rng.nextInt(jugadores.length));
}
for (final i in impostoresElegidos) {
jugadores[i].esImpostor = true;
}
for (final jugador in jugadores) {
if (!jugador.esImpostor) {
jugador.palabra = palabra;
}
}
_partida = Partida(
config: config,
jugadores: jugadores,
palabraSecreta: palabra,
categoriaReal: categoriaReal,
);
_votos.clear();
ServicioNotas.limpiarNotas();
notifyListeners();
}
/// Avanza a la fase de debate
void iniciarDebate() {
if (_partida == null) return;
_partida!.fase = FaseJuego.debate;
notifyListeners();
}
/// Avanza a la fase de votación
void iniciarVotacion() {
if (_partida == null) return;
_partida!.fase = FaseJuego.votacion;
_votos.clear();
notifyListeners();
}
/// Registra un voto (modo un solo móvil)
void registrarVoto(String votanteId, String votadoId) {
_votos[votanteId] = votadoId;
notifyListeners();
}
/// Elimina un voto
void eliminarVoto(String votanteId) {
_votos.remove(votanteId);
notifyListeners();
}
/// Comprueba si todos los jugadores activos (no eliminados) han votado
bool todosHanVotado() {
if (_partida == null) return false;
final activos = _partida!.jugadoresActivos;
return activos.every((j) => _votos.containsKey(j.id));
}
/// Procesa los votos y determina el eliminado
ResultadoVotacion? procesarVotacion() {
if (_partida == null) return null;
// Contar votos
final conteo = <String, int>{};
for (final votado in _votos.values) {
conteo[votado] = (conteo[votado] ?? 0) + 1;
}
if (conteo.isEmpty) return null;
// Encontrar máximo
final maxVotos = conteo.values.reduce(max);
final masVotados = conteo.entries
.where((e) => e.value == maxVotos)
.toList();
// En caso de empate, elegir aleatoriamente (usar Random.secure para consistencia)
final rng = Random.secure();
final eliminadoId = masVotados[rng.nextInt(masVotados.length)].key;
final eliminado = _partida!.jugadores.firstWhere(
(j) => j.id == eliminadoId,
);
eliminado.eliminado = true;
final resultado = ResultadoVotacion(
eliminadoId: eliminadoId,
eliminadoNombre: eliminado.nombre,
eraImpostor: eliminado.esImpostor,
votos: Map.from(_votos),
);
_partida!.historialVotaciones.add(resultado);
_partida!.fase = FaseJuego.resultado;
notifyListeners();
return resultado;
}
/// Comprueba si la partida ha terminado y actualiza el estado
bool comprobarFinPartida() {
if (_partida == null) return false;
final impostoresVivos = _partida!.impostoresActivos.length;
final jugadoresVivos = _partida!.jugadoresNormalesActivos.length;
// Los jugadores ganan si no quedan impostores
if (impostoresVivos == 0) {
_partida!.ganador = 'jugadores';
_partida!.fase = FaseJuego.finPartida;
notifyListeners();
return true;
}
// Los impostores ganan si son >= que los jugadores normales
if (impostoresVivos >= jugadoresVivos) {
_partida!.ganador = 'impostores';
_partida!.fase = FaseJuego.finPartida;
notifyListeners();
return true;
}
return false;
}
/// Avanza a la fase de adivinanza del impostor
void iniciarAdivinanza() {
if (_partida == null) return;
_partida!.fase = FaseJuego.adivinanza;
notifyListeners();
}
/// El impostor intenta adivinar la palabra
bool intentarAdivinar(String intento) {
if (_partida == null) return false;
final acierto =
intento.trim().toLowerCase() ==
_partida!.palabraSecreta.trim().toLowerCase();
if (acierto) {
_partida!.ganador = 'impostores';
_partida!.fase = FaseJuego.finPartida;
notifyListeners();
}
return acierto;
}
/// Inicia la siguiente ronda
void siguienteRonda() {
if (_partida == null) return;
_partida!.rondaActual++;
_partida!.fase = FaseJuego.debate;
_votos.clear();
notifyListeners();
}
/// Revancha: mismos jugadores, nueva palabra
void revancha() {
if (_partida == null || _banco == null) return;
final nombres = _partida!.jugadores.map((j) => j.nombre).toList();
crearPartida(config: _partida!.config, nombresJugadores: nombres);
}
/// Limpia la partida actual
void limpiar() {
_partida = null;
_votos.clear();
notifyListeners();
}
}