From 76f95ed72e3c82eea461f275ae12110838cf1e3c Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 18 Jun 2026 16:44:45 +0000 Subject: [PATCH] =?UTF-8?q?Cubo:=20persistir=20la=20partida=20en=20curso?= =?UTF-8?q?=20=E2=80=94=20reanuda=20el=20tablero=20al=20reabrir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El servicio ya no hace reset() ciego: restaura el tablero guardado (celdas + score + won) si encaja con el tamaño del cubo, guarda tras cada jugada y borra al perder (el récord ya queda sellado aparte). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/com/rizoma/cubo/CuboViewModel.kt | 26 ++++++++-- .../java/com/rizoma/cubo/data/Progreso.kt | 52 ++++++++++++++++++- .../main/java/com/rizoma/cubo/game/Cubo.kt | 12 +++++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/cubo/src/main/java/com/rizoma/cubo/CuboViewModel.kt b/cubo/src/main/java/com/rizoma/cubo/CuboViewModel.kt index 383e942..388dd81 100644 --- a/cubo/src/main/java/com/rizoma/cubo/CuboViewModel.kt +++ b/cubo/src/main/java/com/rizoma/cubo/CuboViewModel.kt @@ -35,8 +35,18 @@ class CuboViewModel(app: Application) : AndroidViewModel(app) { init { viewModelScope.launch { progreso.mejorScore.collect { mejorScore = it } } viewModelScope.launch { progreso.mejorBaldosa.collect { mejorTile = it } } - juego.reset() - sync() + // Reanuda la partida a medias si la hay (y encaja con el tamaño del + // cubo); si no, empieza una nueva. El tablero arranca en blanco y esta + // carga es asíncrona, así que la UI verá el tablero apenas resuelva. + viewModelScope.launch { + val saved = progreso.cargarPartida() + if (saved != null && saved.cells.size == juego.cells.size) { + juego.restore(saved.cells, saved.score, saved.won) + } else { + juego.reset() + } + sync() + } } /** Instantánea barata del tablero para el renderer (copia defensiva). */ @@ -61,8 +71,16 @@ class CuboViewModel(app: Application) : AndroidViewModel(app) { won = juego.won over = juego.over version++ - if (over || won) { - viewModelScope.launch { progreso.registrar(juego.score, juego.maxValue) } + // Captura el estado para persistir fuera del hilo (la partida es mutable). + val cells = juego.cells.copyOf() + val s = juego.score + val mt = juego.maxValue + val w = juego.won + val finished = juego.over + viewModelScope.launch { + if (finished || w) progreso.registrar(s, mt) + // Guarda mientras se juega; al terminar, borra (nada que reanudar). + if (finished) progreso.limpiarPartida() else progreso.guardarPartida(cells, s, w) } } } diff --git a/cubo/src/main/java/com/rizoma/cubo/data/Progreso.kt b/cubo/src/main/java/com/rizoma/cubo/data/Progreso.kt index 738e9a8..ae470ba 100644 --- a/cubo/src/main/java/com/rizoma/cubo/data/Progreso.kt +++ b/cubo/src/main/java/com/rizoma/cubo/data/Progreso.kt @@ -3,15 +3,34 @@ package com.rizoma.cubo.data import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map private val Context.dataStore: DataStore by preferencesDataStore(name = "cubo") -/** Récords del cubo, persistidos en DataStore: mejor puntuación y mejor baldosa. */ +/** Una partida a medias: el tablero tal cual, con su puntuación y si ya ganó. */ +data class PartidaCubo(val cells: IntArray, val score: Int, val won: Boolean) { + // data class con IntArray: equals/hashCode a mano para comparar por contenido. + override fun equals(other: Any?): Boolean = + other is PartidaCubo && score == other.score && won == other.won && + cells.contentEquals(other.cells) + + override fun hashCode(): Int = + (cells.contentHashCode() * 31 + score) * 31 + won.hashCode() +} + +/** + * Persistencia del cubo en DataStore. Dos cosas: los **récords** (mejor + * puntuación y mejor baldosa, sobreviven a cualquier partida) y la **partida + * en curso** (el tablero a medias, para reanudar al reabrir la app). La + * partida se borra al terminar (game over): ya no hay nada que reanudar. + */ class Progreso(context: Context) { private val store = context.applicationContext.dataStore @@ -25,8 +44,39 @@ class Progreso(context: Context) { } } + /** Lee la partida a medias guardada, o null si no hay ninguna. */ + suspend fun cargarPartida(): PartidaCubo? { + val p = store.data.first() + val csv = p[KEY_CELLS] ?: return null + val cells = csv.split(',').mapNotNull { it.toIntOrNull() }.toIntArray() + if (cells.isEmpty()) return null + return PartidaCubo(cells, p[KEY_PSCORE] ?: 0, p[KEY_PWON] ?: false) + } + + /** Guarda el tablero en curso (se llama tras cada jugada). */ + suspend fun guardarPartida(cells: IntArray, score: Int, won: Boolean) { + store.edit { p -> + p[KEY_CELLS] = cells.joinToString(",") + p[KEY_PSCORE] = score + p[KEY_PWON] = won + } + } + + /** Borra la partida en curso (game over: ya no hay nada que reanudar). */ + suspend fun limpiarPartida() { + store.edit { p -> + p.remove(KEY_CELLS) + p.remove(KEY_PSCORE) + p.remove(KEY_PWON) + } + } + private companion object { val KEY_SCORE = intPreferencesKey("mejor_score") val KEY_TILE = intPreferencesKey("mejor_baldosa") + // Partida en curso. + val KEY_CELLS = stringPreferencesKey("partida_cells") + val KEY_PSCORE = intPreferencesKey("partida_score") + val KEY_PWON = booleanPreferencesKey("partida_won") } } diff --git a/cubo/src/main/java/com/rizoma/cubo/game/Cubo.kt b/cubo/src/main/java/com/rizoma/cubo/game/Cubo.kt index 54157cd..1899ce7 100644 --- a/cubo/src/main/java/com/rizoma/cubo/game/Cubo.kt +++ b/cubo/src/main/java/com/rizoma/cubo/game/Cubo.kt @@ -34,6 +34,18 @@ class Cubo(val n: Int = 3, seed: Long = 0x43_55_42_4FL /* "CUBO" */) { spawn() } + /** + * Restaura una partida a medias guardada. [saved] debe tener el mismo + * tamaño que el tablero (n³); el llamador lo valida. No reaparece nada: el + * tablero queda exactamente como estaba al guardarse. + */ + fun restore(saved: IntArray, score: Int, won: Boolean) { + saved.copyInto(cells) + this.score = score + this.won = won + lastSpawn = -1 + } + /** * Deja caer todo hacia la cara dada y, si algo se movió, aparece una baldosa * nueva. Devuelve si el tablero cambió (un movimiento nulo no cuenta ni hace