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`.
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user