Portada de «Cuando el progreso deja de mentir: rediseño de estadísticas en Puzzles y Cards» — Desarrollo de Blupoli.

El día que abrimos el panel y vimos un pentágono vacío

Las viejas estadísticas de Puzzles tenían un radar. Era bonito, geométrico y muy fácil de explicar en una demo: cinco ejes, cinco áreas cognitivas, un polígono que crecía con la práctica. Lo que tenía de bonito lo tenía de tramposo. Si el jugador aún no había tocado cuatro de las cinco áreas, el pentágono se dibujaba igualmente, con tres ejes clavados en el centro y dos ejes cortos a un tercio del recorrido. Eso produce una figura cerrada que parece información y solo es geometría residual. El radar nunca estuvo mintiendo con malicia; estaba mintiendo por defecto, como miente cualquier gráfico que dibuja un polígono cuando una variable vale cero.

Ese problema, el del visual que parece medición sin estar midiendo, era la punta de un iceberg. Debajo había tres más. Las estadísticas de Cards dependían de un contador local que se podía perder con una limpieza de navegador; las de Puzzles vivían duplicadas entre localStorage y los eventos de Supabase, con reconciliaciones a mano; y ambos productos tenían un botón de «Sincronizar» que el jugador olvidaba pulsar la mitad de las veces. El resultado era un ecosistema donde el progreso podía ser real o podía no serlo, y no había forma de saberlo sin abrir la consola del navegador.

A finales de septiembre habíamos completado dos movimientos previos que preparaban el terreno. La migración de Numberlink a un generador certificado había demostrado que medir antes de migrar es mejor que migrar y después mirar; la consolidación de Puzzles y Cards dentro de un mismo monorepo había hecho posible tocar ambas estadísticas en una sola tarde. Faltaba el impulso: cuándo decidimos que medir era lo importante.

La decisión: tres capas que no deberían estar enredadas

Cuando por fin nos sentamos a planificar, abrimos una hoja con tres columnas. La primera era datos: dónde vive la verdad, quién la escribe, cómo se reconcilia, qué pasa si dos pestañas guardan a la vez. La segunda era visualización: cómo se cuentan las partidas, cómo se muestran, qué no se debe mostrar nunca. La tercera era narrativa: qué parte del progreso se enseña al jugador en cada superficie, y por qué. Hasta entonces, las tres capas vivían mezcladas en un puñado de archivos y eso era exactamente lo que producía los pentágonos vacíos y los botones que se olvidaban.

Separarlas no era un capricho arquitectónico. Era la condición para que cada capa se pudiese auditar. Si la capa de datos sabe lo que tiene que saber (eventos idempotentes, cola durable, snapshots separados del dispositivo), puede responder «¿este jugador ganó 23 partidas y abandonó 4?» sin escuchar a la UI. Si la capa de visualización sabe solo lo que la capa de datos le pasa, puede renunciar a dibujar gráficos bonitos con valores en cero. Y si la capa narrativa decide qué enseñar en cada pantalla, puede hacerlo sin romper las otras dos al reorganizar contenido.

Ese fue el criterio que aplicamos para decidir qué aceptación exigía cada PR. Por eso la PR #465 cierra la primera columna, la PR #464 trabaja sobre la segunda y la tercera, la PR #467 pule el acoplamiento entre las tres, y la PR #474 corona el rediseño visual. La PR #470 y la PR #485 cierran cabos que las demás dejaron abiertos. Cada PR responde una pregunta concreta y deja la siguiente lista para que se pueda revisar sin acumular seis meses de diff.

Capa uno: que Supabase sea la verdad y la cola sea durable

El primer movimiento, la PR #465, fue casi enteramente invisible desde fuera. Sustituyó el botón «Sincronizar» por un guardado automático, hizo que Supabase fuese la única fuente de verdad para el historial y pidió a la UI que se suscribiese a los cambios en lugar de recargar la página. Suena limpio, pero limpiarlo costó integrar cinco PR parciales previas: #439, #451, #461, #465 y la propia automatización, y abordar dos связанных issues, #45 y #346. La razón de tantas idas y vueltas es que estábamos descubriendo, en cada paso, una suposición que ya no aguantaba.

La suposición más cara era que el jugador decidiría cuándo sincronizar. La evidencia llegó pronto: la mitad de los abandonos en Puzzles y Cards se quedaban sin subir nunca, porque nadie pulsa un botón después de perder. La nueva regla es que toda partida completada se envía a Supabase en cuanto el resultado se registra localmente con seguridad; el progreso agregado se escribe con un debounce corto y se reintenta en cambios de conexión, de visibilidad y al volver a abrir la pestaña. El indicador «Guardado en Supabase» solo aparece después de la confirmación remota, lo que evita la falsa tranquilidad de un «guardado» que luego se pierde.

Pero el guardado automático resuelve solo la mitad del problema. La otra mitad era el camino inverso: cuando el jugador abre Puzzles o Cards en una pestaña nueva, el cliente tiene que saber qué partidas ganó hace tres meses sin tirar de un snapshot local que ya no existe o de un localStorage que pudo haberse borrado. La respuesta fue una consulta directa a la tabla game_events filtrada por el usuario autenticado con RLS, sin conservar copias históricas en IndexedDB. La cola de eventos pendientes usa sessionStorage como proyección efímera y se vincula al identificador de la cuenta para que un cambio de usuario no filtre partidas a otro perfil. Cuando esa primera sincronización remota se confirma, las cachés locales antiguas se eliminan. No hay doble libro, y eso es exactamente lo que pedíamos.

El último detalle que cuidó esta PR fue el de los snapshots. Antes, una partida a medias vivía replicada en local y en la nube, con todos los problemas que eso crea cuando el dispositivo cambia. La nueva regla es que el tablero inacabado solo vive en el dispositivo; si el jugador cierra el navegador, continúa donde lo dejó, sin esperar a una sincronización. Cuando la partida termina, el snapshot local se elimina y el resultado remoto queda como único registro. Los dieciocho snapshots antiguos que quedaban sin migrar se dejaron en Supabase sin leer, marcados para una limpieza futura, en lugar de borrarlos y romper partidas que algunos jugadores podrían tener abiertas en otra pestaña.

La sorpresa: recargar la página era peor que el botón

Cuando dejamos a Supabase como autoridad, apareció un problema que no habíamos previsto. La PR #465 hacía que el cliente supiese cuándo habían llegado datos nuevos, pero la forma antigua de reflejarlo era llamar a location.reload(). Resultado: cada vez que el panel de estadísticas terminaba de hidratar, el navegador recargaba la página, parpadeaba un instante y devolvía al jugador al inicio. En Puzzles la sensación era leve; en Cards, donde la pestaña suele mostrar el solitario del día, era demoledora: parecía un reinicio cada treinta segundos.

La PR #467 y su cierre PR #468 sustituyeron la recarga por un evento: cuando Supabase confirma que el agregado de datos cambió, los consumidores actualizan sus proyecciones en el mismo documento, sin tocar motores ni esquema, y sin interrumpir la navegación. Detrás del cambio hay un módulo de sincronización compartido que emite eventos tipados y unas vistas que saben suscribirse a la parte que les interesa. Lo importante no es la técnica — son patrones conocidos — sino el efecto: el jugador ve su panel actualizarse en silencio, mientras sigue mirando el solitario que tenía abierto.

Para validar que la hidratación ya no recarga, escribimos regresiones de navegador específicas: hidratar la página, comprobar que la ruta no cambió, comprobar que el estado de juego sigue siendo el mismo, y solo entonces comparar los números mostrados con la fuente. Si la URL o el foco cambian, la prueba falla. Esta es la clase de regresión que no aparece en una rúbrica funcional pero que un jugador nota cada vez que la web se siente estable.

Capa dos: reanudar solo lo que no terminó, no todo lo que hay en disco

El siguiente movimiento fue más quirúrgico. La PR #470 corrigió un comportamiento heredado: si un jugador tenía tres partidas guardadas y solo había terminado dos, la aplicación intentaba reanudar las tres. En Puzzles la mayoría de las veces bastaba con pulsar «Continuar», pero en Cards abrir el menú para descartar manualmente lo que no querías jugar era un fastidio diario.

La regla nueva es simple y se aplica en la capa de motor: solo se ofrece reanudar una partida si el resultado guardado es inacabado. Las partidas terminadas se archivan y desaparecen del menú como opciones de reanudación, aunque siguen contando para las estadísticas y para el historial remoto. Este cambio es invisible cuando funciona, pero es exactamente el tipo de fricción que un jugador nunca comenta y que separa una app que cuida los detalles de una que no los cuida.

Lo interesante es que la lógica de reanudación vive ahora en un módulo compartido entre Puzzles y Cards, llamado desde el motor cuando empieza una partida. Eso permite que ambos productos hereden la regla sin reescribirla, y deja la puerta abierta a futuras variaciones (por ejemplo, reanudar con un número distinto de jugadas o pidiendo confirmación) sin tocar los juegos uno por uno.

Capa tres: la portada de Cards por fin enseña lo que juegas

Una vez que los datos se podían enseñar sin mentir, llegó el momento de enseñarlos. La PR #464 rediseñó la portada de Cards siguiendo el mismo lenguaje visual aprobado para Puzzles. Antes, la portada enseñaba cinco solitarios destacados en una rejilla regular; ahora muestra una composición clara con portada operativa, solitario del día con los siete días reales de la semana, cuatro destacados principales y un panel lateral de beneficios.

El panel lateral es la pieza nueva. Se llama bienestar semanal y reemplaza la antigua promesa de «rachas» por algo más concreto y menos punitivo. El bienestar se cumple con cualquiera de tres condiciones: cuatro días con al menos una partida ganada en la semana local, siete partidas en total o sesenta minutos activos. La barra de progreso se actualiza con cada resultado remoto confirmado y nunca infla: si hoy no has jugado, el porcentaje de la semana no avanza. Eso no es innovación; es la misma promesa de «no mentir» aplicada a la narrativa del producto.

Detrás del panel hay un dominio nuevo de Cards que cuenta partidas activas a partir de los eventos remotos, no del estado local. Esto significa que el bienestar se mantiene sincronizado entre dispositivos sin duplicar contadores, y que el cambio entre una cuenta y otra no acumula logros cruzados por accidente. La portada también reorganiza el catálogo de los trece solitarios para que se acceda a los más jugados primero sin perder la búsqueda ni los filtros. El rediseño se validó visualmente en pruebas E2E con capturas a 320, 390, 768 y 1440 píxeles, sin overflow móvil y conservando la cabecera en claro y oscuro.

El rediseño visual: diez categorías, barras sparse y un anillo SVG

La PR #474, que cerró la implementación y cuyo plan detallamos en el SDD correspondiente, fue el componente más extenso. Cambió la composición visual completa de las estadísticas de Puzzles para los tres ámbitos —Global, Categoría y Juego— sin tocar el modelo de datos, y rehízo las de Cards con selectores profundos y series de catorce días. En Puzzles, el cambio más visible fue el abandono del radar.

En su lugar aparece ahora un desglose por categoría con barras reales, que se ocultan automáticamente cuando no hay suficiente actividad para rellenarlas. Las categorías con cero partidas no se dibujan a un mínimo ficticio; se enumeran al final del desglose, con su etiqueta, en un orden estable, para que el jugador sepa que existen sin engañarse pensando que tiene práctica en ellas. Esa distinción —mostrar una barra corta o no mostrar nada — es la misma filosofía que llevó a quitar el pentágono, aplicada a un gráfico distinto.

Otra pieza importante fue la introducción del icono-juego y la jerarquía de cabeceras. Cada KPI lleva un pequeño marcador visual relacionado con el juego dominante (un número para puzzles numéricos, una pieza para Slitherlink, una torre para Castle Wall), de modo que la pantalla es escaneable sin tener que leer cada cifra. El héroe de la pantalla cambia cuando se selecciona una categoría o un juego: los totales se recalculan sobre ese ámbito, la cabecera explica qué incluye («solo partidas de juegos de esta categoría») y el desglose se vuelve a ordenar. La promesa explícita es que cada filtro representa exactamente lo que dice representar, sin agregaciones inventadas.

En Cards la lógica es ligeramente distinta porque la taxonomía es por juego y no por categoría cognitiva. La PR #474 introdujo un selector profundo de solitario con récords, mejor tiempo, racha del día, victorias fechadas y un anillo SVG para el porcentaje de victorias accesible por teclado y lector de pantalla. Se descartó el gradiente cónico prohibido por el sistema de diseño y se sustituyó por una composición de arcos planos con un porcentaje textual visible. La misma idea: si la métrica es real, el visual debe poder representarla con la paleta canónica, no con un efecto que la disimula cuando no hay datos.

Diagrama de capas que muestra arriba las fuentes remotas, en el medio el agregador de Supabase con cola durable, debajo las proyecciones por documento y al pie las pantallas de Puzzles, Cards y Home con tres tipos de visual
Tres capas separadas: datos en Supabase, proyecciones en el documento, narrativa en cada superficie. Mezclarlas fue lo que produjo los visuales engañosos.

Las diez categorías cognitivas, por fin

La PR #485 cerró un cabo que la #474 había dejado suelto: el viejo radar mostraba cinco categorías, no diez. Cuando la taxonomía cognitiva de Puzzles se amplió a ocho procesos y dos familias adicionales, el radar se quedó obsoleto y el desglose, ya en formato de barras, necesitaba alinearse. Esta PR corrigió dos cosas a la vez: primero, que las diez categorías se contasen exactamente una vez cuando un juego pertenece a varias, sin doble atribución; segundo, que las categorías con cero partidas se mostrasen al final, ordenadas, en lugar de inflarse a un valor residual.

Detrás del cambio hay un detalle de modelo: la atribución sigue siendo por categoría primaria, no por intersección. Si un juego es a la vez «Lógica» y «Numérico», cuenta en su categoría primaria y se documenta como secundaria. Esa elección evita el doble conteo, pero también evita el problema contrario: que un jugador con mucha práctica en una sola categoría infle todas las demás. Es una decisión menos vistosa que quitar el radar, pero más sostenible a largo plazo.

El plan original del equipo lo dejó por escrito: «mostrar diez ejes, una leyenda completa, legible en móvil y accesible, sin inferir habilidades cognitivas ni inventar actividad». Esa última cláusula merece destacarse. Algunas bibliotecas de autoevaluación cognitiva existen y prometen medir tu inteligencia a partir de tus partidas. Blupoli no entra en ese terreno. El panel cuenta cuántas partidas has jugado, cuánto te han llevado y cuántas has abandonado, y deja la interpretación cognitiva a quien quiera hacerla, con una herramienta que no se vende como lo que no es.

Nonogram: el tamaño 5×5 se va y entra el 7×7

Una decisión que pasó casi desapercibida pero merece atención: la PR #478 sustituyó el tamaño 5×5 de Nonogram por un 7×7 y la PR #481 cerró la retirada del generador heredado. ¿Por qué importa para un devlog de estadísticas? Porque introduce un cambio de tamaño en un juego ya publicado, lo que obliga a las propias estadísticas a manejar tamaños de partida con sesiones existentes en el dispositivo. La regla que aplicamos fue la misma que en el cambio de banco de Numberlink: las partidas guardadas en 5×5 se preservan por compatibilidad, pero los tableros nuevos se generan en 7×7, 10×10, 15×15 o 20×20 con dificultad independiente del tamaño.

La consecuencia visible es que el tamaño de partida medio en Nonogram sube ligeramente y los tiempos medios crecen, sin que signifique que el jugador sea más lento: hay más casillas que revelar. El panel de estadísticas por juego documenta la nueva distribución y un texto emergente aclara que las partidas antiguas siguen contando en su formato original. Es uno de esos cambios donde el visual casi no cambia, pero la política de medición cambia por completo.

Un día después, la PR #486 alineó la guía en español e inglés con los tamaños vigentes. Existía una incongruencia menor: la guía seguía describiendo los tamaños como si fuesen 5×5, 10×10 y 15×15, cuando la realidad llevaba varios días siendo otra. Es una corrección pequeña, pero la mencionamos porque la promesa de «no mentir» también se aplica al contenido editorial: si la documentación refleja un estado anterior, deja de ser fuente fiable.

Cómo se validó: rúbrica, E2E y una limitación que sigue ahí

El equipo documentó la calidad de la entrega con una rúbrica explícita: la PR #474 se puntuó a 95 sobre 100, descontando cinco puntos por dos razones. La primera es que no se hizo una comparación visual independiente entre el antes y el después en un conjunto amplio de dispositivos. La segunda es que la auditoría de contraste de los nuevos visuales se considera preliminar. Esas dos afirmaciones están escritas en el SDD correspondiente, no son un descuido. Reconocer lo que no se ha medido vale tanto como describir lo que se midió, y es una práctica que queremos mantener en cada entrega grande.

El resto de la rúbrica se cumplió: las nuevas estadísticas funcionan en los tres ámbitos sin cambiar el modelo, las de Cards conservan registros, récords, rachas y logros, la internacionalización cubre los seis idiomas publicados y los flujos E2E de filtros por solitario, móvil y traducciones están en verde. En el pipeline, el plan detallaba que «los flujos E2E comprueban que ningún cambio rompe una navegación ya existente y que los textos en idiomas traducidos son los correctos». Esa garantía se cumple con pruebas que recorren los filtros, simulan dispositivo móvil y comparan resultados agregados contra los datos remotos sin necesidad de hacer scraping.

Por último, las pruebas de navegador hidratan datos remotos sin recargar y verifican explícitamente que ni la URL ni el foco cambian. Si una PR futura rompe esa promesa, sabremos cuál fue. La promesa de «no mentir» exige la misma disciplina en la regresión que en el visual.

Qué cambió para el jugador

Para quien abre la app todos los días, lo más visible es que las pantallas se sienten estables. La portada de Cards enseña los siete días reales del solitario diario con marcas en los que ya se han jugado; el bienestar semanal avanza cuando corresponde y no avanza cuando no corresponde; los récords y las rachas viven en una pestaña dedicada; y los logros de Blupoli se desbloquean con la fecha real en que se cumplieron, no con la fecha de revisión.

En Puzzles, las estadísticas globales por fin se leen de un vistazo. La cabecera resume partidas completadas, mejor tiempo, días activos y tasa de finalización; el desglose por categoría muestra barras reales, ordenadas; cuando se selecciona un juego, los números dejan de mezclarse con el resto del catálogo. En el perfil de actividad, los diez ejes existen aunque varias categorías estén vacías; basta con jugar un par de partidas a un Kakuro o un Slitherlink para ver aparecer la barra. Esa progresión, de polígono cerrado siempre a barras que solo aparecen cuando hay datos, es probablemente la diferencia más pedagógica entre el antes y el después.

Para el mantenimiento, lo que cambió es que ya no hace falta reconciliar manualmente. Los eventos viven en Supabase, las proyecciones se calculan en el cliente desde esa fuente única, y cada PR que toque estadísticas tiene un plan SDD con objetivos, decisiones y validación abierta en docs/specai/. Si una métrica deja de cuadrar, sabemos qué capa romper y por qué.

Lo que quedó fuera a propósito

Quedan tres frentes abiertos que conviene nombrar. El primero es la comparación visual exhaustiva del rediseño entre dispositivos reales: la rúbrica lo dejó explícito y no lo hemos cerrado. El segundo es el contraste de los nuevos visuales, evaluado de forma preliminar pero pendiente de una auditoría específica con herramientas dedicadas. El tercero es la conexión entre logros y rachas de toda la plataforma: la pestaña de Streaks mantiene su comportamiento, pero el sistema de rachas general sigue siendo un objetivo marcado en content/devlog.json como siguiente paso, no como parte de esta entrega.

También queda un trabajo de armonización entre el blog y las nuevas estadísticas. Algunas guías mencionan cifras que ahora son distintas — por ejemplo, el catálogo de Nonogram, que pasó de cinco a cuatro tamaños documentados. La PR #486 ya alineó las guías en español e inglés; el siguiente paso será revisar las guías que tocan récords y rachas para que la promesa editorial sea coherente con la promesa técnica. Esa coherencia es, a la larga, más importante que el rediseño mismo.

La lección: medir mejor antes de mostrar más

Este recorrido deja una enseñanza que va más allá de las estadísticas. Cuando un producto acumula dashboards,botones y gráficos con el tiempo, la tentación es añadir más: otro panel, otra métrica, otra rúbrica. Aquí hicimos lo contrario: medimos qué mentía, separamos las capas que estaban mezcladas, y rediseñamos solo lo que sobrevivía a esa separación. La medida no añadió números; quitó números que confundían.

La segunda lección tiene que ver con la procedencia. Mover la autoridad a Supabase no fue copiar y pegar; fue decidir qué eventos se persisten, qué se reintenta, qué se descarta, qué se preserva para futuras auditorías. La confianza se delega a la herramienta, la responsabilidad queda en el código que decide qué se le pide.

Y la tercera es sobre el ritmo. Cinco pull requests en menos de una semana, cada una con su plan, sus tests, su validación y su historial de revisión, avanzaron más de lo que avanzaba cualquier sprint anterior con menos cuidado. La cadencia no la dicta el calendario editorial; la dicta la capacidad de cerrar cada PR con el plan cumplido. Esa cadencia es la que queremos seguir.

Puzzles y Cards ya no necesitan un botón de sincronizar para estar al día, ni un radar falso para mostrar diez categorías, ni una portada sin contexto para animar a volver. Midieron lo que importaba, mostraron lo que medían y dejaron de mostrar lo que no podían medir. La promesa editorial de Blupoli — acceso abierto, sin instalaciones, sin promesas inventadas — encontró esta semana su forma más honesta.