Portada de «La misma partida, otra orientación: cerrando Number Match» — Desarrollo de Blupoli.

La pregunta que no podía responder una captura

Number Match llegó a Blupoli Puzzles el 27 de septiembre de 2026 con una propiedad que nos importaba mucho más que una cuadrícula bonita: cada tablero generado llevaba consigo una ruta constructiva capaz de vaciarlo. La Issue #239 y la PR #241 habían definido el juego nativo, la persistencia, Deshacer y Rehacer, pistas, el botón para añadir números, onboarding, estadísticas, logros, localización y una guía extensa. La primera entrega cerró ese alcance y pasó su verificación. Al día siguiente apareció una pregunta mejor que cualquier checklist: «¿esto se puede terminar de verdad?».

La duda era razonable porque una captura puede mostrar un 9 y no enseñar ningún 1 cerca. Si uno recuerda sólo la regla de “sumar diez”, ese 9 parece huérfano. Sin embargo, Number Match tiene dos relaciones válidas: dos valores iguales también forman pareja. Un 9 puede desaparecer con un 1, pero también con otro 9. Aun así, responder con la regla no bastaba. Había que revisar el generador, comprobar qué propiedad demostraban realmente los tests y separar esa cuestión matemática de dos defectos de producto que la revisión también había revelado: los cuatro “niveles” estaban haciendo a la vez de dificultad y tamaño, y el tablero seguía forzando nueve columnas visibles en móvil.

La Issue #280 y la PR #281 nacieron de esa segunda revisión. Su objetivo no fue rehacer Number Match ni sustituir un motor defectuoso por otro. Fue más incómodo y más útil: comprobar qué partes de la primera implementación eran sólidas, identificar dónde habíamos confundido conceptos y ajustar el contrato público para que lo que ve el jugador coincidiera con lo que el código realmente sabía.

Primero: qué significa exactamente que un tablero sea resoluble

El generador de Number Match no lanza dígitos al azar y luego confía en que aparezcan combinaciones. Construye bloques anidados. Un par exterior encierra uno o varios pares interiores; la secuencia de solución se registra empezando por el interior y avanzando hacia fuera. Cuando desaparece el par interno, deja huecos entre los extremos del siguiente. Esos extremos fueron creados como dos números iguales o como dos valores que suman diez. Por construcción, llega un momento en que su camino queda libre bajo las mismas reglas que usa el jugador.

Los bloques se concatenan para formar el tablero completo. Eso permite variar la profundidad sin cambiar las reglas y, al mismo tiempo, conservar una prueba sencilla: si recorremos las parejas constructivas en el orden almacenado y cada una es legal en el estado que le corresponde, al final no debe quedar ninguna celda activa. La función que ahora encapsula esa comprobación se llama followsConstructiveSolution. No usa una puerta trasera del motor. Invoca canMatch, la misma lógica pura que determina si dos casillas son una jugada legal.

Hay una distinción importante. Garantizar una ruta desde el tablero inicial no equivale a afirmar que cualquier secuencia arbitraria de jugadas legales acabará ganando. Number Match admite decisiones alternativas, y una decisión puede cambiar las conexiones posteriores. El botón “Añadir números” forma parte de la mecánica para posiciones sin parejas visibles, y Deshacer existe precisamente porque explorar otra rama puede ser útil. La garantía que podemos demostrar es concreta: el generador no publica una posición inicial sin una ruta completa conocida. Decir algo más fuerte exigiría demostrar una propiedad diferente sobre todos los estados alcanzables, y no tenemos esa prueba.

Esa precisión editorial importa. “Tiene una solución construida y verificada” es una afirmación útil y comprobable. “No puedes estropear nunca una partida” sería otra cosa. El cambio de esta revisión no intenta esconder esa diferencia; la lleva a tests, documentación y al texto que acompaña al juego.

Una prueba no es fuerte sólo porque tenga un nombre tranquilizador

La primera versión ya incluía tests. Había una prueba unitaria que generaba un tablero determinista por dificultad y seguía su secuencia constructiva, además de un E2E que hacía lo mismo en el navegador real. Esa cobertura encontró problemas reales durante la PR original y demostraba que el mecanismo funcionaba. Pero al revisar la pregunta “¿es realizable?” vimos que el espacio de entrada estaba a punto de crecer y que cuatro ejemplos ya no eran una evidencia proporcionada.

El nuevo test recorre los cuatro tamaños, las cuatro dificultades y veinte semillas distintas por combinación. Son 320 tableros únicos. Para cada uno exige determinismo, recuento correcto de celdas, al menos una jugada inicial y, sobre todo, que la secuencia constructiva completa pase por canMatch hasta dejar cero números activos. Generar el mismo caso dos veces también debe producir exactamente la misma estructura. El selfTest interno repite una muestra adicional sobre toda la matriz y comprueba que la profundidad media crezca de forma ordenada con la dificultad.

No convertimos “muchas semillas” en una prueba matemática de todos los enteros posibles. La parte que da la garantía es el método constructivo; la matriz sirve para atacar errores de implementación en la parametrización, los offsets entre bloques o combinaciones que una prueba única podría no tocar. Son dos tipos de evidencia distintos y complementarios: razonamiento sobre el algoritmo y ejecución repetible sobre una superficie amplia.

También conservamos un E2E de resolución en Playwright. La prueba abre el juego real, pulsa las parejas de la ruta constructiva a través del DOM y espera el estado de finalización común. Así cubrimos un puente que una prueba del núcleo no puede demostrar: que los índices producidos por el generador llegan intactos hasta los botones, que el motor elimina las mismas casillas y que solved() se dispara cuando el contador activo llega a cero.

El problema escondido en las antiguas “cuatro dificultades”

La revisión encontró algo más conceptual. El núcleo original tenía cuatro configuraciones llamadas Fácil, Media, Difícil y Experta. Cada una aumentaba dos cosas a la vez: el número de parejas y la profundidad media de los bloques. El resultado visible era 9×4, 9×6, 9×8 y 9×10. Técnicamente funcionaba; como modelo de producto era demasiado rígido. Elegir “Experta” implicaba aceptar también el tablero más largo, y pedir un tablero corto implicaba bajar la dificultad aunque una cosa no tenga por qué depender de la otra.

Blupoli ya tenía un lenguaje común para juegos que exponen ambas dimensiones. Numberlink, por ejemplo, declara tamaños y dificultades separadas. El motor de logros también entiende size y difficulty como dimensiones distintas. Mantener Number Match fuera de ese patrón obligaba al metajuego a interpretar una característica geométrica como si fuese una medida de dificultad.

La solución es ahora explícita. SIZES contiene 9×4, 9×6, 9×8 y 9×10. Cada tamaño determina cuántas parejas se generan. DIFFICULTIES conserva Fácil, Media, Difícil y Experta, pero su trabajo es controlar una profundidad objetivo. A partir de esa profundidad se calcula cuántos bloques constructivos necesita el tamaño elegido. Menos bloques para el mismo número de parejas significa, de media, cadenas más profundas; más bloques producen estructuras más cortas y más locales.

Con esta separación aparecen dieciséis combinaciones reales sin multiplicar reglas ni motores. Un 9×4 Experto puede ser pequeño y concentrado, mientras un 9×10 Fácil puede ser largo pero formado por bloques poco profundos. La persona decide cuánto tablero quiere gestionar y cuánta anticipación quiere exigir a sus movimientos. Eso es más expresivo y, sobre todo, más honesto con lo que cada selector significa.

La integración de logros dejó de ser una casilla marcada

Number Match ya tenía tres logros especiales en content/achievement-specials.json. “Sin ampliar” se activa al terminar sin usar Añadir números; “A simple vista”, sin pistas; “Sin vuelta atrás”, sin Deshacer ni Rehacer. El motor ya emitía las tres señales al completar la partida, así que esa parte de la integración era real y no necesitaba reconstruirse.

Lo que sí cambió es el dominio genérico de maestría. El sistema común inspecciona las variantes declaradas por cada juego. Si encuentra varios tamaños y varias dificultades, genera el producto de esas dimensiones para saber qué combinaciones existen. Antes, Number Match sólo declaraba dificultad, por lo que el dominio veía cuatro celdas. Ahora domainDefinition(game) devuelve dieciséis. La prueba específica de Number Match lo afirma de forma explícita para impedir que una futura edición del JSON vuelva a reducir el dominio sin que nadie lo note.

Esto tiene consecuencias en “Explorador”, “Desafío” y “Dominio”, pero sin crear lógica especial para Number Match. Ese es el punto de una plataforma: el juego declara correctamente su superficie y el sistema común hace el resto. Una integración es más creíble cuando desaparece código ad hoc, no cuando acumulamos condiciones con el nombre del juego.

Estadísticas: tamaño y dificultad también tenían que viajar con la sesión

La primera implementación ya llamaba a updateSession y a solved(). Guardaba movimientos, pistas, duración a través del host y una cadena de tamaño derivada de las nueve columnas y las filas iniciales. Por eso las estadísticas básicas existían desde el primer lanzamiento. Sin embargo, mientras tamaño y dificultad eran la misma decisión, esa cadena de tamaño no podía representar una elección independiente.

La revisión hace que cada sesión transporte size: 9xN y difficulty por separado desde el inicio, durante la persistencia y al completar. El identificador del puzzle incorpora tamaño, dificultad y semilla para evitar que dos variantes distintas compartan una identidad demasiado ambigua. La configuración de finalización incluye ahora size junto a duración, movimientos, pistas y dificultad.

El sistema de récords común ya agrupa métricas por dimensiones como tamaño y dificultad. No hubo que construir una “estadística de Number Match”. Hubo que enviar datos correctos al contrato compartido. Esta clase de trabajo es menos visible que una gráfica nueva, pero evita que dos partidas comparables sólo en apariencia terminen dentro del mismo récord.

El responsive original preservaba la lógica, pero no la forma

La primera PR tomó una decisión conservadora: mantener nueve columnas visibles en cualquier anchura y permitir desplazamiento interno cuando el tablero creciera. Era técnicamente segura porque el DOM reflejaba exactamente la cuadrícula lógica, y el E2E de móvil lo comprobaba de forma literal: esperaba nueve columnas CSS en un viewport de 390×844.

La revisión del juego cambió el criterio de producto. Un 9×4 es una forma claramente apaisada. Encoger nueve columnas para hacerlas caber en un teléfono desperdicia la dimensión vertical disponible y hace los objetivos táctiles más pequeños de lo necesario. La petición era simple de expresar: “que sea el mismo tablero, pero girado 90 grados”. La parte difícil era respetar la palabra “mismo”.

No podíamos regenerar con cuatro columnas en móvil. Eso cambiaría filas, diagonales, límites de lectura y todos los índices. Tampoco servía copiar los números a un segundo modelo orientado al teléfono: habría dos estados que sincronizar y el historial tendría que traducirse. La solución adecuada estaba una capa más arriba. La cuadrícula lógica sigue usando nueve columnas. Sólo la rejilla CSS intercambia ejes cuando la relación entre filas y columnas y el ancho del viewport lo aconsejan.

Girar la vista, no los datos

En un tablero lógico de nueve columnas y cuatro filas, el índice cero sigue siendo el índice cero. La función canMatch sigue calculando su fila con Math.floor(index / 9) y su columna con index % 9. La persistencia guarda el mismo array de celdas. El historial clona ese mismo array. Nada de eso sabe si la pantalla es estrecha.

El render sí conoce cuántas filas lógicas existen en el estado actual. Expone esa cantidad como una propiedad CSS y clasifica la forma como ancha, alta o cuadrada. En móvil, una forma lógica ancha se presenta con las filas lógicas como columnas visibles y las nueve columnas lógicas como filas visibles. La dirección de la rejilla completa el giro de noventa grados mientras cada número recupera su dirección normal para que el texto no rote. En escritorio hacemos lo contrario cuando la forma lógica es más alta que ancha.

El resultado conserva líneas rectas y diagonales: una relación horizontal se convierte visualmente en vertical y una vertical en horizontal. No hemos inventado una nueva adyacencia. También ajustamos las flechas del teclado. Si la vista está girada, pulsar izquierda o derecha debe recorrer el eje que el usuario percibe como horizontal, aunque internamente ese movimiento corresponda a subir o bajar una fila lógica.

Cuando Añadir números cambia la proporción del tablero, la clase se recalcula durante el render. Un tablero que deja de ser ancho puede volver a la orientación lógica en móvil. Esa transición ocurre en el momento en que la geometría acaba de cambiar de todos modos, y evita que una partida que ya tiene muchas filas termine creciendo lateralmente sin límite en una pantalla estrecha.

La compatibilidad de una partida guardada también era parte del “mismo tablero”

Añadir una dimensión nueva al estado podía tener una consecuencia poco visible: invalidar las partidas guardadas entre la versión del 27 y la revisión del 28. El almacenamiento común valida una versión de estado por motor, y aumentar esa versión habría limpiado snapshots anteriores que ya no coincidiesen. Para un cambio tan reciente podría parecer aceptable, pero no era necesario.

El snapshot de Number Match mantiene su versión y añade sizeRows de forma opcional. Cuando una partida antigua no lo tiene, el motor infiere el tamaño usando la relación histórica que sí era cierta en esa versión: Fácil era 4 filas, Media 6, Difícil 8 y Experta 10. Después de cargar, el siguiente guardado ya escribe el tamaño explícito. No intentamos preservar compatibilidad indefinida con formatos arbitrarios; preservamos una migración concreta que podemos deducir sin ambigüedad.

La misma lógica se aplica a parámetros de URL. Si una URL fija una semilla, un tamaño o una dificultad que no coincide con el snapshot almacenado, el juego no mezcla ambos estados. Limpia esa reanudación y genera la variante solicitada. Es preferible descartar una reanudación incompatible que mostrar un selector diciendo 9×4 mientras las celdas guardadas pertenecen a otro tamaño.

La orientación también necesitaba una prueba de identidad

Una prueba responsive que sólo mida ancho y alto puede pasar incluso si el juego ha cambiado de contenido. Por eso el nuevo E2E abre primero un 9×4 en un viewport de escritorio y captura el texto de todas las celdas. Después reduce el viewport a 390×844 y vuelve a leer el DOM. El array debe ser idéntico. Sólo entonces comprueba que la rejilla visible tenga cuatro columnas y nueve filas y que la altura sea mayor que la anchura.

Otra prueba abre 9×10 en escritorio y exige la forma inversa: diez columnas visibles, nueve filas y eje largo horizontal. Entre ambas queda codificada la intención del diseño sin duplicar el motor. Si alguien “arregla” más adelante el CSS volviendo a nueve columnas visibles en móvil, Playwright lo detectará. Si alguien decide regenerar el tablero al cambiar el viewport, la comparación de celdas también fallará.

La prueba de reanudación completa el triángulo: juega una pareja, recarga, verifica que siguen exactamente dos huecos y usa Deshacer para recuperar las dos casillas. Así la nueva dimensión no sólo existe en el selector; sobrevive al almacenamiento y convive con el historial común.

Blog y Devlog no son el mismo requisito editorial

La auditoría encontró que Number Match sí tenía una guía de Blog extensa en español e inglés, con portada, infografía, SEO y enlaces. También encontró que no existía una entrada de Devlog dedicada. No quisimos copiar la guía cambiando el tono. El Blog explica cómo leer parejas, huecos, sumas y estrategias desde el punto de vista del jugador. Este texto existe porque la historia técnica es distinta: una garantía de solvencia, una confusión entre tamaño y dificultad, una adaptación responsive que no puede cambiar topología y una integración transversal que había que demostrar.

La guía del Blog se actualiza para no conservar documentación falsa. Ya no dice que Fácil equivale a cuatro filas ni que un teléfono debe mostrar obligatoriamente nueve columnas en pantalla. Explica los cuatro tamaños, las cuatro dificultades y la idea de que nueve columnas siguen existiendo en el modelo aunque la presentación se gire. Desde allí enlaza a este Devlog para quien quiera la explicación de ingeniería; desde aquí enlazamos de vuelta a la guía de Number Match para no mezclar aprendizaje del puzzle con decisiones de implementación.

También conectamos esta historia con dos piezas anteriores: Generar no es resolver, porque aquí volvemos a distinguir una propiedad demostrable de una impresión visual, y El progreso empieza cuando juegas, porque tamaños y dificultades sólo aportan valor si llegan correctamente a la sesión común.

Qué quedó realmente integrado después de la revisión

El inventario final es más útil que decir “todo conectado”. Number Match usa el host nativo y su ciclo común de sesión. Guarda snapshots y restaura historial. Envía movimientos, pistas, tamaño, dificultad, identificador de puzzle y metadatos del tablero. Al terminar pasa por solved(), lo que alimenta el resultado común. Declara cuatro tamaños y cuatro dificultades para que logros y progreso entiendan el dominio. Emite tres señales especiales medibles. Tiene onboarding y localización del juego en los seis idiomas soportados. La guía de Blog existe en español e inglés y ahora refleja el comportamiento real. El Devlog técnico que faltaba es esta entrada.

Ese inventario también marca límites. La ruta constructiva demuestra que el tablero inicial tiene al menos una forma completa de resolverse. No hemos añadido un solver exhaustivo que certifique todas las continuaciones posibles después de cualquier elección humana. “Añadir números” sigue siendo una regla del juego, no una demostración universal de que cualquier estado arbitrario sea recuperable en un número finito de pulsaciones. Si quisiéramos afirmar eso, sería otra feature y necesitaría otra prueba.

Ser preciso con los límites aumenta la confianza más que una etiqueta de “garantizado” sin definición. Ahora sabemos qué garantiza el generador, dónde se verifica, cómo se representa una variante, qué sistemas consumen sus datos y qué parte corresponde sólo a presentación.

La lección: responsive y solvencia pertenecen a capas distintas

Este ajuste acabó tocando generador, UI, persistencia, estadísticas, logros, tests y editorial, pero la arquitectura se mantuvo simple porque cada capa conservó una responsabilidad clara. El núcleo decide qué parejas existen y si una ruta es legal. El motor guarda la partida y publica dimensiones. CSS decide cómo orientar la misma geometría para el dispositivo. El sistema de progreso consume dimensiones sin conocer Number Match. Los artículos explican dos historias distintas a dos lectores distintos.

La tentación ante una queja visual habría sido cambiar el número de columnas en móvil. La tentación ante una duda de solvencia habría sido añadir un texto tranquilizador. Ninguna atacaba el problema correcto. El tablero podía ser matemáticamente resoluble y, a la vez, tener un modelo de tamaños incompleto y un responsive incómodo. Separar esas preguntas nos permitió corregir cada una sin debilitar las otras.

Eso deja Number Match en una posición mucho más útil para seguir creciendo. Los cuatro tamaños pueden recibir ajustes de densidad sin redefinir dificultad. Las dificultades pueden calibrarse sin cambiar dimensiones físicas. El layout puede evolucionar sin migrar partidas. Los logros pueden medir cobertura 4×4 usando el motor común. Y cuando alguien vuelva a preguntar si una partida concreta nace con solución, habrá una respuesta que no depende de confianza: el generador conserva la ruta y los tests la recorren.

Volver al tablero con una pregunta mejor

Después de todo este trabajo, el gesto del jugador sigue siendo exactamente el mismo: mirar dos números y decidir si pueden desaparecer. Eso es buena señal. La complejidad añadida vive detrás del selector, del guardado y de la orientación, no encima de la regla principal.

Si ves un 9 y no encuentras un 1, busca otro 9. Si no ves ninguna pareja, recuerda que igualdad y suma de diez son sólo la primera capa; también importa el camino. Y si el tablero aparece alto en el móvil y ancho en el escritorio, no estás viendo dos puzzles. Estás viendo la misma secuencia, la misma semilla y el mismo historial presentados de la forma que mejor aprovecha cada pantalla.

Puedes probarlo en Number Match en Blupoli Puzzles. Para aprender la estrategia desde el lado del jugador, la guía completa entra en parejas, huecos, lectura por filas y planificación. Este Devlog queda como registro de la otra mitad: qué tuvimos que demostrar y separar para que “el mismo tablero” significara realmente el mismo tablero.

Diagrama de Number Match que separa topología lógica, presentación responsive, tamaños, dificultades y pruebas
La revisión separa cuatro responsabilidades: reglas, orientación, variantes y evidencia automatizada.