feat(multi-device): host puede participar como jugador

- Añadido modelo Usuario con pool de usuarios sincronizado
- El host ahora recibe palabra y rol como cualquier jugador
- UI de selección de perfil en pantallas de lobby
- Los clientes pueden ver usuarios del servidor o crear nuevos
- El juego no inicia hasta que el host selecciona perfil
This commit is contained in:
ShanaiaBot
2026-04-24 18:47:56 +02:00
parent 3df3ae1e95
commit d3fc3386f9
31 changed files with 1266 additions and 106 deletions

View File

@@ -0,0 +1,115 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:farolero/estado/estado_juego.dart';
import 'package:farolero/modelos/palabra.dart';
import 'package:farolero/modelos/partida.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('EstadoJuego crearPartida with host local', () {
late EstadoJuego estado;
setUp(() async {
estado = EstadoJuego();
await estado.cargarBanco();
});
tearDown(() {
estado.dispose();
});
test(
'should include host in jugadores list when setHostJugador called',
() {
// Set host local player first
estado.setHostJugador('HostJuan');
// Create game with 3 client players + host (host is in the list)
estado.crearPartida(
config: ConfigPartida(
modoMultimovil: true,
categoria: 'objetos',
numImpostores: 1,
pistaImpostor: false,
),
nombresJugadores: ['HostJuan', 'Cliente1', 'Cliente2', 'Cliente3'],
);
expect(estado.partida, isNotNull);
expect(estado.partida!.jugadores.length, 4);
// Verify host is in the list
final hostJugador = estado.partida!.jugadores
.where((j) => j.nombre == 'HostJuan')
.firstOrNull;
expect(hostJugador, isNotNull);
expect(hostJugador!.endpointId, isNull);
},
);
test('should assign word to host local player', () {
estado.setHostJugador('HostJuan');
estado.crearPartida(
config: ConfigPartida(
modoMultimovil: true,
categoria: 'objetos',
numImpostores: 1,
pistaImpostor: false,
),
nombresJugadores: ['HostJuan', 'Cliente1', 'Cliente2', 'Cliente3'],
);
final hostJugador = estado.partida!.jugadores
.where((j) => j.nombre == 'HostJuan')
.firstOrNull;
// Host should receive a word if not impostor
expect(hostJugador, isNotNull);
if (!hostJugador!.esImpostor) {
expect(hostJugador.palabra, isNotNull);
expect(hostJugador.palabra!.isNotEmpty, isTrue);
}
});
test('should include host in impostor selection pool', () async {
// Run multiple times to increase chance of host being impostor
bool hostWasImpostorAtLeastOnce = false;
bool hostWasNormalAtLeastOnce = false;
for (int i = 0; i < 20; i++) {
final estado2 = EstadoJuego();
await estado2.cargarBanco();
estado2.setHostJugador('HostJuan');
estado2.crearPartida(
config: ConfigPartida(
modoMultimovil: true,
categoria: 'objetos',
numImpostores: 1,
pistaImpostor: false,
),
nombresJugadores: ['HostJuan', 'Cliente1', 'Cliente2', 'Cliente3'],
);
final hostJugador = estado2.partida!.jugadores
.where((j) => j.nombre == 'HostJuan')
.firstOrNull;
if (hostJugador!.esImpostor) {
hostWasImpostorAtLeastOnce = true;
} else {
hostWasNormalAtLeastOnce = true;
}
estado2.dispose();
if (hostWasImpostorAtLeastOnce && hostWasNormalAtLeastOnce) break;
}
// Host should have been impostor at least once and normal at least once
// (statistically likely with 20 iterations)
expect(hostWasImpostorAtLeastOnce, isTrue);
expect(hostWasNormalAtLeastOnce, isTrue);
});
});
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:farolero/estado/estado_juego.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('EstadoJuego host local', () {
late EstadoJuego estado;
setUp(() {
estado = EstadoJuego();
});
tearDown(() {
estado.dispose();
});
test('should start with null hostLocal', () {
expect(estado.hostLocal, isNull);
});
test('should set host jugador correctly', () {
estado.setHostJugador('Juan');
expect(estado.hostLocal, isNotNull);
expect(estado.hostLocal!.nombre, 'Juan');
expect(estado.hostLocal!.endpointId, isNull);
});
test('should update host jugador name', () {
estado.setHostJugador('Juan');
expect(estado.hostLocal!.nombre, 'Juan');
estado.setHostJugador('Maria');
expect(estado.hostLocal!.nombre, 'Maria');
});
});
}

View File

@@ -0,0 +1,70 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:farolero/modelos/usuario.dart';
import 'package:farolero/servicios/servicio_nearby.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('ServicioNearby user pool', () {
late ServicioNearby servicio;
setUp(() {
servicio = ServicioNearby();
});
tearDown(() {
servicio.dispose();
});
test('should start with empty user pool', () {
expect(servicio.usuarios, isEmpty);
});
test('should add user to pool', () {
final usuario = Usuario(id: 'user-1', nombre: 'Juan');
servicio.agregarUsuario(usuario);
expect(servicio.usuarios.length, 1);
expect(servicio.usuarios.first.nombre, 'Juan');
});
test('should remove user from pool', () {
final usuario = Usuario(id: 'user-1', nombre: 'Juan');
servicio.agregarUsuario(usuario);
expect(servicio.usuarios.length, 1);
servicio.eliminarUsuario('user-1');
expect(servicio.usuarios, isEmpty);
});
test('should synchronize users from list', () {
final usuarios = [
Usuario(id: 'user-1', nombre: 'Juan'),
Usuario(id: 'user-2', nombre: 'Maria'),
];
servicio.sincronizarUsuarios(usuarios);
expect(servicio.usuarios.length, 2);
expect(servicio.usuarios.map((u) => u.nombre).toList(), contains('Juan'));
expect(
servicio.usuarios.map((u) => u.nombre).toList(),
contains('Maria'),
);
});
test('should get usuario by id', () {
final usuario = Usuario(id: 'user-1', nombre: 'Juan');
servicio.agregarUsuario(usuario);
final found = servicio.getUsuario('user-1');
expect(found, isNotNull);
expect(found!.nombre, 'Juan');
});
test('should return null for non-existent user', () {
final found = servicio.getUsuario('non-existent');
expect(found, isNull);
});
});
}

37
test/usuario_test.dart Normal file
View File

@@ -0,0 +1,37 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:farolero/modelos/usuario.dart';
void main() {
group('Usuario', () {
test('should create Usuario with id and nombre', () {
final usuario = Usuario(id: 'test-id', nombre: 'Juan');
expect(usuario.id, 'test-id');
expect(usuario.nombre, 'Juan');
});
test('should serialize to JSON correctly', () {
final usuario = Usuario(id: 'test-id', nombre: 'Juan');
final json = usuario.toJson();
expect(json['id'], 'test-id');
expect(json['nombre'], 'Juan');
});
test('should deserialize from JSON correctly', () {
final json = {'id': 'test-id', 'nombre': 'Juan'};
final usuario = Usuario.fromJson(json);
expect(usuario.id, 'test-id');
expect(usuario.nombre, 'Juan');
});
test('should handle JSON with avatar field', () {
final json = {'id': 'test-id', 'nombre': 'Juan', 'avatar': '👤'};
final usuario = Usuario.fromJson(json);
expect(usuario.id, 'test-id');
expect(usuario.nombre, 'Juan');
});
});
}