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:
117
lib/modelos/inicio_partida_multijugador.dart
Normal file
117
lib/modelos/inicio_partida_multijugador.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
class AsignacionJugador {
|
||||
final String jugadorId;
|
||||
final String nombre;
|
||||
final String clientId;
|
||||
final String? endpointId;
|
||||
|
||||
const AsignacionJugador({
|
||||
required this.jugadorId,
|
||||
required this.nombre,
|
||||
required this.clientId,
|
||||
required this.endpointId,
|
||||
});
|
||||
}
|
||||
|
||||
class JugadorInicioPartida {
|
||||
final String jugadorId;
|
||||
final String nombre;
|
||||
final bool esImpostor;
|
||||
final String? palabra;
|
||||
|
||||
const JugadorInicioPartida({
|
||||
required this.jugadorId,
|
||||
required this.nombre,
|
||||
required this.esImpostor,
|
||||
required this.palabra,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'jugadorId': jugadorId,
|
||||
'nombre': nombre,
|
||||
'esImpostor': esImpostor,
|
||||
if (palabra != null) 'palabra': palabra,
|
||||
};
|
||||
|
||||
factory JugadorInicioPartida.fromJson(Map<String, dynamic> json) {
|
||||
return JugadorInicioPartida(
|
||||
jugadorId: json['jugadorId'] as String,
|
||||
nombre: json['nombre'] as String,
|
||||
esImpostor: json['esImpostor'] as bool? ?? false,
|
||||
palabra: json['palabra'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InicioPartidaCliente {
|
||||
final String clientId;
|
||||
final String? endpointId;
|
||||
final String categoria;
|
||||
final List<JugadorInicioPartida> jugadores;
|
||||
|
||||
const InicioPartidaCliente({
|
||||
required this.clientId,
|
||||
required this.endpointId,
|
||||
required this.categoria,
|
||||
required this.jugadores,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'clientId': clientId,
|
||||
if (endpointId != null) 'endpointId': endpointId,
|
||||
'categoria': categoria,
|
||||
'jugadores': jugadores.map((jugador) => jugador.toJson()).toList(),
|
||||
};
|
||||
|
||||
factory InicioPartidaCliente.fromJson(Map<String, dynamic> json) {
|
||||
return InicioPartidaCliente(
|
||||
clientId: json['clientId'] as String,
|
||||
endpointId: json['endpointId'] as String?,
|
||||
categoria: json['categoria'] as String,
|
||||
jugadores: (json['jugadores'] as List<dynamic>? ?? [])
|
||||
.map((jugadorJson) => JugadorInicioPartida.fromJson(
|
||||
jugadorJson as Map<String, dynamic>,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InicioPartidaMultijugador {
|
||||
static Map<String, InicioPartidaCliente> crearPayloadsPorCliente({
|
||||
required List<AsignacionJugador> asignaciones,
|
||||
required String palabraSecreta,
|
||||
required String categoria,
|
||||
required Map<String, bool> impostoresPorJugadorId,
|
||||
}) {
|
||||
final payloads = <String, InicioPartidaCliente>{};
|
||||
|
||||
for (final asignacion in asignaciones) {
|
||||
final esImpostor = impostoresPorJugadorId[asignacion.jugadorId] ?? false;
|
||||
final payloadActual = payloads[asignacion.clientId];
|
||||
final jugador = JugadorInicioPartida(
|
||||
jugadorId: asignacion.jugadorId,
|
||||
nombre: asignacion.nombre,
|
||||
esImpostor: esImpostor,
|
||||
palabra: esImpostor ? null : palabraSecreta,
|
||||
);
|
||||
|
||||
if (payloadActual == null) {
|
||||
payloads[asignacion.clientId] = InicioPartidaCliente(
|
||||
clientId: asignacion.clientId,
|
||||
endpointId: asignacion.endpointId,
|
||||
categoria: categoria,
|
||||
jugadores: [jugador],
|
||||
);
|
||||
} else {
|
||||
payloads[asignacion.clientId] = InicioPartidaCliente(
|
||||
clientId: payloadActual.clientId,
|
||||
endpointId: payloadActual.endpointId,
|
||||
categoria: payloadActual.categoria,
|
||||
jugadores: [...payloadActual.jugadores, jugador],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return payloads;
|
||||
}
|
||||
}
|
||||
294
lib/modelos/sala_multijugador.dart
Normal file
294
lib/modelos/sala_multijugador.dart
Normal file
@@ -0,0 +1,294 @@
|
||||
import 'usuario.dart';
|
||||
|
||||
enum FaseSalaMultijugador { lobby, enPartida, finalizada }
|
||||
|
||||
class ResultadoOperacionSala {
|
||||
final bool exitoso;
|
||||
final String? codigo;
|
||||
final String? mensaje;
|
||||
|
||||
const ResultadoOperacionSala._({
|
||||
required this.exitoso,
|
||||
this.codigo,
|
||||
this.mensaje,
|
||||
});
|
||||
|
||||
const ResultadoOperacionSala.ok([String? mensaje])
|
||||
: this._(exitoso: true, mensaje: mensaje);
|
||||
|
||||
const ResultadoOperacionSala.error(String codigo, [String? mensaje])
|
||||
: this._(exitoso: false, codigo: codigo, mensaje: mensaje);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'exitoso': exitoso,
|
||||
if (codigo != null) 'codigo': codigo,
|
||||
if (mensaje != null) 'mensaje': mensaje,
|
||||
};
|
||||
}
|
||||
|
||||
class ClienteSala {
|
||||
final String clientId;
|
||||
final String? endpointId;
|
||||
final String nombre;
|
||||
final bool esHost;
|
||||
final bool conectado;
|
||||
|
||||
const ClienteSala({
|
||||
required this.clientId,
|
||||
this.endpointId,
|
||||
required this.nombre,
|
||||
this.esHost = false,
|
||||
this.conectado = true,
|
||||
});
|
||||
|
||||
ClienteSala copiar({
|
||||
String? clientId,
|
||||
String? endpointId,
|
||||
String? nombre,
|
||||
bool? esHost,
|
||||
bool? conectado,
|
||||
}) {
|
||||
return ClienteSala(
|
||||
clientId: clientId ?? this.clientId,
|
||||
endpointId: endpointId ?? this.endpointId,
|
||||
nombre: nombre ?? this.nombre,
|
||||
esHost: esHost ?? this.esHost,
|
||||
conectado: conectado ?? this.conectado,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'clientId': clientId,
|
||||
if (endpointId != null) 'endpointId': endpointId,
|
||||
'nombre': nombre,
|
||||
'esHost': esHost,
|
||||
'conectado': conectado,
|
||||
};
|
||||
|
||||
factory ClienteSala.fromJson(Map<String, dynamic> json) => ClienteSala(
|
||||
clientId: json['clientId'] as String,
|
||||
endpointId: json['endpointId'] as String?,
|
||||
nombre: json['nombre'] as String,
|
||||
esHost: json['esHost'] as bool? ?? false,
|
||||
conectado: json['conectado'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
class EstadoSalaMultijugador {
|
||||
final String roomId;
|
||||
final String nombreSala;
|
||||
final String hostClientId;
|
||||
FaseSalaMultijugador fase;
|
||||
final Map<String, ClienteSala> clientes;
|
||||
final Map<String, Usuario> usuarios;
|
||||
|
||||
EstadoSalaMultijugador({
|
||||
required this.roomId,
|
||||
required this.nombreSala,
|
||||
required this.hostClientId,
|
||||
this.fase = FaseSalaMultijugador.lobby,
|
||||
Map<String, ClienteSala>? clientes,
|
||||
Map<String, Usuario>? usuarios,
|
||||
}) : clientes = clientes ?? {},
|
||||
usuarios = usuarios ?? {};
|
||||
|
||||
factory EstadoSalaMultijugador.crear({
|
||||
required String roomId,
|
||||
required String nombreSala,
|
||||
required String hostClientId,
|
||||
required String hostNombre,
|
||||
}) {
|
||||
final sala = EstadoSalaMultijugador(
|
||||
roomId: roomId,
|
||||
nombreSala: nombreSala,
|
||||
hostClientId: hostClientId,
|
||||
);
|
||||
sala.registrarCliente(
|
||||
ClienteSala(
|
||||
clientId: hostClientId,
|
||||
nombre: hostNombre,
|
||||
esHost: true,
|
||||
),
|
||||
);
|
||||
return sala;
|
||||
}
|
||||
|
||||
List<Usuario> get usuariosSeleccionados =>
|
||||
usuarios.values.where((usuario) => usuario.estaSeleccionado).toList();
|
||||
|
||||
List<Usuario> get usuariosDisponibles =>
|
||||
usuarios.values.where((usuario) => usuario.estaDisponible).toList();
|
||||
|
||||
int get cantidadUsuariosSeleccionados => usuariosSeleccionados.length;
|
||||
|
||||
List<Usuario> usuariosPorCliente(String clientId) {
|
||||
return usuarios.values
|
||||
.where((usuario) => usuario.clienteIdSeleccionado == clientId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
ClienteSala? clientePorEndpoint(String endpointId) {
|
||||
for (final cliente in clientes.values) {
|
||||
if (cliente.endpointId == endpointId) return cliente;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ResultadoOperacionSala registrarCliente(ClienteSala cliente) {
|
||||
clientes[cliente.clientId] = cliente;
|
||||
return const ResultadoOperacionSala.ok();
|
||||
}
|
||||
|
||||
ResultadoOperacionSala crearUsuario(Usuario usuario) {
|
||||
if (fase != FaseSalaMultijugador.lobby) {
|
||||
return const ResultadoOperacionSala.error('sala_cerrada');
|
||||
}
|
||||
if (usuarios.containsKey(usuario.id)) {
|
||||
return const ResultadoOperacionSala.error('usuario_duplicado');
|
||||
}
|
||||
final nombreNormalizado = usuario.nombre.trim().toLowerCase();
|
||||
final nombreExiste = usuarios.values.any(
|
||||
(u) => u.nombre.trim().toLowerCase() == nombreNormalizado,
|
||||
);
|
||||
if (nombreExiste) {
|
||||
return const ResultadoOperacionSala.error('nombre_duplicado');
|
||||
}
|
||||
usuarios[usuario.id] = usuario;
|
||||
return const ResultadoOperacionSala.ok();
|
||||
}
|
||||
|
||||
ResultadoOperacionSala seleccionarUsuario({
|
||||
required String usuarioId,
|
||||
required String clienteId,
|
||||
}) {
|
||||
if (fase != FaseSalaMultijugador.lobby) {
|
||||
return const ResultadoOperacionSala.error('sala_cerrada');
|
||||
}
|
||||
final cliente = clientes[clienteId];
|
||||
if (cliente == null || !cliente.conectado) {
|
||||
return const ResultadoOperacionSala.error('cliente_no_disponible');
|
||||
}
|
||||
final usuario = usuarios[usuarioId];
|
||||
if (usuario == null) {
|
||||
return const ResultadoOperacionSala.error('usuario_no_existe');
|
||||
}
|
||||
if (usuario.clienteIdSeleccionado == clienteId) {
|
||||
return const ResultadoOperacionSala.ok();
|
||||
}
|
||||
if (usuario.clienteIdSeleccionado != null) {
|
||||
return const ResultadoOperacionSala.error('usuario_ya_seleccionado');
|
||||
}
|
||||
usuarios[usuarioId] = usuario.copiar(clienteIdSeleccionado: clienteId);
|
||||
return const ResultadoOperacionSala.ok();
|
||||
}
|
||||
|
||||
ResultadoOperacionSala liberarUsuario({
|
||||
required String usuarioId,
|
||||
required String solicitanteClientId,
|
||||
}) {
|
||||
if (fase != FaseSalaMultijugador.lobby) {
|
||||
return const ResultadoOperacionSala.error('sala_cerrada');
|
||||
}
|
||||
final usuario = usuarios[usuarioId];
|
||||
if (usuario == null) {
|
||||
return const ResultadoOperacionSala.error('usuario_no_existe');
|
||||
}
|
||||
final solicitante = clientes[solicitanteClientId];
|
||||
final puedeLiberar =
|
||||
usuario.clienteIdSeleccionado == solicitanteClientId ||
|
||||
(solicitante?.esHost ?? false);
|
||||
if (!puedeLiberar) {
|
||||
return const ResultadoOperacionSala.error('usuario_de_otro_cliente');
|
||||
}
|
||||
usuarios[usuarioId] = usuario.copiar(liberarSeleccion: true);
|
||||
return const ResultadoOperacionSala.ok();
|
||||
}
|
||||
|
||||
ResultadoOperacionSala eliminarUsuario({
|
||||
required String usuarioId,
|
||||
required String solicitanteClientId,
|
||||
}) {
|
||||
final solicitante = clientes[solicitanteClientId];
|
||||
if (!(solicitante?.esHost ?? false)) {
|
||||
return const ResultadoOperacionSala.error('solo_host');
|
||||
}
|
||||
final usuario = usuarios[usuarioId];
|
||||
if (usuario == null) {
|
||||
return const ResultadoOperacionSala.error('usuario_no_existe');
|
||||
}
|
||||
if (usuario.estaSeleccionado) {
|
||||
return const ResultadoOperacionSala.error('usuario_seleccionado');
|
||||
}
|
||||
usuarios.remove(usuarioId);
|
||||
return const ResultadoOperacionSala.ok();
|
||||
}
|
||||
|
||||
void desconectarCliente(String clientId) {
|
||||
final cliente = clientes[clientId];
|
||||
if (cliente == null) return;
|
||||
clientes[clientId] = cliente.copiar(conectado: false);
|
||||
if (fase != FaseSalaMultijugador.lobby) return;
|
||||
for (final entry in usuarios.entries.toList()) {
|
||||
if (entry.value.clienteIdSeleccionado == clientId) {
|
||||
usuarios[entry.key] = entry.value.copiar(liberarSeleccion: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResultadoOperacionSala validarInicio() {
|
||||
if (fase != FaseSalaMultijugador.lobby) {
|
||||
return const ResultadoOperacionSala.error('sala_cerrada');
|
||||
}
|
||||
if (cantidadUsuariosSeleccionados < 3) {
|
||||
return const ResultadoOperacionSala.error('faltan_jugadores');
|
||||
}
|
||||
if (usuariosPorCliente(hostClientId).isEmpty) {
|
||||
return const ResultadoOperacionSala.error('host_sin_usuario');
|
||||
}
|
||||
return const ResultadoOperacionSala.ok();
|
||||
}
|
||||
|
||||
ResultadoOperacionSala iniciarPartida() {
|
||||
final validacion = validarInicio();
|
||||
if (!validacion.exitoso) return validacion;
|
||||
fase = FaseSalaMultijugador.enPartida;
|
||||
return const ResultadoOperacionSala.ok();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'roomId': roomId,
|
||||
'nombreSala': nombreSala,
|
||||
'hostClientId': hostClientId,
|
||||
'fase': fase.name,
|
||||
'clientes': clientes.values.map((cliente) => cliente.toJson()).toList(),
|
||||
'usuarios': usuarios.values.map((usuario) => usuario.toJson()).toList(),
|
||||
};
|
||||
|
||||
factory EstadoSalaMultijugador.fromJson(Map<String, dynamic> json) {
|
||||
final clientes = <String, ClienteSala>{};
|
||||
for (final clienteJson in json['clientes'] as List<dynamic>? ?? []) {
|
||||
final cliente = ClienteSala.fromJson(
|
||||
clienteJson as Map<String, dynamic>,
|
||||
);
|
||||
clientes[cliente.clientId] = cliente;
|
||||
}
|
||||
|
||||
final usuarios = <String, Usuario>{};
|
||||
for (final usuarioJson in json['usuarios'] as List<dynamic>? ?? []) {
|
||||
final usuario = Usuario.fromJson(usuarioJson as Map<String, dynamic>);
|
||||
usuarios[usuario.id] = usuario;
|
||||
}
|
||||
|
||||
return EstadoSalaMultijugador(
|
||||
roomId: json['roomId'] as String,
|
||||
nombreSala: json['nombreSala'] as String,
|
||||
hostClientId: json['hostClientId'] as String,
|
||||
fase: FaseSalaMultijugador.values.firstWhere(
|
||||
(fase) => fase.name == json['fase'],
|
||||
orElse: () => FaseSalaMultijugador.lobby,
|
||||
),
|
||||
clientes: clientes,
|
||||
usuarios: usuarios,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,53 @@ class Usuario {
|
||||
final String id;
|
||||
final String nombre;
|
||||
final String? avatar;
|
||||
final String? creadoPorClienteId;
|
||||
final String? clienteIdSeleccionado;
|
||||
|
||||
Usuario({required this.id, required this.nombre, this.avatar});
|
||||
Usuario({
|
||||
required this.id,
|
||||
required this.nombre,
|
||||
this.avatar,
|
||||
this.creadoPorClienteId,
|
||||
this.clienteIdSeleccionado,
|
||||
});
|
||||
|
||||
bool get estaSeleccionado => clienteIdSeleccionado != null;
|
||||
bool get estaDisponible => clienteIdSeleccionado == null;
|
||||
|
||||
Usuario copiar({
|
||||
String? id,
|
||||
String? nombre,
|
||||
String? avatar,
|
||||
String? creadoPorClienteId,
|
||||
String? clienteIdSeleccionado,
|
||||
bool liberarSeleccion = false,
|
||||
}) {
|
||||
return Usuario(
|
||||
id: id ?? this.id,
|
||||
nombre: nombre ?? this.nombre,
|
||||
avatar: avatar ?? this.avatar,
|
||||
creadoPorClienteId: creadoPorClienteId ?? this.creadoPorClienteId,
|
||||
clienteIdSeleccionado: liberarSeleccion
|
||||
? null
|
||||
: (clienteIdSeleccionado ?? this.clienteIdSeleccionado),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'nombre': nombre,
|
||||
if (avatar != null) 'avatar': avatar,
|
||||
if (creadoPorClienteId != null) 'creadoPorClienteId': creadoPorClienteId,
|
||||
if (clienteIdSeleccionado != null)
|
||||
'clienteIdSeleccionado': clienteIdSeleccionado,
|
||||
};
|
||||
|
||||
factory Usuario.fromJson(Map<String, dynamic> json) => Usuario(
|
||||
id: json['id'] as String,
|
||||
nombre: json['nombre'] as String,
|
||||
avatar: json['avatar'] as String?,
|
||||
creadoPorClienteId: json['creadoPorClienteId'] as String?,
|
||||
clienteIdSeleccionado: json['clienteIdSeleccionado'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user