⎇ c02fd3a4d4a9b6e3a51ddd2f59112f3584e6ea12
nonet/src/main/java/com/rizoma/nonet/NonetViewModel.kt
Historia
crudo
Nonet: 3 variantes de pista externa, dificultad real y menú en páginas
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}