Trabajo planificado por fases. Cada fase es funcional por sí sola antes de empezar la siguiente.
Dart puro, sin dependencias de Flutter. Totalmente testeable en aislamiento.
- Modelos inmutables:
CardModel,PlayerModel,DeckModel,TurnModel,GameState -
CardTypecon todas las cartas oficiales (base + gatos) -
DeckBuilder— construcción y reparto según número de jugadores -
DeckManager— robar, descartar, insertar en posición, peek top N -
GameRules— validación de acciones antes de aplicarlas -
CardRules— jugabilidad por tipo, par/trío de gatos, Nope, Defuse -
NopeRules— cadena Nope/Nope-a-Nope con contador impar/par -
TurnRules— rotación de turno, Attack chains -
WinCondition— detección de ganador -
ActionProcessor— aplicación de efectos: Attack, Skip, Favor, Shuffle, See the Future, Cat Pair/Trio, Defuse, Nope -
GameEngine— fachada pública (startGame/apply) -
GameEventBus— stream broadcast de eventos para UI y red - Tests unitarios:
CardModel,DeckManager,CardRules
- Rutas completas en
app_router.dart(lobby, game, game-over, settings) -
SplashScreen— logo animado con Lottie -
HomeScreen— menú principal con botones animados -
SettingsScreen— volumen, nombre de jugador, preferencias - Sistema de tema oscuro consolidado
- Fuente personalizada integrada (
ExplodingFont) - Tests de widgets para pantallas principales —
HomeScreen(render + navegación) ySettingsScreen(carga de prefs, guardado de nombre, toggle de sonido)
-
MdnsDiscoverer— descubrimiento de salas (al principio via beacons UDP broadcast propios; migrado a mDNS/DNS-SD real connsden Fase 6, ver más abajo) -
WsServer— servidor WebSocket en el host (AppConstants.localGamePort) -
LobbyRoom/LobbyPlayercon estados víaLobbyStatus -
LobbyRepository— createRoom / joinRoom / leaveRoom / setReady / startGame (sin UseCases separados; coordinados directamente en el repositorio) -
LobbyScreen— creación de sala, descubrimiento y unión, lista de jugadores conectados con estado ready, botón de inicio - Tests del lobby (modelos, repositorio, providers) — 34 tests nuevos, 51 totales
- Prework de engine:
ActionProcessor.resolveNopeWindow()/GameEngine.resolveNopeWindow()— difiere y resuelve los efectos de Favor, Cat Pair/Trío y Shuffle según si la cadena de Nope quedó cancelada; fix de la duplicación de bomba en Defuse (GameState.pendingBomb) -
CardAssetResolver/CardVisuals— resuelve el asset real de una carta desde elAssetManifestsi ya existe, o cae a un placeholder (color + icono + nombre) porCardType; permite ir soltando arte final carta por carta sin tocar widgets -
GameStateProvider— implementado comoGameNotifier/gameProvider(Notifier<GameSessionState>, noStateNotifier, siguiendo el patrón ya usado porlobbyProvider), conIGameGateway/LocalGameGatewayde por medio para poder enchufar un gateway remoto en Fase 5 sin tocar la UI -
GameScreen— conectado agameProvider/lobbyProvider; layout adaptativo por reflow (Expanded + scroll interno, sin dos árboles portrait/landscape duplicados); solo el host arranca elGameEnginehoy, los no-host ven un placeholder de "esperando Fase 5" -
PlayerHandWidget— fan de cartas, selección por tap (se descartó drag & drop: más simple y necesario igual para pares/tríos de gato) -
CardWidget— flip animation, glow en cartas jugables; Fase 6 añadió una animación de entrada (fade + slide) para la carta recién robada (justDrawn), independiente del glow de jugable -
DeckWidget— contador de cartas; animación de mezcla y pulso de robo resueltos en Fase 6 (GameTableViewsuscrito aStream<GameEvent>, ver más abajo) -
DiscardPileWidget— pila de descarte con última carta visible; Fase 6 añadió una transición (AnimatedSwitcher) al cambiar la carta de arriba -
PlayersHudWidget— avatares de jugadores con contador de cartas; Fase 6 añadió un resaltado del turno actual (anillo + flecha) con transición animada al cambiar de jugador -
NopeWindowOverlay— temporizador visual (barra de progreso client-side, sin timestamp enGameState) y botón reactivo, habilitado solo si el jugador local tiene un Nope en mano y hay una acción pendiente -
InsertBombOverlay— slider para elegir en qué posición del mazo reinsertar la Exploding Kitten robada, entre "arriba del todo" y "abajo del todo"; solo se muestra al jugador que la robó -
SeeTheFutureOverlay— visualización de top 3 cartas, visibilidad derivada deGameState.seeTheFutureCards, descartado como estado local de UI -
FavorTargetOverlay— selector de objetivo para Favor, pares y tríos de gato; el trío quedó diferido en su momento (necesita elegir una carta concreta de la mano rival, que el actor no puede ver) — resuelto en Fase 6 conCardChoiceOverlayboca abajo (ChooseCardAction, elige el actor a ciegas por posición) -
ExplosionOverlay— animación de eliminación (placeholder con Flutter puro, escala con rebote; se reemplazará por el Lottie real deAssetPaths.animExplosioncuando exista ese asset); se detecta por diff deGameState(un jugador que estaba vivo deja de estarlo), se cierra sola sin acción del jugador -
GameOverScreen— ganador, ranking en orden real de eliminación (fix deWinCondition/nuevoGameState.eliminationOrder: antes seguía el orden de la lista de jugadores, no el cronológico) y botón de revancha, solo para el host (mismo límite queGameScreenhoy); revancha re-arranca el mismoGameEngine/bus con los jugadores de la sala actual; Fase 6 añadió una entrada escalonada (flutter_animate) para el ganador, el ranking y los botones - Integración de
audioplayers(efectos y música de fondo) —IAudioService/AudioService(interfaz + impl, testeable con fake),GameSoundControllerreproduce el efecto de cadaGameEventdel motor mientras dura la partida,GameScreen/GameOverScreenreproducenmusic_ingame.mp3/music_gameover.mp3en loop. De paso se corrigieron los nombres de archivo enAssetPaths(no coincidían con los reales enassets/sounds/). Alcance de esta pasada: solo pantallas de partida;music_menu.mp3para Home/Splash/Lobby/Settings quedó pendiente (no era parte de "pantalla de juego completa") — resuelto en Fase 6 conMenuMusicMixin - Integración de
flutter_animateen cartas y transiciones —CardWidgethace un "pop" de escala al volverse jugable, y los 5 overlays (SeeTheFuture/FavorTarget/NopeWindow/InsertBomb/Explosion) tienen fade-in de entrada, mismo estilo.animate()que ya usaban Home/Splash - Tests de providers y casos de uso —
GameNotifier: un test por método (playCard/playFavor/playCatPair/playCatTrio/playNope/defuse) verificando elTurnActionconcreto que dispatchea, más el nuevo getterevents; 118 tests totales pasando
-
WebSocketServer— host recibe acciones, aplica al engine, retransmite estado (gameNetworkBridgeProviderconectaWsServer.actionMessagesconGameNotifier.applyAction, yGameNotifier.rawStates/eventsde vuelta conWsServer.broadcast) -
WebSocketClient— clientes envían acciones, recibenGameState(RemoteGameNotifier, mismoGameSessionStateque produceGameNotifierpara el host) -
GameStateSerializer—GameState↔ JSON para transmisión (toJson/fromJsonmanuales en todos los modelos del motor, mismo estilo que los del lobby) -
EventSerializer—GameEvent↔ JSON (necesario porque los no-host no tienen motor local; su único origen de sonidos/animaciones es lo que el host reenvía) -
ReconnectionManager— grace period + reconexión con back-off exponencial (ReconnectionManageren el host para el grace period de 60s;WsClientreconecta solo con back-off 1s→16s tras una caída no solicitada) - Manejo de
PlayerStatus.disconnecteden UI (PlayersHudWidgetmuestra "Reconectando…" + icono de wifi apagado) - Tests de serialización y reconexión (round-trip por modelo, integración real servidor+cliente en loopback para el puente y la reconexión)
Nota: se decidió no forzar un
RemoteGameGatewaydentro deIGameGateway(hubiera obligado a convertirapply()/startGame()a streams y reescribir los tests ya existentes deGameNotifier) —RemoteGameNotifieres una clase separada que refleja el mismoGameSessionState, verdocs/VERIFICATION_LOG.mdpara el detalle de la decisión y la verificación manual.
- Diseño responsivo:
GameTableViewtiene árboles diferenciados por orientación (context.isLandscape) en vez de un únicoColumn/Rowcon reflow; ancho de carta de mano y separación mazo/descarte escalan además por tamaño de pantalla (LayoutConstants,context.isTabletcontra el lado corto) - Arrastrar cartas (drag & drop) en
PlayerHandWidget, además de la selección por tap: soltar una carta jugable de inmediato (Skip/Attack/Shuffle/See the Future) sobre elDragTargetde mazo/descarte la juega directo, sin pasar por el botón "Jugar"; soltar cualquier otra carta (o una jugable fuera delDragTarget) simplemente la selecciona, igual que un tap — las combinaciones que necesitan objetivo (Favor, par/trío de gatos) siguen su flujo de selección + overlay sin cambios - Soporte multi-idioma español/inglés siguiendo el idioma del sistema (issue #45) —
flutter_localizations+intl+flutter gen-l10nnativo (l10n.yaml, sin locale fijo enMaterialApp.router). Cubre todas las pantallas/widgets depresentation/; quedan afuera a propósito los mensajes de error delib/network/websocket_server.dart(sinBuildContextdisponible en esa capa) y el nombre de jugador por defecto enAppSettings(capadomain/) — verdocs/ARCHITECTURE.md, sección "Localización"
- Migrar
MdnsAdvertiser/MdnsDiscovererde UDP broadcast a mDNS/Bonjour real (nsd) —nsdes enteramente nativo (Bonjour en Apple, NsdManager en Android); los tests mockeanNsdPlatformInterfacepara la lógica propia, y el registro/descubrimiento real se verificó a mano en dispositivos antes de mergear (verdocs/VERIFICATION_LOG.md) -
WifiManager.MulticastLockvía platform channel en Android 10+ — resuelto como efecto colateral de la migración anterior:nsd_androidya lo adquiere internamente (usa el permisoCHANGE_WIFI_MULTICAST_STATEque ya estaba declarado), no hace falta un platform channel propio - Persistir
playerIdconshared_preferencespara reconexión tras crash - Reproducir
AssetPaths.musicMenuen Home/Splash/Lobby/Settings (MenuMusicMixin; antes soloGameScreen/GameOverScreentenían música víaAudioService) - Validar
hostAddressde un beacon contra la IP real del remitente (MdnsDiscoverer), en vez de confiar ciegamente en el valor autoreportado — superado por la migración ansdde arriba: la dirección ya viene resuelta por el propio protocolo mDNS (Service.addresses), no de un campo autoreportado, así que el caso de spoofing que esto cerraba ya no aplica con la nueva implementación - Separar el manejo mDNS de host/cliente de
LobbyRepositoryen clases propias (HostBeaconSync/ClientRoomDiscovery) -
GameTableViewpuede suscribirse alStream<GameEvent>(mismo que ya consumeGameSoundController) para animaciones que un diff deGameStateno puede detectar por sí solo (mezclar el mazo, distinguir un robo propio de una carta ganada por Favor/pareja/trío) — ver Fase 4 (DeckWidget/CardWidget) yCHANGELOG.mdpara el detalle
- Motor de bots heurístico con proyección (OSLA), configurable en
dificultad — un solo motor paramétrico (
BotConfig/BotWeights: pesos, determinizaciones, temperatura), no dos estrategias fijas separadas como sugería el ítem original ("básica aleatoria" + "avanzada heurística"): la dificultad sale de variar la configuración sobre el mismo núcleo. Verdocs/ARCHITECTURE.md, sección "Motor de bots", para el diseño completo - Partida local (pass-and-play) o LAN contra 1-4 bots — el host los
agrega desde
LobbyScreenantes de arrancar - Bots en modo online (backend Go,
cards_game_service) — issue futuro y separado, sin dependencias pendientes; queda preparado (features/config en tipos serializables) pero no implementado, verdocs/ARCHITECTURE.md
- Mitad cliente de la identidad persistente de
cards_game_service(backend hermano, repo separado):SupabaseAuthService+authServiceProvider/authSessionProvider(featureauth/, sign-in anónimo en la primera lectura),JoinRoomMessage.authTokenopcional,WsClientlo manda en el join inicial y lo reenvía en cada reconexión automática, yplayerIdProviderprefiere elplayerIdde la sesión sobre el UUID de invitado — verdocs/ARCHITECTURE.md, sección "Autenticación con Supabase", para el diseño completo - Conectar de verdad a
cards_game_servicedesplegado por Internet —OnlineLobbyRepository(nueva implementación deILobbyRepository),WsClient.connectToUri,OnlineRoomsClient(POST /rooms) y un selector explícito LAN/Online enLobbyScreen. Verificado a mano contra el backend Go real antes de mergear (verdocs/VERIFICATION_LOG.md, sección "Fase 7 — Modo online del lado cliente") - Soporte
wss://(TLS) enWsClient—OnlineConfig.wsUri()mapea el esquemahttps/httpdelONLINE_SERVER_URLawss/ws;WsClient.connectToUriacepta cualquier esquema tal cual se lo paseIOWebSocketChannel - Vincular una cuenta anónima a una cuenta real por correo/contraseña (issue #27) —
SupabaseAuthService.signUpAndLinkAnonymous(updateUsersobre la sesión anónima activa, nolinkIdentity) ysignInWithPassword, pantallasAccountScreen/SignUpScreen/LoginScreenaccesibles desde Ajustes. Login con Google sigue pendiente, issue aparte
- Backend de salas (WebSocket server desplegado) —
cards_game_service(repo separado), ver Autenticación (Fase 7) arriba para la mitad cliente - Soporte cliente para tokens de sesión emitidos por el servidor (
JoinRoomMessage.token,SessionTokenMessageenWsClient) — reenviados en cada reconexión, distinto delauthTokende Fase 7 (ese identifica al jugador vía Supabase; este prueba ante el backend que la reconexión es la misma sesión de red ya conocida).WsServer(host LAN) no lo implementa a propósito, así que el juego local por WiFi no se ve afectado - Persistir
_sessionToken(hoy solo en memoria enWsClient) igual que ya se hace conplayerIdvíashared_preferences— si no, un crash/reinicio de la app pierde el token y el backend online rechazaría el reconnect aunque elplayerIdsí sobreviva - Sistema de cuentas / nicknames persistentes — el backend ya expone
GET /players/{id}yPATCH /players/{id}/nickname, sin consumir todavía desde el cliente - Matchmaking por código de sala — crear sala online muestra el código; unirse online lo pide por diálogo
- Jugar de verdad una partida online (issue #35, Stage A): el host dejó de correr
GameEnginelocal en modo online — ahora refleja el servidor autoritativo decards_game_serviceigual que cualquier no-host, con un codec nuevo (online_wire_codec.dart) que traduce elViewredactado del servidor (manos rivales ocultas,CardType/TurnPhaseensnake_case) alGameStatede Dart en las dos direcciones - Trío de gatos a ciegas en modo online (issue #35, Stage B) —
View.PlayerView.HiddenHandIdsexpone elidde carta (sintype) de la mano rival, solo mientras el viewer tiene un trío pendiente contra ella; ver cards_game_service#9 - Revancha en modo online (issue #35, Stage C) —
onStartGameacepta la sala enphaseFinished, no solophaseWaiting; ver cards_game_service#10 - Ranking global — el backend ya expone
GET /leaderboard, sin consumir todavía desde el cliente
- Imploding Kittens (6 jugadores, cartas nuevas)
- Streaking Kittens
- Barking Kittens (2 jugadores cooperativo)
- Firma y build de release para Android (Google Play)
- Firma y build de release para iOS (App Store)
- Assets gráficos originales completos
- Onboarding / tutorial interactivo
feat(scope): nueva funcionalidad
fix(scope): corrección de bug
test(scope): tests añadidos o corregidos
refactor: sin cambio de comportamiento externo
chore: dependencias, configuración
ci: pipeline y GitHub Actions
docs: documentación
Scopes principales: core · engine · features · network · assets · ci