gioser

sergio/humanoid

HTML 57Markdown 2TOML 10 Definiciones

Nonet: variantes Argyle y Cadenas + arreglo de generación de regiones

Dos variantes nuevas (18 en total): Argyle (clásico + diagonales en rombo que
no repiten 1-9) y Cadenas/Chain (cajas sustituidas por cadenas serpenteantes de
9 celdas, vía camino hamiltoniano "backbite").

Arreglo importante de generación de regiones: el solver ahora lleva presupuesto
de nodos, así una distribución irresoluble se descarta en vez de colgar el
backtracking (antes Chain podía colgar la generación). Y el generador jigsaw
—que se atascaba y degeneraba silenciosamente en cajas 3×3— se rehízo con
Voronoi (semillas en centros de caja con jitter) + rebalanceo conexo: ahora
produce regiones irregulares de verdad y resolubles (~94% irregulares, el resto
cae a cajas como respaldo seguro). Verificado: 18 variantes × 4 dificultades,
todas válidas, únicas y sin cuelgues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

7f7209e4457a345d22fccfe27a68f342ae9df18e · por Sergio · (hace 4 meses) · padre c02fd3a4d · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

4 archivos · +178 −35

diff --git a/nonet/src/main/java/com/rizoma/nonet/game/Motor.kt b/nonet/src/main/java/com/rizoma/nonet/game/Motor.kt
index faed257..0cd11b4 100644
--- a/nonet/src/main/java/com/rizoma/nonet/game/Motor.kt
+++ b/nonet/src/main/java/com/rizoma/nonet/game/Motor.kt
@@ -71,30 +71,58 @@ object Motor {
 
     fun generar(v: Variant, dif: Dificultad, index: Int): Puzzle {
         val rng = Random(seedFor(v, dif, index))
-
-        val regiones = if (v.tipo == Variant.Tipo.JIGSAW) jigsaw(rng) else null
-        val baseGroups = ArrayList<IntArray>(36)
-        for (r in 0 until 9) baseGroups.add(IntArray(9) { r * 9 + it })
-        for (c in 0 until 9) baseGroups.add(IntArray(9) { it * 9 + c })
-        baseGroups.addAll(regiones?.let { regionGroups(it) } ?: boxes())
-        baseGroups.addAll(v.extraGroups())
-
         val solution = IntArray(81)
-        fillFull(solution, soloPeers(baseGroups), rng)
+        var regiones: IntArray? = null
+        var baseGroups: List<IntArray> = emptyList()
+
+        if (v.tipo == Variant.Tipo.JIGSAW || v.tipo == Variant.Tipo.CHAIN) {
+            // No toda distribución de regiones admite solución de Sudoku. Probamos
+            // varias (acotando el solver para descartar rápido las irresolubles);
+            // si ninguna sirve, caemos a cajas 3×3 (siempre resoluble).
+            var ok = false
+            for (intento in 0 until 120) {
+                val cand = if (v.tipo == Variant.Tipo.JIGSAW) jigsaw(rng) else chains(rng)
+                val grupos = gruposBase(cand, v)
+                solution.fill(0)
+                // Una distribución resoluble se llena en pocos nodos; una
+                // irresoluble agota el presupuesto y se descarta sin colgarse.
+                if (fillFull(solution, soloPeers(grupos), rng, intArrayOf(80_000))) {
+                    regiones = cand; baseGroups = grupos; ok = true; break
+                }
+            }
+            if (!ok) {
+                regiones = IntArray(81) { (it / 9 / 3) * 3 + (it % 9 / 3) }
+                baseGroups = gruposBase(regiones, v)
+                solution.fill(0)
+                fillFull(solution, soloPeers(baseGroups), rng, intArrayOf(Int.MAX_VALUE))
+            }
+        } else {
+            baseGroups = gruposBase(null, v)
+            fillFull(solution, soloPeers(baseGroups), rng, intArrayOf(Int.MAX_VALUE))
+        }
 
         val construido = construir(v, solution, baseGroups, rng)
-
         val givens = solution.copyOf()
         dig(givens, construido.reglas, dif.pistasObjetivo, dif.soloLogica, rng)
 
         return Puzzle(v, dif, index, givens, solution, construido.reglas, regiones, construido.aristas, construido.borde)
     }
 
+    private fun gruposBase(regiones: IntArray?, v: Variant): List<IntArray> {
+        val g = ArrayList<IntArray>(36)
+        for (r in 0 until 9) g.add(IntArray(9) { r * 9 + it })
+        for (c in 0 until 9) g.add(IntArray(9) { it * 9 + c })
+        g.addAll(regiones?.let { regionGroups(it) } ?: boxes())
+        g.addAll(v.extraGroups())
+        return g
+    }
+
     fun contarSoluciones(grid: IntArray, reglas: Reglas, limite: Int): Int =
         count(grid.copyOf(), reglas, limite)
 
-    /** Una distribución jigsaw fija (para ilustrar la variante en el menú). */
+    /** Distribuciones fijas (para ilustrar las variantes de región en el menú). */
     fun jigsawEjemplo(): IntArray = jigsaw(Random(7))
+    fun chainEjemplo(): IntArray = chains(Random(11))
 
     // ---- Construcción de reglas por variante --------------------------
 
@@ -284,14 +312,17 @@ object Motor {
         return true
     }
 
-    private fun fillFull(grid: IntArray, reglas: Reglas, rng: Random): Boolean {
+    /** Rellena una solución; [budget] acota los nodos para no atascarse en distribuciones irresolubles. */
+    private fun fillFull(grid: IntArray, reglas: Reglas, rng: Random, budget: IntArray): Boolean {
+        if (budget[0] <= 0) return false
+        budget[0]--
         val (idx, mask) = mrv(grid, reglas)
         if (idx == RESUELTO) return true
         if (idx == SIN_SALIDA) return false
         val digits = digitsOf(mask).also { shuffle(it, rng) }
         for (d in digits) {
             grid[idx] = d
-            if (restriccionesOk(grid, idx, reglas) && fillFull(grid, reglas, rng)) return true
+            if (restriccionesOk(grid, idx, reglas) && fillFull(grid, reglas, rng, budget)) return true
             grid[idx] = 0
         }
         return false
@@ -374,33 +405,102 @@ object Motor {
     private fun regionGroups(reg: IntArray): List<IntArray> =
         (0 until 9).map { id -> (0 until 81).filter { reg[it] == id }.toIntArray() }
 
+    /**
+     * 9 regiones irregulares de 9 celdas. Voronoi por BFS multi-fuente desde 9
+     * semillas (da 9 regiones conexas de tamaño desigual) y luego rebalanceo:
+     * se mueven celdas de frontera de una región grande a una pequeña vecina,
+     * manteniendo la conexión, hasta dejar las nueve con 9 celdas.
+     */
+    // Centros de las 9 cajas: semillas base del Voronoi (con jitter → regiones
+    // compactas pero irregulares, que son mucho más resolubles que las caóticas).
+    private val CENTROS = intArrayOf(10, 13, 16, 37, 40, 43, 64, 67, 70)
+
     private fun jigsaw(rng: Random): IntArray {
-        repeat(300) {
-            val reg = IntArray(81) { -1 }; val sizes = IntArray(9)
-            val seeds = (0 until 81).toMutableList().also { shuffle2(it, rng) }.take(9)
-            seeds.forEachIndexed { id, cell -> reg[cell] = id; sizes[id] = 1 }
-            var faltan = 81 - 9; var atascado = false
-            while (faltan > 0 && !atascado) {
-                var elegida = -1; var menor = 10
-                for (id in 0 until 9) if (sizes[id] in 1 until 9 && sizes[id] < menor && tieneVecinoLibre(reg, id)) { menor = sizes[id]; elegida = id }
-                if (elegida == -1) { atascado = true; break }
-                val libres = vecinosLibres(reg, elegida)
-                reg[libres[rng.nextInt(libres.size)]] = elegida; sizes[elegida]++; faltan--
+        repeat(50) {
+            val reg = IntArray(81) { -1 }
+            val usados = HashSet<Int>(); val seeds = ArrayList<Int>(9)
+            for (cen in CENTROS) {
+                var pick = -1
+                for (t in 0 until 10) {
+                    val r = ((cen / 9) + rng.nextInt(3) - 1).coerceIn(0, 8)
+                    val c = ((cen % 9) + rng.nextInt(3) - 1).coerceIn(0, 8)
+                    val cand = r * 9 + c
+                    if (cand !in usados) { pick = cand; break }
+                }
+                if (pick == -1) pick = cen
+                usados.add(pick); seeds.add(pick)
             }
-            if (!atascado && sizes.all { it == 9 }) return reg
+            val q = ArrayDeque<Int>()
+            seeds.forEachIndexed { id, c -> reg[c] = id; q.addLast(c) }
+            while (q.isNotEmpty()) {
+                val x = q.removeFirst()
+                for (n in ortog(x).toMutableList().also { shuffle2(it, rng) }) if (reg[n] == -1) { reg[n] = reg[x]; q.addLast(n) }
+            }
+            val sizes = IntArray(9); for (i in 0 until 81) sizes[reg[i]]++
+            if (rebalancear(reg, sizes, rng)) return reg
         }
         return IntArray(81) { (it / 9 / 3) * 3 + (it % 9 / 3) }
     }
 
-    private fun tieneVecinoLibre(reg: IntArray, id: Int): Boolean {
-        for (i in 0 until 81) if (reg[i] == id) for (n in ortog(i)) if (reg[n] == -1) return true
-        return false
+    /** Iguala las regiones a 9 celdas moviendo celdas de frontera (grande→pequeña vecina). */
+    private fun rebalancear(reg: IntArray, sizes: IntArray, rng: Random): Boolean {
+        repeat(8000) {
+            if (sizes.all { it == 9 }) return true
+            val grandes = (0 until 9).filter { sizes[it] > 9 }
+            if (grandes.isEmpty()) return false
+            val a = grandes[rng.nextInt(grandes.size)]
+            val borde = ArrayList<Pair<Int, Int>>()
+            for (i in 0 until 81) if (reg[i] == a) for (n in ortog(i)) { val t = reg[n]; if (t != a && sizes[t] < 9) borde.add(i to t) }
+            if (borde.isEmpty()) return@repeat
+            val (cell, t) = borde[rng.nextInt(borde.size)]
+            reg[cell] = t
+            if (conexa(reg, a)) { sizes[a]--; sizes[t]++ } else reg[cell] = a
+        }
+        return sizes.all { it == 9 }
+    }
+
+    /** ¿Todas las celdas de la región [id] están conectadas ortogonalmente? */
+    private fun conexa(reg: IntArray, id: Int): Boolean {
+        var start = -1; var total = 0
+        for (i in 0 until 81) if (reg[i] == id) { total++; if (start == -1) start = i }
+        if (start == -1) return true
+        val vis = BooleanArray(81); val pila = ArrayDeque<Int>()
+        pila.addLast(start); vis[start] = true; var seen = 0
+        while (pila.isNotEmpty()) {
+            val x = pila.removeLast(); seen++
+            for (n in ortog(x)) if (!vis[n] && reg[n] == id) { vis[n] = true; pila.addLast(n) }
+        }
+        return seen == total
     }
 
-    private fun vecinosLibres(reg: IntArray, id: Int): IntArray {
-        val s = HashSet<Int>()
-        for (i in 0 until 81) if (reg[i] == id) for (n in ortog(i)) if (reg[n] == -1) s.add(n)
-        return s.toIntArray()
+    /**
+     * 9 cadenas serpenteantes de 9 celdas. Se parte de un camino hamiltoniano en
+     * serpentina y se aleatoriza con movimientos "backbite" (que lo mantienen
+     * hamiltoniano); luego se corta en 9 segmentos de 9 → cada uno es una cadena
+     * conexa. Siempre tiene éxito (a diferencia de hacer crecer 9 serpientes a la vez).
+     */
+    private fun chains(rng: Random): IntArray {
+        val path = IntArray(81); var idx = 0
+        for (r in 0 until 9) { val cols = if (r % 2 == 0) 0..8 else 8 downTo 0; for (c in cols) path[idx++] = r * 9 + c }
+        val posOf = IntArray(81); for (i in 0 until 81) posOf[path[i]] = i
+        repeat(4000) {
+            val cabeza = rng.nextBoolean()
+            val end = if (cabeza) path[0] else path[80]
+            for (u in ortog(end).toMutableList().also { shuffle2(it, rng) }) {
+                val k = posOf[u]
+                if (cabeza && k >= 2) { reverseTramo(path, 0, k - 1, posOf); break }
+                if (!cabeza && k <= 78) { reverseTramo(path, k + 1, 80, posOf); break }
+            }
+        }
+        return IntArray(81) { posOf[it] / 9 }
+    }
+
+    private fun reverseTramo(path: IntArray, lo: Int, hi: Int, posOf: IntArray) {
+        var i = lo; var j = hi
+        while (i < j) {
+            val t = path[i]; path[i] = path[j]; path[j] = t
+            posOf[path[i]] = i; posOf[path[j]] = j; i++; j--
+        }
     }
 
     private fun jaulas(sol: IntArray, rng: Random): List<Cage> {
diff --git a/nonet/src/main/java/com/rizoma/nonet/model/Variant.kt b/nonet/src/main/java/com/rizoma/nonet/model/Variant.kt
index 286049f..023775e 100644
--- a/nonet/src/main/java/com/rizoma/nonet/model/Variant.kt
+++ b/nonet/src/main/java/com/rizoma/nonet/model/Variant.kt
@@ -95,13 +95,23 @@ enum class Variant(
         "rossini", "Rossini",
         "Una flecha en el borde indica que las tres casillas más cercanas van en orden creciente hacia donde apunta.",
         Tipo.ROSSINI, Realce.BORDE,
+    ),
+    ARGYLE(
+        "argyle", "Argyle",
+        "Además del clásico, varias diagonales en patrón de rombo (argyle) llevan 1-9 sin repetir.",
+        Tipo.REGION, Realce.ARGYLE,
+    ),
+    CHAIN(
+        "chain", "Cadenas",
+        "Las cajas se sustituyen por cadenas serpenteantes de 9 celdas, cada una con 1-9.",
+        Tipo.CHAIN, Realce.REGIONES,
     );
 
     /** Familia de la variante: determina qué restricciones añade el motor. */
-    enum class Tipo { REGION, JIGSAW, PARIDAD, CONSEC, XV, MAYOR, KILLER, SKY, LITTLE, ROSSINI }
+    enum class Tipo { REGION, JIGSAW, CHAIN, PARIDAD, CONSEC, XV, MAYOR, KILLER, SKY, LITTLE, ROSSINI }
 
     /** Cómo se dibuja la restricción extra en el tablero. */
-    enum class Realce { NINGUNO, DIAGONALES, CELDAS, POSICION, REGIONES, PARIDAD, ARISTAS, JAULAS, BORDE }
+    enum class Realce { NINGUNO, DIAGONALES, CELDAS, POSICION, REGIONES, PARIDAD, ARISTAS, JAULAS, BORDE, ARGYLE }
 
     /**
      * Grupos extra (cada uno, 9 índices) de las variantes de "región extra".
@@ -120,9 +130,28 @@ enum class Variant(
         PUNTO_CENTRAL -> listOf(IntArray(9) { b -> (((b / 3) * 3 + 1) * 9) + ((b % 3) * 3 + 1) })
         ASTERISCO -> listOf(cells(ASTERISK_RC))
         GIRANDOLA -> listOf(cells(GIRANDOLA_RC))
+        ARGYLE -> argyleDiagonals()
         else -> emptyList()
     }
 
+    /**
+     * Las diagonales del patrón argyle, ordenadas a lo largo de cada línea: las
+     * dos diagonales principales (la X completa) más cuatro cortas que forman el
+     * rombo central. Cada una es un grupo de no-repetición (longitud 5 ó 9).
+     */
+    fun argyleDiagonals(): List<IntArray> {
+        val out = ArrayList<IntArray>(6)
+        for (d in intArrayOf(0, 4, -4)) {   // ↘: c = r - d
+            val cs = (0..8).filter { it - d in 0..8 }.map { it * 9 + (it - d) }
+            out.add(cs.toIntArray())
+        }
+        for (s in intArrayOf(8, 4, 12)) {    // ↗: c = s - r
+            val cs = (0..8).filter { s - it in 0..8 }.map { it * 9 + (s - it) }
+            out.add(cs.toIntArray())
+        }
+        return out
+    }
+
     /** Celdas resaltadas (unión de grupos extra) para las variantes de tipo CELDAS. */
     fun highlightCells(): Set<Int> =
         if (realce == Realce.CELDAS) extraGroups().flatMap { it.asList() }.toSet() else emptySet()
diff --git a/nonet/src/main/java/com/rizoma/nonet/ui/BoardScreen.kt b/nonet/src/main/java/com/rizoma/nonet/ui/BoardScreen.kt
index 5215f05..5bc6ed5 100644
--- a/nonet/src/main/java/com/rizoma/nonet/ui/BoardScreen.kt
+++ b/nonet/src/main/java/com/rizoma/nonet/ui/BoardScreen.kt
@@ -159,6 +159,10 @@ private fun Tablero(vm: NonetViewModel) {
                 drawLine(NeonRosa.copy(alpha = 0.30f), Offset(0f, 0f), Offset(size.width, size.height), 3f)
                 drawLine(NeonRosa.copy(alpha = 0.30f), Offset(size.width, 0f), Offset(0f, size.height), 3f)
             }
+            if (variante == Variant.ARGYLE) for (g in Variant.ARGYLE.argyleDiagonals()) {
+                val a = g.first(); val z = g.last()
+                drawLine(NeonRosa.copy(alpha = 0.30f), Offset(cx(a % 9), cy(a / 9)), Offset(cx(z % 9), cy(z / 9)), 3f)
+            }
 
             // Líneas finas de todas las celdas.
             for (k in 0..9) {
diff --git a/nonet/src/main/java/com/rizoma/nonet/ui/MenuScreen.kt b/nonet/src/main/java/com/rizoma/nonet/ui/MenuScreen.kt
index 165bb91..2ea7f97 100644
--- a/nonet/src/main/java/com/rizoma/nonet/ui/MenuScreen.kt
+++ b/nonet/src/main/java/com/rizoma/nonet/ui/MenuScreen.kt
@@ -155,7 +155,13 @@ private fun FormaVariante(v: Variant, modifier: Modifier) {
     val paint = remember {
         Paint().apply { isAntiAlias = true; typeface = Typeface.DEFAULT_BOLD; textAlign = Paint.Align.CENTER }
     }
-    val regionesEj = remember(v) { if (v.tipo == Variant.Tipo.JIGSAW) Motor.jigsawEjemplo() else null }
+    val regionesEj = remember(v) {
+        when (v.tipo) {
+            Variant.Tipo.JIGSAW -> Motor.jigsawEjemplo()
+            Variant.Tipo.CHAIN -> Motor.chainEjemplo()
+            else -> null
+        }
+    }
     Box(modifier.clip(RoundedCornerShape(10.dp)).background(SuperficieAlta).padding(10.dp)) {
         Canvas(Modifier.fillMaxSize()) {
             val cell = size.width / 9f
@@ -235,6 +241,10 @@ private fun FormaVariante(v: Variant, modifier: Modifier) {
                     paint.textSize = cell * 0.7f; paint.color = NeonVerde.toArgb()
                     texto(this, paint, "↓", cx(3), cy(0)); texto(this, paint, "→", cx(0), cy(5))
                 }
+                Variant.ARGYLE -> for (g in v.argyleDiagonals()) {
+                    val a = g.first(); val z = g.last()
+                    drawLine(NeonRosa, Offset(cx(a % 9), cy(a / 9)), Offset(cx(z % 9), cy(z / 9)), 2.5f)
+                }
                 else -> {}
             }
         }