feat: Implement multiplayer game session management
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

- 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`.
This commit is contained in:
Javier Bautista Fernández
2026-04-27 14:02:33 +02:00
parent 4a1abd0be0
commit a8d5b0f002
14 changed files with 1779 additions and 421 deletions

View File

@@ -1,18 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:farolero/l10n/generated/app_localizations.dart';
import 'package:farolero/modelos/inicio_partida_multijugador.dart';
import 'package:farolero/modelos/jugador.dart';
import 'package:farolero/tema/tema_app.dart';
/// Pantalla de votación para el cliente (multidispositivo).
/// El cliente recibe fase=votacion y ve esta pantalla para elegir a quién votar.
/// Pantalla de votación para cliente multidispositivo.
/// Un cliente puede manejar uno o varios jugadores, por eso se recoge un voto
/// por cada jugador controlado activo.
class PantallaVotacionCliente extends StatefulWidget {
final List<Jugador> jugadores;
final Function(String votoporId) onVoto;
final List<JugadorInicioPartida> jugadoresControlados;
final Function(Map<String, String> votos) onVotos;
const PantallaVotacionCliente({
super.key,
required this.jugadores,
required this.onVoto,
this.jugadoresControlados = const [],
required this.onVotos,
});
@override
@@ -20,7 +24,14 @@ class PantallaVotacionCliente extends StatefulWidget {
}
class _PantallaVotacionClienteState extends State<PantallaVotacionCliente> {
String? _votoSeleccionado;
final Map<String, String> _votosPorVotante = {};
List<JugadorInicioPartida> get _votantes => widget.jugadoresControlados;
bool get _modoMultiVotante => _votantes.length > 1;
bool get _votacionCompleta {
if (_votantes.isEmpty) return _votosPorVotante.containsKey('_legacy');
return _votantes.every((votante) => _votosPorVotante[votante.jugadorId] != null);
}
@override
Widget build(BuildContext context) {
@@ -45,56 +56,31 @@ class _PantallaVotacionClienteState extends State<PantallaVotacionCliente> {
),
const SizedBox(height: 8),
Text(
l10n.selectOnePlayer,
_modoMultiVotante
? 'Emití un voto por cada jugador que manejás.'
: l10n.selectOnePlayer,
style: TextStyle(color: TemaApp.colorTextoSecundario),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: widget.jugadores.length,
itemBuilder: (context, index) {
final jugador = widget.jugadores[index];
final selected = _votoSeleccionado == jugador.id;
return Card(
color: selected
? TemaApp.colorAcento.withValues(alpha: 0.3)
: TemaApp.colorTarjeta,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: selected
? TemaApp.colorAcento
: TemaApp.colorAcento.withValues(alpha: 0.3),
child: Text(
'${index + 1}',
style: TextStyle(
color: selected
? Colors.white
: TemaApp.colorTexto,
),
),
),
title: Text(jugador.nombre),
trailing: selected
? const Icon(Icons.check_circle,
color: TemaApp.colorAcento)
: null,
onTap: () {
setState(() => _votoSeleccionado = jugador.id);
child: _votantes.isEmpty
? _buildSelectorLegacy()
: ListView.builder(
itemCount: _votantes.length,
itemBuilder: (context, index) {
final votante = _votantes[index];
return _buildSelectorParaVotante(context, votante);
},
),
);
},
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: _votoSeleccionado == null
? null
: () => widget.onVoto(_votoSeleccionado!),
onPressed: _votacionCompleta
? () => widget.onVotos(Map.unmodifiable(_votosPorVotante))
: null,
icon: const Icon(Icons.how_to_vote),
label: Text(l10n.votar),
style: ElevatedButton.styleFrom(
@@ -109,4 +95,87 @@ class _PantallaVotacionClienteState extends State<PantallaVotacionCliente> {
),
);
}
Widget _buildSelectorLegacy() {
return ListView.builder(
itemCount: widget.jugadores.length,
itemBuilder: (context, index) {
final jugador = widget.jugadores[index];
final selected = _votosPorVotante['_legacy'] == jugador.id;
return _buildJugadorVotable(
jugador: jugador,
index: index,
selected: selected,
onTap: () => setState(() => _votosPorVotante['_legacy'] = jugador.id),
);
},
);
}
Widget _buildSelectorParaVotante(
BuildContext context,
JugadorInicioPartida votante,
) {
return Card(
color: TemaApp.colorSuperficie,
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Voto de ${votante.nombre}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
...widget.jugadores.asMap().entries.map((entry) {
final jugador = entry.value;
final selected = _votosPorVotante[votante.jugadorId] == jugador.id;
return _buildJugadorVotable(
jugador: jugador,
index: entry.key,
selected: selected,
onTap: () => setState(
() => _votosPorVotante[votante.jugadorId] = jugador.id,
),
);
}),
],
),
),
);
}
Widget _buildJugadorVotable({
required Jugador jugador,
required int index,
required bool selected,
required VoidCallback onTap,
}) {
return Card(
color: selected
? TemaApp.colorAcento.withValues(alpha: 0.3)
: TemaApp.colorTarjeta,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: selected
? TemaApp.colorAcento
: TemaApp.colorAcento.withValues(alpha: 0.3),
child: Text(
'${index + 1}',
style: TextStyle(
color: selected ? Colors.white : TemaApp.colorTexto,
),
),
),
title: Text(jugador.nombre),
trailing: selected
? const Icon(Icons.check_circle, color: TemaApp.colorAcento)
: null,
onTap: onTap,
),
);
}
}