Cuando empezamos a hablar de solvers como infraestructura de Blupoli podía sonar a que acabaríamos construyendo una especie de algoritmo universal. La realidad ha sido más interesante. Cuanto más amplio se vuelve el catálogo, más evidente resulta que la representación correcta de cada puzzle importa tanto como la búsqueda.
Dominosa y Stitches pueden describirse como problemas de emparejamiento, pero sus restricciones no son idénticas. Slant necesita vigilar ciclos mientras coloca diagonales. Aquarium impone una geometría de niveles de agua. Str8ts combina unicidad de filas y columnas con secuencias dentro de compartimentos. Kropki convierte cada frontera entre celdas en una relación.
La parte compartida no es el algoritmo
Lo que sí se repite es el contrato. Un motor candidato debe poder responder preguntas concretas: ¿este estado cumple las reglas?, ¿existe al menos una solución?, ¿existe exactamente una cuando el juego lo exige?, ¿podemos reproducir el caso?, ¿la dificultad que anunciamos tiene alguna relación con el trabajo lógico?
Ese contrato permite que dos implementaciones internas completamente distintas se integren en la misma plataforma y pasen por gates similares.
Dominosa: cuando el tablero se convierte en exact cover
En Dominosa cada casilla debe pertenecer a un dominó y cada pareja de valores permitida debe utilizarse exactamente una vez. Visto así, el puzzle deja de ser una cuadrícula decorada y se convierte en una selección de piezas candidatas que deben cubrir simultáneamente posiciones y tipos de dominó.
Esta formulación encaja de manera natural con exact cover. Cada colocación posible cubre un conjunto de requisitos; resolver significa elegir un subconjunto compatible que cubra todos exactamente una vez. La representación reduce mucha lógica especial a un problema combinatorio más limpio.
Stitches: emparejar sin confundirlo con Dominosa
Stitches también conecta celdas o regiones, pero la semántica del enlace es diferente. El solver debe respetar qué regiones pueden conectarse, cuántas puntadas necesita cada una y qué pares siguen disponibles.
El aprendizaje aquí fue evitar una abstracción prematura. Compartir utilidades de búsqueda o conteo puede ser útil; forzar ambos juegos dentro del mismo modelo porque «los dos emparejan cosas» habría hecho el código menos claro, no más.
Slant: una restricción global escondida entre diagonales locales
Slant parece un puzzle de decisiones binarias: cada celda contiene / o \. Las pistas numéricas en los vértices imponen cantidades locales. El problema es que una configuración puede satisfacer todas esas cuentas y aun así ser inválida si las diagonales forman un ciclo.
Eso obliga al solver a combinar dos escalas. Las pistas permiten podar decisiones locales; una estructura de conectividad debe impedir que aparezcan bucles. Es un buen ejemplo de por qué validar solo el aspecto más visible de las reglas produce falsos positivos.
Aquarium: el agua no se decide celda por celda
Aquarium utiliza pistas de filas y columnas, pero las regiones que representan acuarios obedecen una propiedad física estilizada: el agua queda nivelada. Si una celda de un acuario está llena a cierta altura, las celdas del mismo acuario situadas por debajo también deben estarlo.
Tratar cada celda como una variable independiente desperdicia estructura. Modelar niveles posibles por región reduce estados imposibles desde el principio y hace que el solver piense en la misma unidad conceptual que el jugador.
Str8ts: las casillas blancas forman intervalos
Str8ts hereda parte del vocabulario de los puzzles latinos —sin repetir números por fila o columna—, pero su restricción decisiva vive dentro de cada compartimento blanco: los valores deben formar una secuencia consecutiva, aunque puedan aparecer en cualquier orden.
Eso permite razonar con intervalos. Si un compartimento de tres casillas ya contiene un 4 y los candidatos restantes solo permiten 2, 3, 5 o 6, no todas las combinaciones tienen sentido: necesitamos algún conjunto de tres valores consecutivos que incluya el 4.
Kropki: las fronteras también son variables de información
En Kropki, puntos blancos y negros relacionan vecinos, pero la ausencia de punto también restringe cuando usamos la regla negativa completa. El solver no puede mirar únicamente el contenido de las celdas; tiene que interpretar cada frontera ortogonal como una relación explícita.
La heurística de elegir primero la celda con menos candidatos funciona bien aquí porque cada número colocado activa restricciones en fila, columna y vecinos. Pero la eficacia procede del modelo de candidatos, no de que el backtracking tenga algo especial por sí mismo.
Solución única: el segundo resultado importa más que el primero
En muchos de estos juegos el solver se usa durante generación. Encontrar una solución significa que el candidato no está roto. Encontrar una segunda significa que todavía no está terminado.
Por eso varios contadores se detienen en dos. No necesitamos enumerar todo el espacio; solo distinguir cero, una o más de una solución. Esta pequeña decisión transforma al solver en una herramienta práctica para el build y para la generación en navegador.
Los motores nativos también mejoran el producto alrededor del tablero
Sustituir una plantilla fija por un motor verificado no solo añade niveles. Permite tener semillas reproducibles, cuatro dificultades coherentes, botón de nueva partida, estadísticas comparables y onboarding que puede trabajar sobre estados reales.
Es una de las razones por las que estamos priorizando profundidad antes que aumentar el catálogo sin control. Un juego deja de ser una demo cuando puede producir suficientes partidas fiables para sostener una sesión real.
No buscamos un framework de solvers por deporte
Hay una tentación muy de ingeniería: después de implementar varios algoritmos, extraer inmediatamente un framework capaz de representarlos todos. Estamos intentando resistirla. Compartimos contratos, instrumentación y piezas que ya han demostrado repetirse; dejamos que la lógica específica siga siendo específica.
Una abstracción vale la pena cuando elimina repetición sin borrar información importante. En puzzles, la información importante suele ser precisamente la forma particular de la restricción.
La diversidad del catálogo está refinando la arquitectura
Aquarium, Str8ts, Dominosa, Kropki, Stitches y Slant llegaron casi seguidos, y esa concentración ha funcionado como una prueba de estrés. Hemos tenido que alternar entre grafos, matching, exact cover, intervalos y relaciones locales sin perder el contrato de producto.
La conclusión no es que tengamos seis trucos algorítmicos nuevos. Es que Blupoli empieza a tener una forma consistente de incorporar algoritmos distintos, demostrar que hacen lo que prometen y rodearlos de una experiencia común. Esa diferencia es mucho más importante para los próximos 75 juegos que cualquier solver aislado.
Modelar bien suele ahorrar más que optimizar tarde
Antes de discutir heurísticas conviene preguntar qué representa una variable. En Aquarium puede ser un nivel de agua por región; en Dominosa, una colocación; en Slant, una diagonal y sus conexiones. Elegir la unidad correcta elimina estados imposibles antes de la búsqueda. La primera optimización suele ser semántica.
La propagación debería hacer todo lo posible antes de ramificar
Backtracking no significa adivinar pronto. Un motor práctico aplica consecuencias hasta que no puede avanzar más. Candidatos, niveles, emparejamientos y ciclos pueden reducir el espacio enormemente. La ramificación empieza cuando la deducción se agota. Esta disciplina mejora rendimiento y produce señales útiles de dificultad.
Encontrar una solución no demuestra que el puzzle esté listo
Cuando el producto promete unicidad, el primer resultado demuestra que el candidato es resoluble; el segundo que no es publicable. Por eso el contador continúa y se detiene al llegar a dos: cero imposible, uno único, dos “más de uno”. Esta diferencia se desarrolla también en Generar no es resolver.
Contar hasta dos convierte el solver en infraestructura
El algoritmo deja de ser un botón de mostrar respuesta y se convierte en un gate usable en generación, CI y herramientas internas. Responde si el candidato cumple el contrato sin enumerar información innecesaria.
Las seeds reproducibles son parte del contrato
Cuando una partida falla, “genera otra” no sirve. Una seed, ID o estado serializado permite reconstruir el caso. También hace posibles benchmarks sobre una muestra estable.
Serializar protege contra la evolución del generador
Una seed puede producir algo distinto si cambia la generación. Guardar el estado esencial conserva regresiones, facilita diagnósticos y mantiene el puzzle comprensible fuera del DOM.
Generador y solver deberían desconfiar el uno del otro
Si el mismo código crea y certifica una partida, un error conceptual puede repetirse en ambas fases. Invariantes independientes, casos manuales, contadores separados o property tests aportan una segunda perspectiva.
Los tests de propiedades complementan las fixtures
Los casos fijos conservan bugs históricos. Property-based testing explora muchos estados y comprueba invariantes: toda solución respeta reglas, toda partida declarada única tiene una solución y serializar conserva significado. Un fallo descubierto puede convertirse en fixture permanente.
Metamorphic testing añade otra forma de buscar inconsistencias
Renombrar símbolos consistentemente, aplicar una simetría permitida o serializar y cargar no debería invalidar una solución. Estas relaciones ejercitan el motor sin necesitar una salida exacta escrita a mano para cada entrada.
Los casos adversariales merecen un lugar permanente
Una suite necesita estados casi válidos, contradicciones tardías, soluciones múltiples y ejemplos que fuerzan la regla global más cara. Cada bug real puede convertirse en una prueba que documenta una frontera del motor.
La dificultad necesita señales propias de cada familia
No existe una fórmula universal que iguale un difícil de Aquarium con uno de Kropki. Cada motor observa profundidad, candidatos, técnicas, densidad o cadenas. Blupoli puede compartir etiquetas mientras cada juego las calibra con evidencia apropiada.
Difícil no debería significar simplemente menos pistas
Eliminar información puede aumentar el reto, pero también crear ambigüedad o una deducción obvia. Un solver instrumentado ofrece propagación, bifurcaciones y profundidad: una base mejor que contar huecos.
El rendimiento necesita casos reproducibles
Una optimización puede mejorar un tablero y degradar otro. Seeds estables, tamaños representativos y casos patológicos permiten comparar percentiles y peores casos. En generación interactiva, una congelación larga pesa más que pequeñas mejoras medias.
La memoria forma parte del presupuesto
Un solver puede podar bien y consumir demasiado si copia estructuras grandes en cada rama. Rollback, representaciones compactas y mutabilidad controlada pueden ser tan importantes como el orden de búsqueda.
Los solvers largos necesitan cancelación
Una búsqueda debería detenerse si el jugador cambia de dificultad, inicia otra partida o abandona la pantalla. Un resultado tardío desperdicia recursos y puede competir con estado más reciente.
Los Web Workers pueden proteger la interfaz
Mover cálculo fuera del hilo principal mantiene la UI responsiva, pero introduce serialización, cancelación y control de respuestas obsoletas. La concurrencia merece medirse, no aplicarse por reflejo.
La caché de estados exige definir equivalencia
Memoizar puede ahorrar trabajo, pero la clave debe incluir variante, tamaño, reglas opcionales y estado. Solo podemos cachear con seguridad cuando sabemos qué hace equivalentes dos estados.
El solver no debería saber cómo se dibuja el tablero
La lógica no debería depender de CSS, DOM o animaciones. El solver necesita un modelo del puzzle. Esta frontera es central en la arquitectura de motores de Blupoli Puzzles.
La UI tampoco debería reimplementar las reglas
Si el render decide por su cuenta si una acción es válida, aparecen dos fuentes de verdad. La interfaz debe preguntar al dominio o representar estado ya validado.
Validator y solver deben compartir qué significa terminado
Una fuente clásica de bugs es que la UI declare victoria mientras el solver aplicaría otra regla. Completitud, validez y conteo necesitan una definición de dominio común.
Resolver y explicar no son la misma tarea
Un backtracking puede hallar una respuesta mediante una rama que sería una pista pésima. Un sistema de hints necesita elegir una deducción comprensible, justificarla y revelar solo lo necesario. Tener solver no significa tener tutor.
Los hints necesitan límites pedagógicos
Una pista técnicamente correcta puede revelar demasiado o apoyarse en una adivinanza que el producto no quiere fomentar. El solver aporta evidencia; la capa de producto decide qué mostrar.
Las migraciones de formato merecen tests de dominio
Si cambia la serialización, JSON válido no demuestra que el significado sobreviva. El puzzle migrado debe conservar pistas, variante, progreso y propiedades de solución.
Los cambios de reglas necesitan procedencia
Una seed o partida guardada puede representar un contrato anterior. Si una variante evoluciona, conviene conservar suficiente versión para entender por qué un caso histórico se comporta distinto.
La mejor abstracción aparece después de varios motores
Cuando una utilidad se repite en Dominosa, Kropki y Slant, tenemos evidencia para extraerla. Antes, un framework universal puede codificar repetición imaginada.
El determinismo facilita distinguir azar de regresión
Si la misma seed y versión producen resultados distintos, depurar rendimiento y lógica se vuelve ruidoso. Un modo determinista permite congelar orden de candidatos y aleatoriedad para reproducir una incidencia paso a paso, aunque el producto siga ofreciendo variedad a los jugadores.
La cola de latencia importa más que una media bonita
Un solver con buen promedio puede ser problemático si un pequeño porcentaje de partidas tarda varios segundos. Los benchmarks deben incluir percentiles altos y casos patológicos, porque una sola congelación larga puede dominar la percepción de calidad.
La procedencia del motor ayuda a explicar casos históricos
Guardar suficiente contexto de versión junto a fixtures duraderas permite distinguir “el algoritmo cambió” de “el estado se corrompió”. No hace falta versionar cada refactor, pero sí conservar contexto cuando afecta a la interpretación del caso.
Cada bug corregido debería convertirse en regresión
Una corrección gana valor cuando conserva el estado que la provocó y la propiedad que ahora debe mantenerse. Con el tiempo, la suite se convierte en una historia ejecutable de los límites del motor y permite optimizar con más confianza.
La arquitectura compartida está en el contrato de evidencia
Dominosa usa exact cover, Slant conectividad, Aquarium niveles y Kropki relaciones. Lo común es definir estado válido, completitud, solución, unicidad cuando toca, reproducibilidad e instrumentación.
El objetivo no es que todos compartan algoritmo
La diversidad matemática es una fortaleza. Compartimos disciplina: modelar con precisión, propagar pronto, buscar cuando hace falta, contar cuando la unicidad importa, medir casos reproducibles y convertir bugs en regresiones.
Validator y solver necesitan acuerdo explícito, no solo intuición compartida
Que dos piezas de código “usen las mismas reglas” no basta como garantía. La función que decide si el jugador ha terminado puede estar optimizada para una comprobación rápida, mientras el solver usa una representación distinta para explorar estados. Esa separación es sana, pero obliga a probar que ambas interpretaciones coinciden. Fixtures completas, casi completas e inválidas deberían pasar por los dos caminos y producir decisiones compatibles.
La ventaja de esta prueba cruzada es que convierte un supuesto arquitectónico en evidencia. Si un tablero recibe celebración visual y después el solver lo rechaza, el problema deja de ser una rareza de UI: existe una contradicción de dominio que debe fallar en CI.
Los hints necesitan una política de producto además de lógica correcta
Un solver puede justificar una respuesta mediante una bifurcación profunda, pero eso no significa que esa explicación sea una buena pista. Una ayuda útil debería respetar las técnicas que queremos enseñar, la dificultad elegida y cuánto conocimiento revelar. Puede ser correcto decir “esta celda es 7”, pero pedagógicamente más valioso explicar qué candidatos se eliminaron y por qué.
Separar solver de hint engine permite conservar ambos objetivos. El primero busca verdad y evidencia; el segundo convierte una parte de esa evidencia en una intervención comprensible. Así evitamos que la función de ayuda se convierta en un simple botón de resolver.
Las trazas de resolución pueden diseñarse para ser explicables sin acoplarse a la UI
Si en el futuro queremos hints más ricos, resulta útil que el dominio emita eventos semánticos: candidato eliminado por relación vecina, intervalo descartado, ciclo evitado, colocación obligatoria. Son conceptos más estables que “entró en la rama 17”.
Esas trazas no tienen que convertirse en una narración exhaustiva ni almacenarse siempre en producción. Pueden existir como instrumentación de desarrollo y, cuando convenga, alimentar una capa de explicación localizada. El punto es conservar significado suficiente para que la lógica pueda enseñarse sin depender de cómo estaba implementado el backtracking.
La cola de latencia importa más que una media favorable
Un solver que tarda 40 ms de media puede seguir siendo una mala experiencia si uno de cada cien casos bloquea varios segundos. La generación interactiva se percibe por sus peores momentos, no por una media abstracta. Por eso los benchmarks necesitan percentiles altos, límites máximos y un conjunto de casos patológicos conocidos.
Esta observación también cambia el gate de generación: si un candidato no puede verificarse dentro del presupuesto, no rebajamos unicidad. Lo descartamos, lo procesamos en otro contexto o ajustamos el generador para evitar esa región costosa del espacio.
El determinismo de diagnóstico no está reñido con la variedad del producto
Podemos ofrecer partidas diferentes a los jugadores y, al mismo tiempo, disponer de un modo donde la misma seed, versión y configuración sigan exactamente el mismo orden de decisiones. Esa capacidad facilita comparar perfiles, reconstruir fallos y entender por qué una optimización alteró el comportamiento.
La aleatoriedad es una herramienta de contenido. La reproducibilidad es una herramienta de ingeniería. Tratarlas como capas distintas evita elegir entre variedad y mantenibilidad.
La procedencia de una fixture puede explicar cambios legítimos
Un caso guardado hace meses puede haber sido generado con otra heurística, otro formato o una variante anterior. Sin contexto, una diferencia futura parece una regresión aunque sea consecuencia deliberada de una migración. Guardar metadatos suficientes —versión de formato, variante y, cuando importa, generación— permite interpretar correctamente esos casos.
No se trata de versionar cada línea de código. Se trata de que los artefactos duraderos conserven aquello que afecta a su significado.
Las migraciones tienen que conservar propiedades de solución
Una conversión de formato no está terminada solo porque el nuevo JSON sea válido. Para una partida única, el estado migrado debería seguir teniendo la misma solución y las mismas restricciones. Para una partida en curso, las acciones válidas y el progreso deberían conservarse.
Ejecutar validator y, cuando procede, solution count sobre fixtures antes y después de migrar da una garantía mucho más fuerte que comprobar únicamente tipos y campos. El dominio debe sobrevivir a la migración, no solo la estructura.
Cada bug real debería aumentar la memoria del motor
Cuando arreglamos una partida ambigua, una contradicción no detectada o una explosión de búsqueda, el arreglo es más valioso si conserva el caso que lo provocó. Esa fixture y la propiedad asociada se convierten en una barrera contra el mismo error.
Con el tiempo, la suite de regresión empieza a contar la historia del motor: qué supuestos fallaron, qué fronteras resultaron difíciles y qué optimizaciones no pueden romperse. Esa memoria ejecutable permite cambiar algoritmos con mucha más confianza.
La plataforma debería compartir observabilidad, no imponer matemática
Una abstracción común sí puede ofrecer contadores, timeouts, cancelación, logging de desarrollo, formato de seeds y hooks de benchmark. Son responsabilidades que aparecen en muchos motores sin alterar su modelo interno.
Este tipo de reutilización resulta más valiosa que obligar a Dominosa, Slant y Aquarium a fingir que resuelven la misma clase de variable. Compartimos las herramientas para entenderlos; dejamos que cada uno exprese sus restricciones con honestidad.
El criterio de extracción debería ser evidencia de repetición
Cuando una función o contrato aparece de forma similar en tres o cuatro motores reales, ya tenemos datos para decidir si pertenece a la plataforma. Antes de eso, generalizar puede crear un framework que obliga a escribir adaptadores para casos que nunca fueron iguales.
Esta manera de construir es más lenta al principio y más barata después: la capa común nace de problemas comprobados, no de imaginar todos los puzzles posibles. La diversidad del catálogo deja así de ser un obstáculo y se convierte en una prueba continua de nuestras abstracciones.
El solver común de Blupoli es una disciplina de evidencia
Después de Dominosa, Stitches, Slant, Aquarium, Str8ts y Kropki, la conclusión no es que debamos buscar el algoritmo que los unifique. La conclusión es que podemos unificar cómo exigimos confianza: modelos explícitos, propagación, reproducibilidad, unicidad cuando corresponde, presupuesto de rendimiento, tests adversariales y regresiones.
Ese contrato permite seguir añadiendo mecánicas sin perder rigor. El catálogo puede crecer en variedad porque la plataforma no confunde diversidad matemática con ausencia de estándares. Es la misma tensión que aparece al pasar de un puzzle aislado a una plataforma con decenas de juegos: la coherencia viene de los contratos compartidos, no de fingir que todos los motores son iguales.