El Impostor v0.1 — app Flutter completa

Juego de deducción social para 3-20 jugadores.
Modo un solo móvil completamente funcional.
1000 palabras en 10 categorías.
Notas privadas, votación, adivinanza, revancha.
Material 3 dark theme.
Package: es.freetimelab.elimpostor
This commit is contained in:
ShanaiaBot
2026-04-04 00:50:04 +02:00
parent eb7661cb36
commit de2c8ffa18
45 changed files with 4206 additions and 0 deletions

51
lib/modelos/jugador.dart Normal file
View File

@@ -0,0 +1,51 @@
/// Representa un jugador en la partida
class Jugador {
final String id;
final String nombre;
bool esImpostor;
bool eliminado;
String? palabra;
String? endpointId; // Para modo multimóvil
Map<String, String> notas; // nombre_jugador -> nota
Jugador({
required this.id,
required this.nombre,
this.esImpostor = false,
this.eliminado = false,
this.palabra,
this.endpointId,
Map<String, String>? notas,
}) : notas = notas ?? {};
Jugador copiar({
bool? esImpostor,
bool? eliminado,
String? palabra,
String? endpointId,
}) {
return Jugador(
id: id,
nombre: nombre,
esImpostor: esImpostor ?? this.esImpostor,
eliminado: eliminado ?? this.eliminado,
palabra: palabra ?? this.palabra,
endpointId: endpointId ?? this.endpointId,
notas: Map.from(notas),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'nombre': nombre,
'esImpostor': esImpostor,
'eliminado': eliminado,
};
factory Jugador.fromJson(Map<String, dynamic> json) => Jugador(
id: json['id'] as String,
nombre: json['nombre'] as String,
esImpostor: json['esImpostor'] as bool? ?? false,
eliminado: json['eliminado'] as bool? ?? false,
);
}

63
lib/modelos/palabra.dart Normal file
View File

@@ -0,0 +1,63 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
/// Categorías disponibles en el banco de palabras
class BancoPalabras {
final Map<String, List<String>> categorias;
BancoPalabras(this.categorias);
static BancoPalabras? _instancia;
static Future<BancoPalabras> cargar() async {
if (_instancia != null) return _instancia!;
final jsonStr = await rootBundle.loadString('assets/palabras.json');
final data = json.decode(jsonStr) as Map<String, dynamic>;
final cats = data['categorias'] as Map<String, dynamic>;
final mapa = <String, List<String>>{};
for (final entrada in cats.entries) {
mapa[entrada.key] = List<String>.from(entrada.value);
}
_instancia = BancoPalabras(mapa);
return _instancia!;
}
List<String> get nombresCategorias => categorias.keys.toList();
/// Obtiene una palabra aleatoria de la categoría dada (o de todas si es null)
String palabraAleatoria(String? categoria) {
final rng = Random();
if (categoria == null || categoria == 'todas') {
final todasPalabras = categorias.values.expand((l) => l).toList();
return todasPalabras[rng.nextInt(todasPalabras.length)];
}
final lista = categorias[categoria]!;
return lista[rng.nextInt(lista.length)];
}
/// Devuelve la categoría a la que pertenece una palabra
String? categoriaDepalabra(String palabra) {
for (final entrada in categorias.entries) {
if (entrada.value.contains(palabra)) return entrada.key;
}
return null;
}
static String nombreBonitoCategoria(String clave) {
const nombres = {
'todas': 'Todas',
'animales': 'Animales',
'comida': 'Comida',
'paises': 'Países',
'deportes': 'Deportes',
'profesiones': 'Profesiones',
'objetos': 'Objetos',
'lugares': 'Lugares',
'peliculas': 'Películas',
'musica': 'Música',
'tecnologia': 'Tecnología',
};
return nombres[clave] ?? clave;
}
}

79
lib/modelos/partida.dart Normal file
View File

@@ -0,0 +1,79 @@
import 'jugador.dart';
/// Configuración de una partida
class ConfigPartida {
final bool modoMultimovil;
final String categoria; // 'todas' o nombre de categoría
final int numImpostores;
final bool pistaImpostor;
final int? tiempoDebateSegundos; // null = sin límite
const ConfigPartida({
this.modoMultimovil = false,
this.categoria = 'todas',
this.numImpostores = 1,
this.pistaImpostor = false,
this.tiempoDebateSegundos,
});
}
/// Fases del juego
enum FaseJuego {
configuracion,
verPalabra,
debate,
votacion,
resultado,
adivinanza, // El impostor intenta adivinar la palabra
finPartida,
}
/// Resultado de una ronda de votación
class ResultadoVotacion {
final String eliminadoId;
final String eliminadoNombre;
final bool eraImpostor;
final Map<String, String> votos; // votante -> votado
const ResultadoVotacion({
required this.eliminadoId,
required this.eliminadoNombre,
required this.eraImpostor,
required this.votos,
});
}
/// Estado completo de una partida
class Partida {
final ConfigPartida config;
final List<Jugador> jugadores;
final String palabraSecreta;
final String categoriaReal;
FaseJuego fase;
int rondaActual;
final List<ResultadoVotacion> historialVotaciones;
String? ganador; // 'jugadores' | 'impostores' | null
Partida({
required this.config,
required this.jugadores,
required this.palabraSecreta,
required this.categoriaReal,
this.fase = FaseJuego.verPalabra,
this.rondaActual = 1,
List<ResultadoVotacion>? historialVotaciones,
this.ganador,
}) : historialVotaciones = historialVotaciones ?? [];
List<Jugador> get jugadoresActivos =>
jugadores.where((j) => !j.eliminado).toList();
List<Jugador> get impostoresActivos =>
jugadoresActivos.where((j) => j.esImpostor).toList();
List<Jugador> get jugadoresNormalesActivos =>
jugadoresActivos.where((j) => !j.esImpostor).toList();
int get impostoresTotales =>
jugadores.where((j) => j.esImpostor).length;
}