Terminar una partida era demasiado importante para dejarlo en cada juego
Durante mucho tiempo el final de una partida podía resolverse localmente: un motor detectaba que el puzzle estaba resuelto, mostraba un mensaje y ofrecía empezar otra partida. Ese enfoque funciona mientras cada juego vive casi aislado. En Blupoli Puzzles dejó de ser suficiente cuando el final empezó a alimentar estadísticas, récords, logros, rachas y una identidad visual común. Las PR #168 y #175 convirtieron ese momento en un componente de plataforma.
El objetivo no era que todos los juegos tuvieran exactamente la misma personalidad. Queríamos que compartieran una secuencia fiable: conservar el tablero terminado durante un instante, reconocer el resultado, mostrar unas pocas métricas comparables, aplicar la identidad de categoría, celebrar sólo los resultados positivos y respetar preferencias de movimiento. El motor seguiría decidiendo cuándo termina la partida; el shell común decidiría cómo se presenta ese final.
Ese cambio expuso una idea de producto que se había perdido entre modales y callbacks. Resolver un puzzle no debería hacer que el tablero desaparezca inmediatamente. El estado final es la prueba visual del trabajo del jugador. Merece un pequeño momento propio antes de que la interfaz pase a estadísticas y siguientes acciones.
Primera iteración: centralizar antes de espectacularizar
La PR #168 fue una primera versión deliberadamente contenida. Mantuvo el tablero completado visible durante 1,2 segundos, añadió una animación final común y confeti para resultados positivos, incorporó estrellas de desempeño como fallback y empezó a centralizar colores semánticos. El valor principal no estaba todavía en la espectacularidad, sino en demostrar que puzzles y juegos competitivos podían llegar al mismo componente.
Ataxx y Dots and Boxes fueron una prueba útil porque no terminan en “resuelto”. Se normalizó además la convención visual de jugador humano azul y CPU roja. La decisión cromática es pequeña, pero reveló la necesidad de una semántica transversal: un jugador no debería cambiar de identidad visual porque cambie el motor que se está ejecutando.
También se incluyó desde el principio prefers-reduced-motion. La espera que permite contemplar el tablero se conserva, pero las animaciones intensas y el confeti se desactivan si el sistema operativo pide menos movimiento. La accesibilidad no entró como parche posterior; formó parte de la definición de finalización.
La segunda iteración cambió la escala visual
La PR #175 retomó la base común y aumentó la ambición. El tablero completado permanece visible durante 1,7 segundos. Después aparece el resumen compartido y, para resultados positivos, una celebración mucho más visible. En escritorio hay ocho emisores repartidos por la parte inferior, con 22 partículas por emisor: 176 partículas en total. En móvil se reduce a cinco emisores y 16 partículas por emisor, 80 en total.
La reducción móvil afecta a densidad, no a duración. Las partículas suben, alcanzan un ápice y tardan varios segundos en caer fuera del viewport. El código fija 1.700 milisegundos de revelado del tablero, 1.760 antes de lanzar el confeti y 6.000 milisegundos de vida de las partículas. Son números explícitos porque el ritmo dejó de ser un efecto accidental.
Hacer el timing visible y comprobable tiene una ventaja práctica. Una revisión futura puede ajustar la pausa o la vida de la celebración en un único lugar. Antes, un cambio de ese tipo podía exigir perseguir timeouts dispersos entre motores y modales.
Una pausa de 1,7 segundos también es diseño de interacción
El intervalo previo al resumen evita que la aplicación parezca robar el resultado en el mismo instante en que se consigue. El tablero cambia de función: deja de ser sólo una superficie interactiva y se convierte durante un momento en evidencia del estado final. Esa pequeña pausa ayuda a conectar la última acción con la confirmación visual de que la partida realmente terminó.
También separa dos ritmos distintos. Uno pertenece a la comprensión: cuánto tiempo necesita el jugador para reconocer el tablero terminado. Otro pertenece al espectáculo: cuánto dura una animación. El primero se mantiene incluso con movimiento reducido; el segundo puede desaparecer sin romper el significado. Esa diferencia fue importante para no confundir accesibilidad con eliminar la estructura de la transición.
Centralizar la pausa convierte el ritmo en parte del contrato compartido. No todo lo que se centraliza tiene que ser una API. A veces es una decisión temporal que evita que decenas de juegos desarrollen sensaciones de cierre completamente distintas.
No todos los resultados merecen confeti
El componente diferencia resultados positivos de resultados simplemente terminados. solved y win forman el conjunto positivo. Una derrota o un empate competitivo se registra, se resume y conserva sus métricas, pero no recibe la misma celebración que una victoria. El componente no necesita comprender las reglas de Ataxx o Dots and Boxes; necesita recibir un GameResult correcto.
Esta separación hace que el confeti deje de estar pegado al solver. Es una interpretación visual de una semántica común. Un nuevo juego competitivo puede adoptar el mismo cierre sin copiar lógica y un cambio de animación puede hacerse sin tocar los motores. La fuente de verdad es el resultado, no el detalle de cómo cada juego llegó hasta él.
La misma idea evita mensajes engañosos. Una partida competitiva perdida sigue siendo una partida completada para estadísticas, pero no se presenta como éxito. Datos y emoción pueden usar el mismo evento sin tener que asignarle el mismo significado.
Las estrellas necesitaban un fallback común
No todos los juegos tenían un sistema propio de valoración. Para que el resumen no quedara vacío o incoherente, el componente calcula estrellas cuando el juego no aporta una estrategia específica. En puzzles se parte de cinco y se pueden perder estrellas por pistas, reinicios o por un rendimiento claramente inferior a una marca personal comparable.
En modos competitivos se utiliza el outcome y, si existe, la diferencia de marcador. Una victoria puede producir cuatro o cinco estrellas, un empate una valoración intermedia y una derrota una valoración menor. No pretende convertir todos los juegos en la misma competición. Sirve como señal compacta cuando el motor no dispone de una puntuación nativa con significado propio.
La configuración del juego puede sustituir ese fallback. Hay estrategias basadas, por ejemplo, en movimientos frente a una referencia. El shell común ofrece un valor por defecto, pero no borra un sistema de rating que realmente pertenezca a la mecánica.
Las métricas del resumen también necesitaban jerarquía
GameResult puede contener mucho más de lo que cabe razonablemente en una pantalla final. El componente elige unas pocas métricas. Para puzzles, la selección por defecto prioriza tiempo, movimientos, pistas, dificultad y tamaño. Para competitivos, resultado, marcador, tiempo, movimientos y dificultad. El objetivo es resumir, no reconstruir el dashboard de estadísticas dentro de un modal.
Los juegos pueden declarar su propia lista. El formateador común entiende campos como referencia de movimientos, piezas, doble eje, errores o retrocesos, y también puede leer valores específicos desde metadata. La estructura visual permanece estable mientras el contenido se adapta a lo que realmente importa en cada motor.
Ahí aparece una distinción útil entre consistencia y uniformidad. Consistencia significa que el jugador sabe dónde mirar y cómo continuar. Uniformidad significaría obligar a todos los juegos a mostrar exactamente los mismos números aunque algunos sean irrelevantes. El sistema busca lo primero y evita lo segundo.
La identidad de categoría debía vivir en CSS
La segunda iteración eliminó la tabla CATEGORY_THEMES de JavaScript. El host expone la categoría principal mediante data-game-category y el sistema visual decide cómo se traduce esa semántica a color. Cada categoría cuenta con valores explícitos para modo claro y oscuro.
Mantener una tabla paralela en JavaScript habría creado dos fuentes de verdad: lógica y diseño. Una actualización de paleta exigiría sincronizarlas y el desajuste podría aparecer sólo en un tema. La categoría es dato; el color es presentación. CSS es el lugar natural para resolver esa segunda parte.
Los tests protegen la frontera. Exigen variantes light/dark para todas las categorías e impiden que vuelva a aparecer una tabla de colores de categoría en JavaScript. No es sólo una refactorización estética: convierte una decisión de arquitectura visual en un guardrail.
Los tokens semánticos sustituyen el patrón “oscuro más parche claro”
El trabajo introdujo roles como --game-control-*. Antes era fácil que un control naciera con valores concretos pensados para oscuro y que más tarde se añadiera un override para claro. Ese patrón funciona en un componente; repetido por todo el catálogo produce CSS difícil de razonar y de auditar.
Con tokens semánticos el componente pide un fondo de control, un texto o un borde. El tema proporciona valores. game-shell.css puede consumir la misma intención en ambos modos. El componente deja de saber si está en light o dark y se reduce el número de excepciones.
La PR #175 fue explícita en una limitación: no cerraba toda la auditoría de colores de cada motor. Ese trabajo siguió asociado a #174. Centralizar la finalización y el shell común era una base, no una declaración de que todo el catálogo visual estuviera ya normalizado.
El confeti tenía que estar por encima del resumen
Uno de los problemas visibles era que una celebración podía quedar ocultada justo cuando aparecía la pantalla de finalización. El nuevo efecto se trata como capa de viewport, no como decoración encerrada dentro de la tarjeta. Los emisores nacen abajo y las trayectorias permiten que las partículas sigan cruzando la pantalla después de entrar el resumen.
Esto obligó a considerar z-index, duración y densidad como un solo problema. Más partículas no mejoran nada si caen detrás de un overlay. Una vida más larga es contraproducente si bloquea interacción. Una distribución amplia funciona en escritorio, pero puede saturar un móvil. El efecto final surge de equilibrar esas variables, no de maximizar una sola.
También cambia la percepción: la celebración pertenece a la partida completa, no al botón “Jugar otra”. Visualmente cubre el contexto del resultado y por eso se entiende como cierre del reto.
Móvil no debía ser una versión empobrecida
En escritorio los ocho emisores ocupan casi todo el ancho inferior. En móvil se usan cinco y se reduce el número de partículas, pero se conserva la vida de 6 segundos. La experiencia mantiene tiempo y trayectoria; adapta densidad a espacio y rendimiento. Esta decisión sigue el mismo principio mobile-first que estamos reforzando en otras superficies.
Una interfaz móvil no debería ser simplemente la de escritorio encogida, pero tampoco debería perder todos los momentos expresivos. Lo importante es conservar jerarquía: reconocer el tablero, celebrar si corresponde, mostrar el resumen y permitir la siguiente acción. El modo de ocupar el viewport cambia; la secuencia no.
Esta adaptación es especialmente relevante en juegos porque el tablero ya consume gran parte del espacio. La celebración debe sentirse grande sin competir con los controles o hacer ilegible el resultado.
Reduced motion conserva significado sin conservar espectáculo
Cuando prefers-reduced-motion está activo se eliminan las animaciones fuertes y el confeti. La pausa de contemplación y el resumen permanecen. Dos usuarios reciben por tanto el mismo contenido semántico con cantidades distintas de movimiento. Ambos entienden qué ocurrió y ambos pueden continuar sin depender de una partícula animada.
Esta separación convierte la animación en mejora progresiva. Si un dispositivo no la reproduce, si una preferencia la desactiva o si una futura plataforma decide sustituirla, el cierre sigue funcionando. El HTML contiene el resultado y las acciones; el espectáculo es una capa adicional.
Implementarlo en un componente común evita que cada motor interprete por su cuenta qué significa reducir movimiento. Una preferencia del usuario se aplica al catálogo completo.
Sonido, texto y acciones también pertenecen a la capa compartida
El componente mantiene una preferencia local para sonido de feedback y centraliza tonos de resultado. Los motores no necesitan administrar una opción audiovisual global. Del mismo modo, el copy común incluye títulos de victoria, empate, derrota o puzzle resuelto y etiquetas para tiempo, movimientos, dificultad y acciones.
Ese copy existe en español, inglés, italiano, portugués, francés y alemán. El momento más emocional del juego no puede convertirse en una isla que vuelva al idioma por defecto. Centralizar frases comunes significa además que corregir una traducción beneficia a todos los motores.
Las acciones principales —jugar otra partida, ver estadísticas, cerrar— tampoco dependen de la animación. Deben seguir disponibles aunque el usuario reduzca movimiento o el confeti no pueda renderizarse.
La finalización combina hechos de sesión con contexto histórico
El resumen mezcla dos capas de información. Tiempo, movimientos, resultado o marcador describen la partida actual. Una marca personal o un logro recién desbloqueado comparan esa sesión con el historial. Mostrar ambas cosas juntas es útil, pero la interfaz debe dejar claro cuándo está hablando del presente y cuándo está haciendo una comparación.
Por eso el componente recibe GameResult y, cuando lo necesita, consulta el estado de progreso. Las estrellas pueden apoyarse en una mejor marca previa y una etiqueta puede anunciar “tu mejor tiempo”, pero las métricas básicas continúan siendo hechos de la sesión actual. En una primera partida, el resumen sigue funcionando sin inventar contexto.
Esta frontera también simplifica la persistencia. El componente no se convierte en dueño de los récords; sólo consume lo que el sistema de progreso ya sabe.
Los logros pudieron integrarse después sin reescribir el final
Cuando se implementó el nuevo sistema de logros, el resumen común pudo mostrar desbloqueos agrupados y enlazar a la colección. El componente no ejecuta reglas de logro. Reacciona al evento de desbloqueo y representa los resultados. El motor de logros, a su vez, no necesita saber cómo se anima la pantalla final.
Es un beneficio concreto de haber centralizado antes. Con modales distintos por juego, añadir esta capa habría obligado a repetir interfaz e integración en decenas de motores. Con una superficie común, la nueva información entra una vez y aparece de forma coherente en todo el catálogo.
La pantalla final empieza así a actuar como punto de integración: progreso aporta récords, logros aporta desbloqueos y futuras funciones podrán aportar contexto siempre que respeten una jerarquía compacta.
Los tests protegen intención visual, no sólo sintaxis
La suite comprueba temporización, confeti, estrellas, tokens compartidos y variantes de categoría. También protege que los colores no regresen a JavaScript y que la celebración mantenga la intensidad y duración definidas. Son tests poco habituales para UI, pero responden a decisiones que ya habían sufrido regresiones visibles.
Una prueba automática no puede decidir si una animación “se siente” excelente. Sí puede detectar que alguien redujo ocho emisores a uno, eliminó accidentalmente la pausa o dejó una categoría sin variante clara. Los tests conservan invariantes; la revisión humana juzga calidad perceptiva.
En una plataforma con muchos motores, esa combinación es más escalable que intentar reproducir manualmente todos los finales cada vez que cambia una hoja de estilos compartida.
El componente de cierre se volvió un punto de integración
Antes, cualquier función nueva relacionada con el final pedía una pregunta incómoda: ¿en qué motores hay que añadirla? Después de la centralización, la pregunta cambia a: ¿qué dato o evento necesita el componente común? Esa diferencia reduce el coste de futuras iteraciones.
Progreso ya puede aportar marcas personales. Logros puede aportar desbloqueos. Una futura función de compartir resultados o resumir un reto diario podría integrarse en el mismo punto siempre que respete la jerarquía y no convierta la pantalla final en un panel interminable.
La centralización no garantiza automáticamente una buena UX; sí garantiza que la conversación de producto ocurre en un lugar común. Eso hace posible revisar una vez la densidad, la accesibilidad y el orden de la información.
Por qué la celebración usa la categoría principal
Muchos juegos pueden pertenecer a varias categorías, pero una pantalla final necesita una identidad visual concreta. El componente utiliza la categoría principal expuesta por el host como ancla de tema. Eso evita mezclar varios colores de marca en una misma celebración y mantiene una relación reconocible con las tarjetas y superficies del juego.
La categoría principal no cambia el resultado ni el almacenamiento. Sólo selecciona variables visuales. Esa separación es importante porque permite reorganizar taxonomía sin contaminar GameResult con detalles de presentación. Los datos siguen describiendo la partida; el DOM aporta el contexto necesario para tematizarla.
También crea continuidad con el trabajo de progreso y exploración, donde cada categoría conserva su color a lo largo de la plataforma. El final de partida se convierte en otro punto donde esa identidad ayuda a orientarse.
Un componente común también reduce deuda de QA
Antes de la centralización, verificar un cambio de cierre podía significar revisar comportamientos ligeramente distintos. Una corrección en un motor no garantizaba que otro tuviera el mismo fix. Con una implementación compartida, la mayor parte de la regresión visual se puede cubrir en un único conjunto de pruebas, complementado por unos pocos casos representativos.
Eso no elimina la comprobación real en juegos concretos. Sigue siendo necesario validar que cada motor emite el outcome y las métricas correctas. Pero una vez que esos datos llegan bien, el comportamiento de panel, confeti, tema, sonido y accesibilidad ya no se reimplementa decenas de veces.
La reducción de superficie de QA es una consecuencia técnica directa de la consistencia. Menos implementaciones significa menos lugares donde el mismo bug puede reaparecer con una variante diferente.
Qué no resolvió esta iteración
El sistema común no obliga a todos los juegos a mostrar las mismas métricas ni a usar idéntico rating. La configuración sigue permitiendo especialización. Tampoco significa que toda la auditoría cromática del catálogo haya terminado: la PR dejó explícitamente fuera el trabajo exhaustivo por motor y lo vinculó a #174.
Ese límite es saludable. Un componente compartido se vuelve frágil si intenta anticipar cada excepción futura. Es mejor que controle el contrato: secuencia, resultado, densidad, accesibilidad, temas, acciones y puntos de extensión. Las diferencias reales entran por configuración.
La meta no era uniformar los juegos hasta que perdieran personalidad. Era eliminar duplicación accidental en el lugar donde todos hacen lo mismo: terminar.
La lección: el final de partida es una superficie de plataforma
El trabajo empezó como una mejora de confeti y terminó reforzando arquitectura. El final conecta datos, accesibilidad, tema, localización, progreso y navegación. Mantenerlo dentro de cada motor hacía que todas esas preocupaciones se repitieran y que cualquier novedad transversal costara mucho más de integrar.
Ahora el motor informa un resultado; el componente común decide cómo reconocerlo; CSS aporta identidad de categoría; las preferencias controlan movimiento y sonido; progreso y logros añaden contexto; y los tests protegen timing y estructura. El sistema admite especialización sin renunciar a una experiencia coherente.
La mejora visible son más cañones, más partículas y una celebración que por fin se ve por encima de la pantalla final. La mejora duradera es que la siguiente función de cierre podrá añadirse una vez y llegar a todo el catálogo sin volver a diseñar el final desde cero.
Lecturas relacionadas
La capa de datos que alimenta el resumen está en El progreso empieza cuando juegas. La integración posterior con recompensas se cuenta en De insignias sueltas a un sistema de logros. El contexto visual anterior está en El sistema de UI compartido.