Auto-commit after successful build: update app gameplay/UI, improve build setup

Changed files:\napp/build.gradle.kts
app/src/main/java/solutions/tretter/githugandroid/AppLog.kt
app/src/main/java/solutions/tretter/githugandroid/GameProgressStore.kt
app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt
app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt
This commit is contained in:
Joe Tretter
2026-04-24 13:52:19 -05:00
parent 66b664d9d5
commit eee8827f76
5 changed files with 86 additions and 12 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId = "solutions.tretter.githugandroid" applicationId = "solutions.tretter.githugandroid"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 71 versionCode = 72
versionName = "0.1.70" versionName = "0.1.71"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@@ -0,0 +1,15 @@
package solutions.tretter.githugandroid
import android.util.Log
object AppLog {
private const val TAG = "GitHugAndroid"
fun d(area: String, message: String) {
Log.d(TAG, "[$area] $message")
}
fun e(area: String, message: String, error: Throwable? = null) {
Log.e(TAG, "[$area] $message", error)
}
}

View File

@@ -17,23 +17,52 @@ class GameProgressStore(private val context: Context) {
) )
private val completedLevelsKey = stringPreferencesKey("completed_levels") private val completedLevelsKey = stringPreferencesKey("completed_levels")
private val activeLevelIdKey = stringPreferencesKey("active_level_id")
val completedLevelsFlow: Flow<Set<String>> = dataStore.data data class GameProgress(
val completedLevels: Set<String> = emptySet(),
val activeLevelId: String? = null,
)
val progressFlow: Flow<GameProgress> = dataStore.data
.catch { error -> .catch { error ->
if (error is IOException) emit(emptyPreferences()) else throw error if (error is IOException) {
AppLog.e("ProgressStore", "Failed reading progress preferences, using defaults", error)
emit(emptyPreferences())
} else {
throw error
}
} }
.map { preferences -> .map { preferences ->
preferences[completedLevelsKey] val completedLevels = preferences[completedLevelsKey]
?.split(',') ?.split(',')
?.map { it.trim() } ?.map { it.trim() }
?.filter { it.isNotEmpty() } ?.filter { it.isNotEmpty() }
?.toSet() ?.toSet()
?: emptySet() ?: emptySet()
val activeLevelId = preferences[activeLevelIdKey]?.takeIf { it.isNotBlank() }
AppLog.d(
"ProgressStore",
"Loaded progress completed=${completedLevels.sorted()} activeLevelId=$activeLevelId",
)
GameProgress(
completedLevels = completedLevels,
activeLevelId = activeLevelId,
)
} }
suspend fun saveCompletedLevels(completedLevels: Set<String>) { suspend fun saveProgress(completedLevels: Set<String>, activeLevelId: String?) {
dataStore.edit { preferences -> dataStore.edit { preferences ->
preferences[completedLevelsKey] = completedLevels.joinToString(",") preferences[completedLevelsKey] = completedLevels.joinToString(",")
if (activeLevelId.isNullOrBlank()) {
preferences.remove(activeLevelIdKey)
} else {
preferences[activeLevelIdKey] = activeLevelId
} }
} }
AppLog.d(
"ProgressStore",
"Saved progress completed=${completedLevels.sorted()} activeLevelId=$activeLevelId",
)
}
} }

View File

@@ -40,7 +40,7 @@ fun GitHugApp() {
val paneLayoutStore = remember(context) { PaneLayoutStore(context.applicationContext) } val paneLayoutStore = remember(context) { PaneLayoutStore(context.applicationContext) }
val gameProgressStore = remember(context) { GameProgressStore(context.applicationContext) } val gameProgressStore = remember(context) { GameProgressStore(context.applicationContext) }
val persistedPaneLayout by paneLayoutStore.layoutFlow.collectAsState(initial = defaultPaneLayout()) val persistedPaneLayout by paneLayoutStore.layoutFlow.collectAsState(initial = defaultPaneLayout())
val persistedCompletedLevels by gameProgressStore.completedLevelsFlow.collectAsState(initial = null) val persistedProgress by gameProgressStore.progressFlow.collectAsState(initial = null)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val screenScrollState = rememberScrollState() val screenScrollState = rememberScrollState()
val screenHeightDp = configuration.screenHeightDp val screenHeightDp = configuration.screenHeightDp
@@ -81,13 +81,28 @@ fun GitHugApp() {
} }
} }
LaunchedEffect(persistedCompletedLevels) { suspend fun persistProgress(completed: Set<String>, activeLevelId: String?) {
val restoredCompletedLevels = persistedCompletedLevels ?: return@LaunchedEffect gameProgressStore.saveProgress(completed, activeLevelId)
}
LaunchedEffect(persistedProgress) {
val restoredProgress = persistedProgress ?: return@LaunchedEffect
val restoredCompletedLevels = restoredProgress.completedLevels
completedLevels = restoredCompletedLevels completedLevels = restoredCompletedLevels
AppLog.d(
"GitHugApp",
"Observed persisted progress completed=${restoredCompletedLevels.sorted()} activeLevelId=${restoredProgress.activeLevelId} restored=$hasRestoredProgress",
)
if (!hasRestoredProgress) { if (!hasRestoredProgress) {
hasRestoredProgress = true hasRestoredProgress = true
val firstIncompleteIndex = levels.indexOfFirst { it.id !in restoredCompletedLevels } val resumeIndex = restoredProgress.activeLevelId
val resumeIndex = firstIncompleteIndex.takeIf { it >= 0 } ?: levels.lastIndex ?.let { activeId -> levels.indexOfFirst { it.id == activeId }.takeIf { it >= 0 } }
?: levels.indexOfFirst { it.id !in restoredCompletedLevels }.takeIf { it >= 0 }
?: levels.lastIndex
AppLog.d(
"GitHugApp",
"Restoring app to levelIndex=$resumeIndex levelId=${levels[resumeIndex].id}",
)
currentLevelIndex = resumeIndex currentLevelIndex = resumeIndex
repo = runtime.prepareLevel(levels[resumeIndex]) repo = runtime.prepareLevel(levels[resumeIndex])
output = listOf( output = listOf(
@@ -171,6 +186,7 @@ fun GitHugApp() {
} }
fun loadLevel(index: Int) { fun loadLevel(index: Int) {
AppLog.d("GitHugApp", "Loading level index=$index id=${levels[index].id} title=${levels[index].title}")
currentLevelIndex = index currentLevelIndex = index
repo = runtime.prepareLevel(levels[index]) repo = runtime.prepareLevel(levels[index])
output = listOf("Loaded level: ${levels[index].title}") output = listOf("Loaded level: ${levels[index].title}")
@@ -264,6 +280,10 @@ fun GitHugApp() {
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw) val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
val solvedAfterCommand = currentLevel.validator(newRepo, raw) val solvedAfterCommand = currentLevel.validator(newRepo, raw)
val wasAlreadyCompleted = currentLevel.id in completedLevels val wasAlreadyCompleted = currentLevel.id in completedLevels
AppLog.d(
"GitHugApp",
"Command='$raw' level=${currentLevel.id} solved=$solvedAfterCommand alreadyCompleted=$wasAlreadyCompleted completedBefore=${completedLevels.sorted()}",
)
val newOutput = buildList { val newOutput = buildList {
addAll(output) addAll(output)
add("$ $raw") add("$ $raw")
@@ -275,17 +295,25 @@ fun GitHugApp() {
if (solvedAfterCommand && !wasAlreadyCompleted) { if (solvedAfterCommand && !wasAlreadyCompleted) {
val updatedCompletedLevels = completedLevels + currentLevel.id val updatedCompletedLevels = completedLevels + currentLevel.id
completedLevels = updatedCompletedLevels completedLevels = updatedCompletedLevels
scope.launch { gameProgressStore.saveCompletedLevels(updatedCompletedLevels) }
val nextLevelIndex = levels.indexOfFirst { it.id !in updatedCompletedLevels } val nextLevelIndex = levels.indexOfFirst { it.id !in updatedCompletedLevels }
if (nextLevelIndex >= 0) { if (nextLevelIndex >= 0) {
val nextLevelId = levels[nextLevelIndex].id
AppLog.d(
"GitHugApp",
"Level solved ${currentLevel.id}; advancing to next incomplete index=$nextLevelIndex id=$nextLevelId",
)
scope.launch { persistProgress(updatedCompletedLevels, nextLevelId) }
loadLevel(nextLevelIndex) loadLevel(nextLevelIndex)
} else { } else {
AppLog.d("GitHugApp", "All levels completed")
scope.launch { persistProgress(updatedCompletedLevels, levels.lastOrNull()?.id) }
repo = newRepo repo = newRepo
output = listOf("🏁 All Githug levels completed.") output = listOf("🏁 All Githug levels completed.")
clearCommandInput() clearCommandInput()
suppressedImeEcho = null suppressedImeEcho = null
} }
} else { } else {
AppLog.d("GitHugApp", "Staying on level=${currentLevel.id}")
repo = newRepo repo = newRepo
output = newOutput output = newOutput
applyRecommendedPaneWeights(persist = false) applyRecommendedPaneWeights(persist = false)

View File

@@ -17,6 +17,7 @@ class GitRepositoryRuntime(private val context: Context) {
} }
fun prepareLevel(level: Level): RepoState { fun prepareLevel(level: Level): RepoState {
AppLog.d("GitRuntime", "Preparing level id=${level.id} title=${level.title}")
val nativeGit = nativeGitBinary() val nativeGit = nativeGitBinary()
if (nativeGit == null) { if (nativeGit == null) {
return level.setup() return level.setup()
@@ -52,6 +53,7 @@ class GitRepositoryRuntime(private val context: Context) {
} }
fun execute(level: Level, currentRepo: RepoState, command: String): Pair<RepoState, List<String>> { fun execute(level: Level, currentRepo: RepoState, command: String): Pair<RepoState, List<String>> {
AppLog.d("GitRuntime", "Executing command for level=${level.id}: $command")
val nativeGit = nativeGitBinary() val nativeGit = nativeGitBinary()
if (nativeGit == null) { if (nativeGit == null) {
return GitSandboxEngine.execute(currentRepo, command) return GitSandboxEngine.execute(currentRepo, command)