Cubo: persistir la partida en curso — reanuda el tablero al reabrir

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) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-06-18 16:44:45 +00:00
co-authored by Claude Opus 4.8
parent 5c80ec5d46
commit 76f95ed72e
3 changed files with 85 additions and 5 deletions
@@ -35,8 +35,18 @@ class CuboViewModel(app: Application) : AndroidViewModel(app) {
init { init {
viewModelScope.launch { progreso.mejorScore.collect { mejorScore = it } } viewModelScope.launch { progreso.mejorScore.collect { mejorScore = it } }
viewModelScope.launch { progreso.mejorBaldosa.collect { mejorTile = it } } viewModelScope.launch { progreso.mejorBaldosa.collect { mejorTile = it } }
juego.reset() // Reanuda la partida a medias si la hay (y encaja con el tamaño del
sync() // 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). */ /** Instantánea barata del tablero para el renderer (copia defensiva). */
@@ -61,8 +71,16 @@ class CuboViewModel(app: Application) : AndroidViewModel(app) {
won = juego.won won = juego.won
over = juego.over over = juego.over
version++ version++
if (over || won) { // Captura el estado para persistir fuera del hilo (la partida es mutable).
viewModelScope.launch { progreso.registrar(juego.score, juego.maxValue) } 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)
} }
} }
} }
@@ -3,15 +3,34 @@ package com.rizoma.cubo.data
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "cubo") private val Context.dataStore: DataStore<Preferences> 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) { class Progreso(context: Context) {
private val store = context.applicationContext.dataStore 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 { private companion object {
val KEY_SCORE = intPreferencesKey("mejor_score") val KEY_SCORE = intPreferencesKey("mejor_score")
val KEY_TILE = intPreferencesKey("mejor_baldosa") 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")
} }
} }
@@ -34,6 +34,18 @@ class Cubo(val n: Int = 3, seed: Long = 0x43_55_42_4FL /* "CUBO" */) {
spawn() 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 * 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 * nueva. Devuelve si el tablero cambió (un movimiento nulo no cuenta ni hace