Demostrar que cada nivel tiene solución antes de publicarlo
Un nivel de puzle solo merece publicarse si alguien ya lo ha ganado. En Sunny Sort ningún nivel llega a un teléfono antes de que una búsqueda sin conexión lo juegue entero, lo puntúe y lo pase por un conjunto de filtros — y los niveles que fallan se descartan, no se parchean.
El fallo que lo forzó
Los niveles salían directamente de una semilla aleatoria. El generador existía como un esbozo que imprimía generate: not implemented, y el catálogo era lo que las semillas produjeran por casualidad.
Probar diez niveles difíciles reveló que cuatro de ellos no se podían ganar en absoluto, ni siquiera con una búsqueda de haz de anchura 2000. Un jugador que llegara al nivel 13 chocaba contra un muro, y el juego anunciaba OUT OF SPACE como si la derrota fuera suya.
Eso es peor que un problema de dificultad. Un nivel difícil te enseña algo cuando lo reintentas. Un nivel imposible te enseña que el juego miente.
Por qué una semilla en bruto no puede publicarse
La semilla determina la bandeja: qué pilas llegan y en qué orden. Nada en una semilla sabe si el nivel resultante puede limpiarse dentro de su tablero, sus celdas bloqueadas y su objetivo. Tener solución es una propiedad de la configuración entera, y la única forma fiable de averiguarlo es jugando.
Así que el catálogo pasó a ser algo generado, no algo muestreado.
El flujo
generate ejecuta un bucle por cada etapa de la curva:
GENERATE → SOLVE → ANALYZE → SCORE → FILTER → KEEP
Se producen semillas candidatas, las juega un solucionador de haz, las mide el analizador, se puntúan y luego se filtran. Las doce mejores de cada etapa se conservan y se escriben en LevelCatalog.cs, que es código generado y nunca se edita a mano.
Cuatro reglas de rechazo hacen el trabajo:
- imposible — ninguna línea de búsqueda lo gana;
- trivial — fuera del tutorial, un nivel que gana cualquier bot tonto no es un puzle;
- dominado — una estrategia gana desde una esquina, así que la posición deja de importar;
- margen cero — existe exactamente una línea ganadora.
Los niveles de tutorial tienen su propio filtro: como mucho 14 movimientos y al menos una cascada de tres oleadas. El primer minuto tiene que demostrar la mecánica, no ponerla a prueba.
Qué rechazó
En la curva publicada, se validaron 96 semillas. Seis niveles imposibles y cincuenta y dos triviales se tiraron durante la generación.
Más de la mitad de lo que produce una semilla aleatoria no es un puzle. Esa proporción por sí sola justifica el flujo.
Tener solución no bastaba
Que un nivel se pudiera ganar resultó ser una garantía más débil de lo que suena. Alguien que jugaba la versión publicada se atascó en el nivel 13 y dijo que era imposible. No lo era, pero por poco:
| Línea óptima del nivel 13 | 11 movimientos |
| Presupuesto de movimientos | 11 movimientos |
| Margen | cero |
Existía exactamente una secuencia ganadora. Un movimiento desperdiciado terminaba el nivel. Y no era solo el 13: el 7, el 10, el 13, el 14 y el 15 tenían margen cero.
La causa era una premisa falsa en la calibración del presupuesto. Lo ensanchaba solo hasta que un buscador deliberadamente débil también ganara. Cuando ese buscador acertaba la línea óptima a la primera, el presupuesto no se ensanchaba nada. "Al buscador débil le resultó fácil" no es "a un humano le resultará fácil": un buscador ve el tablero entero de golpe y no está aprendiendo las reglas al mismo tiempo. El jugador hace las dos cosas.
La corrección fue un suelo: MinimumSlack = 2. Cada nivel concede al menos dos movimientos por encima de la solución más corta, por muy fácil que le resultara a un buscador.
Qué costó
Yo esperaba pagarlo en profundidad. Una medición anterior decía que un presupuesto de óptimo+2 lleva la anchura de decisión al 97%, es decir, casi ningún movimiento pierde. No costó eso:
| Antes | Después | |
|---|---|---|
| Niveles que exigen la línea perfecta | 5 | 0 |
| Niveles triviales (7-20) | 0 | 0 |
| Niveles que derrotan al bot voraz | 5 | 5 |
El motivo es que el generador no solo aflojó el techo: volvió a elegir las semillas bajo el techo nuevo, quedándose con las que siguen siendo exigentes con más aire. Aflojar el presupuesto por sí solo habría costado profundidad. Hacer las dos cosas juntas no costó nada.
Qué lo sujeta en su sitio
Una prueba, CatalogCoversEveryStage, falla si alguna etapa de la curva se queda sin semillas validadas. El flujo no puede dejar de producir niveles para una etapa en silencio y dejar el hueco para que lo encuentre un jugador.
La garantía que le llega al jugador es estrecha y merece enunciarse con precisión: ningún nivel publicado es imposible, y ninguno exige una única secuencia perfecta. Todavía puedes perder llenando el tablero. Esa parte es tuya.