tawasuyu

sergio/humanoid

HTML 50Markdown 1TOML 10 Sutichaykuna

⎇ c02fd3a4d4a9b6e3a51ddd2f59112f3584e6ea12 nonet/src/main/java/com/rizoma/nonet/NonetViewModel.kt 254 siq'ikuna · 9.5 KB Ñawpa willay ch'uya

Nonet: 3 variantes de pista externa, dificultad real y menú en páginas c02fd3a4d · Sergio ·

1package com.rizoma.nonet
2
3import android.app.Application
4import androidx.compose.runtime.getValue
5import androidx.compose.runtime.mutableIntStateOf
6import androidx.compose.runtime.mutableStateOf
7import androidx.compose.runtime.setValue
8import androidx.compose.runtime.snapshots.SnapshotStateList
9import androidx.compose.runtime.toMutableStateList
10import androidx.lifecycle.AndroidViewModel
11import androidx.lifecycle.viewModelScope
12import com.rizoma.nonet.data.Progreso
13import com.rizoma.nonet.game.Motor
14import com.rizoma.nonet.game.Puzzle
15import com.rizoma.nonet.model.Dificultad
16import com.rizoma.nonet.model.Variant
17import kotlinx.coroutines.Dispatchers
18import kotlinx.coroutines.launch
19import kotlinx.coroutines.withContext
20
21enum class Pantalla { Menu, Tablero }
22
23/**
24 * Orquesta el Sudoku: navegación del menú (variante + dificultad + lista de
25 * puzzles generados), generación determinista en segundo plano y la partida en
26 * curso, que se guarda en cada jugada para reanudarla al volver.
27 */
28class NonetViewModel(app: Application) : AndroidViewModel(app) {
29
30    private val progreso = Progreso(app)
31
32    var pantalla by mutableStateOf(Pantalla.Menu); private set
33
34    // Selección del menú.
35    var variante by mutableStateOf(Variant.CLASICO); private set
36    var dificultad by mutableStateOf(Dificultad.FACIL); private set
37
38    // Logros/avance, observados para refrescar el menú al instante.
39    var generados by mutableStateOf<Map<String, Int>>(emptyMap()); private set
40    var completados by mutableStateOf<Set<String>>(emptySet()); private set
41    var enCurso by mutableStateOf<Set<String>>(emptySet()); private set
42
43    // Partida en curso.
44    var puzzle by mutableStateOf<Puzzle?>(null); private set
45    var generando by mutableStateOf(false); private set
46    var completado by mutableStateOf(false); private set
47
48    // Dinámica "elige número → toca celdas": [activo] es el dígito 1..9 que se
49    // marca al tocar, o [BORRAR]. [celdaFoco] es la última tocada (resalte/pista).
50    var activo by mutableIntStateOf(1); private set
51    var celdaFoco by mutableIntStateOf(-1); private set
52    // "edit"/borrador: OFF marca dígitos duros; ON marca notas a lápiz (posibles).
53    var modoEdicion by mutableStateOf(false); private set
54    // Celdas marcadas en rojo por "comprobar" (valor distinto al de la solución).
55    var errores by mutableStateOf<Set<Int>>(emptySet()); private set
56
57    // Historial para deshacer/rehacer (instantáneas de rejilla + notas).
58    private val historial = ArrayDeque<Pair<IntArray, IntArray>>()
59    private val rehacer = ArrayDeque<Pair<IntArray, IntArray>>()
60    var puedeDeshacer by mutableStateOf(false); private set
61    var puedeRehacer by mutableStateOf(false); private set
62
63    /** Rejilla actual (81; 0 = vacía) y notas a lápiz (81 máscaras de bits 1..9). */
64    var entradas: SnapshotStateList<Int> = IntArray(81).toList().toMutableStateList(); private set
65    var notas: SnapshotStateList<Int> = IntArray(81).toList().toMutableStateList(); private set
66
67    init {
68        viewModelScope.launch { progreso.generados.collect { generados = it } }
69        viewModelScope.launch { progreso.completados.collect { completados = it } }
70        viewModelScope.launch { progreso.enCurso.collect { enCurso = it } }
71    }
72
73    // ---- Menú ---------------------------------------------------------
74
75    fun elegirVariante(v: Variant) { variante = v }
76    fun elegirDificultad(d: Dificultad) { dificultad = d }
77
78    /** Cuántos puzzles existen ya de la (variante, dificultad) seleccionadas. */
79    fun cuenta(v: Variant = variante, d: Dificultad = dificultad): Int =
80        generados["${v.id}:${d.ordinal}"] ?: 1
81
82    fun puzzleId(v: Variant, d: Dificultad, index: Int) = "${v.id}:${d.ordinal}:$index"
83
84    /** Genera el siguiente puzzle de (variante, dificultad) y lo abre. */
85    fun generarNuevo(v: Variant = variante, d: Dificultad = dificultad) {
86        viewModelScope.launch {
87            val index = progreso.generarSiguiente(v.id, d.ordinal)
88            abrir(v, d, index)
89        }
90    }
91
92    /** Abre (genera deterministamente + restaura avance) un puzzle concreto. */
93    fun abrir(v: Variant, d: Dificultad, index: Int) {
94        variante = v
95        dificultad = d
96        pantalla = Pantalla.Tablero
97        generando = true
98        activo = 1
99        celdaFoco = -1
100        completado = false
101        modoEdicion = false
102        errores = emptySet()
103        historial.clear(); rehacer.clear(); sincronizarPilas()
104        viewModelScope.launch {
105            val p = withContext(Dispatchers.Default) { Motor.generar(v, d, index) }
106            val guardadas = progreso.cargarEntradas(p.id)
107            val guardadasNotas = progreso.cargarNotas(p.id)
108            // Pistas fijas siempre; sobre ellas, el avance guardado si lo hay.
109            val grid = p.givens.copyOf()
110            if (guardadas != null) for (i in 0 until 81) if (p.givens[i] == 0) grid[i] = guardadas[i]
111            puzzle = p
112            entradas = grid.toList().toMutableStateList()
113            notas = (guardadasNotas ?: IntArray(81)).toList().toMutableStateList()
114            completado = grid.toList() == p.solution.toList()
115            generando = false
116        }
117    }
118
119    fun volverAlMenu() {
120        pantalla = Pantalla.Menu
121        puzzle = null
122        celdaFoco = -1
123    }
124
125    // ---- Juego --------------------------------------------------------
126
127    fun esFija(i: Int): Boolean = puzzle?.let { it.givens[i] != 0 } ?: true
128
129    /** Elige el dígito activo (1..9) que se marcará al tocar celdas. */
130    fun elegirActivo(d: Int) { activo = d }
131
132    /** Activa la herramienta de borrado: tocar celdas las vacía. */
133    fun activarBorrar() { activo = BORRAR }
134
135    /** Alterna "edit" (borrador): ON marca notas; OFF marca dígitos duros. */
136    fun alternarEdicion() { modoEdicion = !modoEdicion }
137
138    /** Toca una celda: aplica el activo (dígito duro, nota a lápiz o borrado). */
139    fun tocarCelda(i: Int) {
140        val p = puzzle ?: return
141        celdaFoco = i
142        if (completado || esFija(i)) return
143        when {
144            activo == BORRAR -> {
145                if (entradas[i] == 0 && notas[i] == 0) return
146                empujarHistorial()
147                entradas[i] = 0
148                notas[i] = 0
149            }
150            modoEdicion -> {
151                if (entradas[i] != 0) return        // las notas solo en celdas sin dígito duro
152                empujarHistorial()
153                notas[i] = notas[i] xor (1 shl activo)
154            }
155            else -> {
156                empujarHistorial()
157                entradas[i] = if (entradas[i] == activo) 0 else activo
158                notas[i] = 0
159            }
160        }
161        errores = emptySet()                        // cualquier cambio invalida el último "comprobar"
162        comprobarVictoria(p)
163        guardar()
164    }
165
166    fun deshacer() {
167        if (historial.isEmpty()) return
168        rehacer.addLast(estadoActual())
169        aplicar(historial.removeLast())
170        sincronizarPilas()
171        guardar()
172    }
173
174    fun rehacerJugada() {
175        if (rehacer.isEmpty()) return
176        historial.addLast(estadoActual())
177        aplicar(rehacer.removeLast())
178        sincronizarPilas()
179        guardar()
180    }
181
182    /** "Comprobar": marca las celdas cuyo valor no coincide con la solución. */
183    fun comprobar() {
184        val p = puzzle ?: return
185        errores = (0 until 81).filter { entradas[it] != 0 && entradas[it] != p.solution[it] }.toSet()
186    }
187
188    /** "Pista": rellena la celda en foco (o la primera errónea/vacía) con su valor. */
189    fun pista() {
190        val p = puzzle ?: return
191        if (completado) return
192        val i = celdaFoco.takeIf { it in 0..80 && !esFija(it) && entradas[it] != p.solution[it] }
193            ?: (0 until 81).firstOrNull { !esFija(it) && entradas[it] != p.solution[it] }
194            ?: return
195        empujarHistorial()
196        entradas[i] = p.solution[i]
197        notas[i] = 0
198        celdaFoco = i
199        errores = emptySet()
200        comprobarVictoria(p)
201        guardar()
202    }
203
204    /** Celdas en conflicto (mismo dígito que un par de su grupo). Para pintarlas. */
205    fun conflictos(): Set<Int> {
206        val p = puzzle ?: return emptySet()
207        val out = HashSet<Int>()
208        for (i in 0 until 81) {
209            val v = entradas[i]
210            if (v == 0) continue
211            for (q in p.peers[i]) if (entradas[q] == v) { out.add(i); out.add(q) }
212        }
213        return out
214    }
215
216    private fun comprobarVictoria(p: Puzzle) {
217        for (i in 0 until 81) if (entradas[i] != p.solution[i]) return
218        completado = true
219        viewModelScope.launch { progreso.marcarCompletado(p.id) }
220    }
221
222    // ---- Historial (deshacer/rehacer) --------------------------------
223
224    private fun estadoActual(): Pair<IntArray, IntArray> = entradas.toIntArray() to notas.toIntArray()
225
226    private fun empujarHistorial() {
227        historial.addLast(estadoActual())
228        if (historial.size > 200) historial.removeFirst()
229        rehacer.clear()
230        sincronizarPilas()
231    }
232
233    private fun aplicar(estado: Pair<IntArray, IntArray>) {
234        val (e, n) = estado
235        for (i in 0 until 81) { entradas[i] = e[i]; notas[i] = n[i] }
236        completado = puzzle?.let { p -> (0 until 81).all { entradas[it] == p.solution[it] } } ?: false
237    }
238
239    private fun sincronizarPilas() {
240        puedeDeshacer = historial.isNotEmpty()
241        puedeRehacer = rehacer.isNotEmpty()
242    }
243
244    private fun guardar() {
245        val p = puzzle ?: return
246        val e = entradas.toIntArray()
247        val n = notas.toIntArray()
248        viewModelScope.launch { progreso.guardar(p.id, e, n) }
249    }
250
251    companion object {
252        const val BORRAR = -1
253    }
254}