Portada de «De insignias sueltas a un sistema de logros para toda la plataforma» — Producto de Blupoli.

El problema de los logros empieza antes de la primera insignia

Añadir un logro a un juego aislado es sencillo. Añadir un sistema de logros a una plataforma con decenas de motores obliga a responder preguntas mucho más incómodas: quién decide qué significa jugar perfecto, cómo se compara una victoria de Ataxx con una solución de Sudoku, qué ocurre cuando el catálogo crece y cómo se evita que cada motor termine manteniendo su propio subsistema de recompensas. Ese era el problema real detrás de la PR #172.

La intención no era crear una colección vistosa y resolver la arquitectura después. Queríamos que los logros fueran una capa derivada de datos de partida fiables. La implementación terminó separando cuatro responsabilidades: los motores observan lo que sólo ellos pueden conocer, GameResult conserva la evidencia común, un evaluador central interpreta catálogo y reglas, y un repositorio persiste únicamente la parte irreversible del progreso.

La escala sirvió como test de diseño. En ese momento había 79 juegos catalogados. El motor podía expandirlos en 1.080 insignias base: 32 globales, 100 de categoría y 948 de juego. Además había 237 especiales, exactamente tres por juego. La cifra no es el objetivo del sistema; demuestra que la estructura puede describir un catálogo grande sin escribir 1.080 bloques de lógica independientes.

Doce espacios por juego sin doce copias del mismo código

El evaluador define doce posiciones conceptuales por juego: primera partida, veteranía, exploración, desafío, dominio, perfección, juego limpio o su equivalente, récords, tres especiales y maestría. La posición es común, pero su significado se adapta a las capacidades declaradas por cada motor. Un juego con tamaños y dificultades puede premiar cobertura de combinaciones; otro sin esas dimensiones puede apoyarse en consistencia o rachas.

Los competitivos tampoco se fuerzan dentro del molde de un puzzle de solución. El sistema distingue modo competitivo, victorias, derrotas, empates y dificultad de CPU. Eso permite compartir la gramática de progreso sin afirmar que todas las experiencias terminan en “resuelto”.

Esta estructura también hace que la colección sea legible para el jugador. Hay patrones que se repiten, de modo que no tiene que aprender una taxonomía distinta por juego. A la vez, el contenido especial conserva personalidad. La arquitectura común no busca que los motores sean iguales; busca que sus diferencias puedan expresarse dentro de contratos previsibles.

El dominio se deriva del catálogo

Una de las piezas centrales es domainDefinition(). En lugar de mantener a mano una lista de objetivos para cada juego, la función lee dimensiones declaradas como tamaño, dificultad o dificultad de CPU, variante y rol. Después construye las combinaciones que forman el dominio jugable relevante. Numberlink, por ejemplo, tiene un test que demuestra un dominio de cuatro tamaños por cuatro dificultades: dieciséis celdas.

Una sesión completada puede cubrir una de esas celdas. Por eso “dominar” un juego no equivale simplemente a repetirlo muchas veces. Significa recorrer la superficie que el propio producto considera significativa. El modelo admite exclusiones para combinaciones que no deban contar y cae en un dominio base cuando un juego no tiene dimensiones múltiples.

La ventaja aparece cuando el catálogo cambia. Si añadimos un tamaño o una variante, la definición de dominio puede crecer desde la fuente canónica. No hay una segunda constante escondida en el sistema de logros que alguien deba recordar actualizar. El logro se deriva del producto, no de una copia manual del producto.

Flujo desde GameResult y señales específicas de juego hacia el evaluador central, la persistencia irreversible y las superficies de logros
Los motores observan, GameResult registra, el evaluador interpreta y el repositorio conserva sólo lo que no debe perderse.

“Perfecto” necesitaba varios perfiles

La palabra perfecto parece universal hasta que se ponen juntos Sudoku, Numberlink, Ball Sort y Ataxx. En un puzzle de precisión importan pistas, correcciones y comprobaciones fallidas. En uno de flujo puede importar si se deshizo un camino. En un competitivo, una partida perfecta tiene que ser una victoria y puede depender de reinicios, takebacks o intentos ilegales. Un único criterio habría sido demasiado laxo para unos juegos y absurdo para otros.

El motor agrupa esas diferencias en perfiles como PRECISION, FLOW, CLEAN y COMPETITIVE. Cada perfil describe una familia de evidencia. Algunos motores conservan una condición específica cuando su mecánica realmente lo exige, pero el evaluador no necesita una rama completa por juego.

Este diseño permite explicar el logro. No es “perfecto porque un contador secreto llegó a uno”. Es perfecto porque existe una sesión completada y las métricas que ese tipo de juego considera relevantes fueron medidas y quedaron dentro de los límites definidos.

La ausencia de datos no es una partida perfecta

El trabajo previo del sistema de progreso había dejado una regla importante: no convertir “desconocido” en cero. Los logros hicieron esa regla todavía más necesaria. Una sesión antigua que nunca registró pistas no puede tratarse como si demostrara cero pistas. De lo contrario, actualizar la plataforma podría regalar logros de perfección retroactivos sin evidencia.

Por eso isPerfectSession() exige datos de la era de tracking de logros. Una sesión debe llevar achievementTrackingVersion y no estar marcada como evidencia incompleta. Las capacidades permiten distinguir entre una métrica realmente medida con valor cero y una métrica que el motor nunca registró.

La decisión sacrifica algunos desbloqueos históricos potenciales, pero conserva credibilidad. Un sistema de recompensas deja de tener valor si el usuario no puede confiar en que las condiciones significan lo que dicen. Preferimos no conceder lo que no podemos demostrar antes que rellenar el pasado con supuestos.

Los especiales debían ser específicos sin convertir el evaluador en 79 motores

Los tres logros especiales por juego son el punto donde una arquitectura genérica corre más riesgo de romperse. Un especial interesante debería hablar de la mecánica: una secuencia concreta, una relación poco habitual o una forma de resolver que sólo ese motor puede observar. Si el evaluador central conociera esas reglas una por una, acabaría duplicando conocimiento interno de todo el catálogo.

La solución separa observación y evaluación. El motor puede añadir achievementSignals a la metadata del resultado. El manifiesto content/achievement-specials.json declara qué señal, conjunto de señales o secuencia reciente necesita un especial. El evaluador sólo entiende tipos de regla generales.

Así, el código del juego sabe cuándo ocurre un hecho específico y el sistema de logros sabe cómo convertir hechos en progreso. Ninguno necesita apropiarse de la responsabilidad del otro. Esta frontera también hace que añadir un nuevo especial sea principalmente una tarea de declarar una señal fiable y una regla, no de ampliar un gran bloque condicional central.

Medir no debe cambiar el puzzle

Durante QA apareció una regresión que puso a prueba esa frontera. La medición del logro “Cascada” en Logic Matrix y Zebra Grid estaba analizando consecuencias lógicas de una forma que terminaba modificando candidatos automáticamente. El sistema destinado a observar el juego estaba alterando el flujo manual del propio juego.

La corrección separó análisis y mutación. La lógica puede detectar que una acción provoca una cascada de consecuencias y emitir la señal correspondiente sin aplicar esas consecuencias como ayuda automática. El jugador mantiene el control y el logro sigue teniendo evidencia.

Este fallo fue útil porque convirtió una preferencia arquitectónica en una regla concreta: la instrumentación de logros debe ser observacional siempre que sea posible. Si detectar una condición exige cambiar el tablero, hay que revisar la implementación porque probablemente se ha cruzado la frontera entre telemetría y mecánica.

Maestría no es repetir una tarea hasta llenar una barra

El logro de maestro de juego combina varias clases de evidencia. Los tests exigen dominio completo, al menos una partida perfecta, los tres especiales y un récord establecido. Es una definición deliberadamente compuesta: amplitud, calidad, conocimiento específico y rendimiento.

Esta combinación sólo es viable porque GameResult conserva contexto suficiente. El evaluador puede saber qué dimensiones se han cubierto, qué sesiones cumplen un perfil perfecto, qué señales especiales aparecieron y cómo evolucionaron las marcas. Si cada motor guardara contadores privados, combinar esos conceptos sería mucho más frágil.

La misma idea se extiende a categorías y logros globales. Experiencia, exploración, colección, perfección, récords, rachas, retos diarios, competitivos y dominio son familias distintas. Todas se derivan de eventos comunes y contexto de catálogo, no de una segunda base de datos de contadores invisibles.

Los récords necesitan contexto, no sólo un número mejor

Un mejor tiempo sólo es comparable cuando representa el mismo tipo de reto. El evaluador agrupa marcas por juego y por dimensiones como tamaño, dificultad, variante o rol. Resolver un tablero pequeño más rápido no borra la marca de una configuración grande o difícil. Son experiencias distintas y deben conservar referencias distintas.

La primera sesión medible establece una marca. Las posteriores sólo cuentan como mejora si superan la anterior en la dirección correcta. Los empates no generan un nuevo evento de récord. Hay tests específicos para ese detalle porque una igualdad tratada como mejora podría inflar toda una familia de logros.

Esta parte del diseño refuerza una idea que aparece también en el dashboard: las métricas sin contexto son fáciles de mostrar y difíciles de interpretar. El sistema de logros no debería recompensar ruido estadístico. Debe poder explicar qué marca se estableció y bajo qué condiciones se superó.

1.080 insignias base como guardrail de catálogo

El test de catálogo hace algo poco habitual en pruebas unitarias: verifica números globales de la plataforma. Con los 79 juegos disponibles en el repositorio de ese momento y forzados a estado preparado para la prueba, la evaluación produce 32 logros globales, 100 de categoría y 948 de juego. Los 237 especiales están además presentes como tres por cada juego.

Estos números sirven para detectar pérdidas estructurales. Si una refactorización elimina accidentalmente un slot, una categoría o un grupo de especiales, la suite no se limita a comprobar que una función sigue devolviendo un array: detecta que la colección ya no tiene la forma prometida.

No queremos congelar para siempre esos totales. El catálogo crecerá. Lo importante es que la relación entre juegos, slots y especiales sea verificable y cambie de forma consciente cuando cambie la fuente.

Los juegos futuros no deben hacer imposible el presente

En esa misma revisión había 32 juegos disponibles y 47 en coming-soon. Todos podían tener definiciones preparadas, pero sólo los disponibles debían participar en los denominadores de progreso. Si un juego futuro contara ya para “domina todos los juegos”, el objetivo sería inalcanzable antes de que el producto pudiera ofrecerlo.

El evaluador distingue estados ready, planned y disabled. Por defecto, un juego disponible entra como preparado y uno futuro como planificado. La suite comprueba tanto que todos los juegos jugables participan como que los coming-soon quedan fuera.

Esto permite trabajar por adelantado. Podemos definir especiales de un juego futuro sin alterar la experiencia actual. Cuando ese juego se publique, cambiar su estado lo incorpora al conjunto activo. Preparación editorial y disponibilidad de producto dejan de ser la misma cosa.

Las categorías se calculan sobre los mismos resultados

Los diez espacios de logros por categoría no necesitan una segunda copia del historial. El catálogo indica qué juegos pertenecen a cada categoría y el evaluador proyecta las sesiones existentes sobre esas agrupaciones. Si un juego pertenece a más de una categoría, el mismo GameResult puede contribuir a distintas vistas sin duplicarse en almacenamiento.

Esta decisión es especialmente útil mientras reforzamos la identidad visual y analítica de las categorías. Explorar, Mi progreso y Logros pueden utilizar la misma taxonomía canónica. No hay una lista paralela mantenida dentro del motor de recompensas.

Además, si una taxonomía cambia, los valores derivados pueden recalcularse. El historial personal sigue siendo el mismo. Cambiar la organización del catálogo no requiere reescribir las sesiones pasadas; sólo cambia la forma en que se agrupan para una pregunta concreta.

Un desbloqueo es irreversible aunque el catálogo evolucione

La persistencia vive en blupoli.achievements.v1 y guarda un conjunto pequeño de datos: nivel máximo alcanzado, timestamps de cada nivel, momento de completar, si era oculto y versión del catálogo al desbloquear. No almacena cada contador derivado porque esos contadores se pueden volver a calcular.

La operación de merge es monotónica. Si una evaluación antigua concedió nivel 2 y una evaluación posterior, quizá con un catálogo más grande, devuelve nivel 1 o cero, el repositorio conserva nivel 2. La suite prueba explícitamente que los logros no se revocan.

Esta regla protege la memoria del jugador. Añadir un tamaño, mover un juego de categoría o endurecer el dominio no debería borrar una insignia que la aplicación otorgó bajo condiciones válidas. El progreso hacia niveles futuros puede reajustarse; el pasado reconocido no desaparece.

Guardar menos facilita una sincronización futura

Persistir sólo lo irreversible reduce el número de verdades que hay que reconciliar. Las partidas, rachas, dominios cubiertos y marcas pueden derivarse de sesiones. Si también guardáramos esos totales como autoridad, cualquier migración tendría que resolver divergencias entre datos base y caché.

En el modelo actual, las sesiones son evidencia y el estado de logros es memoria. Esa separación encaja con una futura cuenta: dos dispositivos podrían fusionar resultados por identificador estable y conservar, para cada logro, el nivel máximo y los timestamps conocidos.

No significa que la sincronización ya exista. El producto sigue siendo local-first. Significa que el modelo no obliga a rehacer los logros cuando llegue la capa remota. Preparar una frontera futura sin implementar todavía el backend fue uno de los beneficios indirectos de diseñar el sistema sobre GameResult.

Un evento conecta resultados y logros

El servicio escucha blupoli:game-result. Cuando el repositorio de progreso acepta un resultado, el servicio carga catálogo, manifiesto de especiales e historial, ejecuta la evaluación y fusiona los nuevos niveles. Si aparecen desbloqueos, emite blupoli:achievement-unlocked.

Los motores no llaman a una función “comprueba mis logros”. Su responsabilidad termina al producir un resultado correcto y, si corresponde, señales específicas. Esto evita dependencias circulares y permite que otros consumidores reaccionen al mismo evento.

La arquitectura es sencilla de describir: una partida produce evidencia una vez; progreso, logros, rachas o futuras funciones la interpretan después. Esa secuencia reduce el riesgo de que cada sistema invente su propia versión de lo ocurrido.

La colección necesitaba una superficie propia

La PR #178 añadió la parte visible: una ruta principal /achievements/ con vistas Todos, Globales, Categorías, Juegos y Desbloqueados, además de búsqueda y filtro por estado. Los parámetros ?game= y ?category= permiten abrir una colección ya contextualizada.

Mi progreso no intentó convertirse en una pared de insignias. Conserva próximos objetivos y desbloqueos recientes, con un acceso a la colección completa. Las filas de categoría muestran su avance, las fichas de juego incorporan Logros X/12 y el final de partida puede llevar directamente a los logros del juego recién terminado.

Separar ambas superficies aclara su función. Progreso responde a “¿cómo estoy jugando?”. Logros responde a “¿qué he conseguido y qué retos quedan?”. Comparten datos, pero la densidad y el modo de exploración son diferentes.

Logros entró en el app shell

La navegación principal pasó a Inicio, Hoy, Explorar, Logros, Mi progreso y Rachas. Ese cambio obliga a tratar Logros como parte de la arquitectura de aplicación: responsive, rutas, SEO, sitemap y localización deben funcionar igual que en los demás destinos.

La implementación incluye español, inglés, italiano, portugués, francés y alemán. Los E2E verificaron casos concretos como los doce logros de Sudoku, los diez de su categoría y el comportamiento de búsqueda y filtros. También se validó el contrato responsive del shell con los seis destinos.

Un sistema de logros puede estar bien calculado y seguir siendo un mal producto si encontrarlo es difícil. Convertirlo en destino principal fue la decisión de UX que completó la capa técnica.

El final de partida es el momento natural para descubrir un desbloqueo

Cuando una sesión desbloquea uno o varios niveles, el componente compartido de finalización agrupa los nuevos logros. No se abren múltiples modales ni se obliga a cada motor a diseñar su propia celebración. El jugador ve primero el resultado de la partida y después la consecuencia de progresión.

Desde ese resumen se puede navegar a la colección del juego correspondiente. El enlace conserva el contexto y evita que una recompensa efímera desaparezca sin una forma de revisarla. La colección funciona como memoria y la pantalla final como descubrimiento.

Esta integración fue posible porque resultado, evaluación y presentación ya estaban separados. El motor no conoce la UI de logros y la UI no necesita interpretar las reglas internas del motor.

Las pruebas cubrieron sistema, catálogo y regresiones

La validación incluyó el catálogo completo, perfiles de perfección, dominio, récords, rachas competitivas, maestría, juegos planificados y persistencia sin revocación. También pasó la suite general, Playwright completo y previews de Firebase en el estado revisado de la rama.

Dentro de esa validación se ejecutó además el stress de Castle Wall con 8.000 puzzles con semillas. Ese test no pertenece directamente al motor de logros, pero ayuda a demostrar que una integración transversal que toca muchos motores no ha desestabilizado generación y ejecución existentes.

La meta no era sólo ver aparecer una insignia. Era poder integrar señales en 32 motores disponibles sin convertir la instrumentación en una fuente nueva de fallos de juego.

Qué evitamos construir

No hay un evaluador central lleno de if (game === ...). No se guardan contadores derivados como autoridad. No se considera perfecta una sesión histórica sin evidencia. No se incluyen juegos futuros en objetivos imposibles. No se revocan niveles ya concedidos y no se permite que medir un logro modifique el puzzle como efecto secundario.

Esos límites explican buena parte del código. Una lista pequeña de insignias habría necesitado menos arquitectura, pero habría acumulado deuda muy pronto. La decisión fue aceptar algo más de estructura ahora para que añadir juegos no multiplique sistemas paralelos.

La gamificación deja así de ser una capa decorativa pegada al final. Forma parte de los contratos de datos y de producto de la plataforma, pero sin invadir el motor de juego.

La lección: una recompensa necesita una fuente de verdad

Un logro sólo tiene valor si puede explicarse. GameResult aporta hechos; el catálogo aporta contexto; los motores aportan señales específicas cuando sólo ellos pueden observarlas; el evaluador transforma esa evidencia en progreso; el repositorio conserva lo irreversible; la UI decide cómo enseñarlo.

Esta cadena nos permite crecer sin volver a empezar. Un juego nuevo puede declarar dimensiones, aportar tres especiales, pasar de planned a ready y entrar en la colección sin reescribir el corazón del sistema. Una categoría puede cambiar y recalcularse sin migrar las sesiones. Una insignia ya ganada sigue siendo parte del historial.

Ésa es la diferencia entre añadir badges y construir un sistema de logros para una plataforma: la primera tarea produce recompensas; la segunda produce una forma estable de demostrar por qué existen.

Lecturas relacionadas

La base de datos que hizo posible esta capa se cuenta en El progreso empieza cuando juegas. El salto anterior hacia contratos comunes está en De añadir juegos a construir una plataforma verificable. La colección puede explorarse en Logros de Blupoli Puzzles.