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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c02fd3a4d4
commit
7f7209e445
@@ -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 }
|
||||
}
|
||||
|
||||
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()
|
||||
/** ¿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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user