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/GameModels.kt
app/src/main/java/solutions/tretter/githugandroid/GitHelpCommands.kt
app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt
app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt
app/src/main/java/solutions/tretter/githugandroid/Visual.kt
app/src/main/java/solutions/tretter/githugandroid/levels/AdvancedLevels.kt
app/src/main/java/solutions/tretter/githugandroid/levels/LevelCatalog.kt
app/src/test/java/solutions/tretter/githugandroid/GitHelpCommandsTest.kt
app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt
This commit is contained in:
Joe Tretter
2026-05-05 19:27:33 -05:00
parent 6200fdc912
commit 16d7809150
10 changed files with 322 additions and 46 deletions

View File

@@ -1,7 +1,15 @@
package solutions.tretter.githugandroid
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -10,10 +18,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -23,11 +33,16 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
@@ -63,8 +78,16 @@ fun GitHugApp() {
var editorState by remember { mutableStateOf<TextEditorState?>(null) }
var gitMessageEditorState by remember { mutableStateOf<GitMessageEditorState?>(null) }
var manPageState by remember { mutableStateOf<ManPageState?>(null) }
var solvedCelebrationTitle by remember { mutableStateOf<String?>(null) }
val currentLevel = levels[currentLevelIndex]
LaunchedEffect(solvedCelebrationTitle) {
if (solvedCelebrationTitle != null) {
delay(3_000)
solvedCelebrationTitle = null
}
}
LaunchedEffect(persistedPaneLayout) {
paneLayout = persistedPaneLayout
}
@@ -179,11 +202,11 @@ fun GitHugApp() {
applyRecommendedPaneWeights(persist = false)
}
fun loadLevel(index: Int) {
fun loadLevel(index: Int, message: List<String> = listOf("Loaded level: ${levels[index].title}")) {
AppLog.d("GitHugApp", "Loading level index=$index id=${levels[index].id} title=${levels[index].title}")
currentLevelIndex = index
repo = runtime.prepareLevel(levels[index])
output = listOf("Loaded level: ${levels[index].title}")
output = message
clearCommandInput()
suppressedImeEcho = null
hintIndex = 0
@@ -275,12 +298,15 @@ fun GitHugApp() {
}
addAll(lines)
if (solvedAfterCommand && !wasAlreadyCompleted) {
add("✔ Level solved: ${levelForResult.title}")
add("")
add("👍 Level solved: ${levelForResult.title}")
add("Nice. ${completedLevels.size + 1}/${levels.size} levels complete.")
}
}
if (solvedAfterCommand && !wasAlreadyCompleted) {
val updatedCompletedLevels = completedLevels + levelForResult.id
completedLevels = updatedCompletedLevels
solvedCelebrationTitle = levelForResult.title
val nextLevelIndex = levels.indexOfFirst { it.id !in updatedCompletedLevels }
if (nextLevelIndex >= 0) {
val nextLevelId = levels[nextLevelIndex].id
@@ -289,12 +315,17 @@ fun GitHugApp() {
"Level solved ${levelForResult.id}; advancing to next incomplete index=$nextLevelIndex id=$nextLevelId",
)
scope.launch { persistProgress(updatedCompletedLevels, nextLevelId) }
loadLevel(nextLevelIndex)
loadLevel(
nextLevelIndex,
message = newOutput + listOf(
"Next level loaded: ${levels[nextLevelIndex].title}",
),
)
} else {
AppLog.d("GitHugApp", "All levels completed")
scope.launch { persistProgress(updatedCompletedLevels, levels.lastOrNull()?.id) }
repo = newRepo
output = listOf("🏁 All Githug levels completed.")
output = newOutput + listOf("🏁 All Githug levels completed.")
clearCommandInput()
suppressedImeEcho = null
}
@@ -427,10 +458,10 @@ fun GitHugApp() {
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxSize()
.background(AppBackground)
.verticalScroll(screenScrollState)
.imePadding()
.verticalScroll(screenScrollState)
.padding(horizontal = 10.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
@@ -537,5 +568,55 @@ fun GitHugApp() {
onClose = { manPageState = null },
)
}
solvedCelebrationTitle?.let { title ->
SolvedCelebrationOverlay(title = title)
}
}
}
@Composable
private fun SolvedCelebrationOverlay(title: String) {
val transition = rememberInfiniteTransition(label = "solved-celebration")
val progress by transition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1_200, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "confetti-progress",
)
val colors = listOf(Accent, Success, Color(0xFFFFD166), Color(0xFFFF6B6B), Color(0xFF9BF6FF))
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Canvas(modifier = Modifier.fillMaxSize()) {
repeat(42) { index ->
val x = ((index * 73) % 100) / 100f * size.width
val baseY = ((index * 37) % 100) / 100f * size.height
val y = (baseY + progress * size.height * 0.6f) % size.height
val pieceSize = 6.dp.toPx() + (index % 4) * 2.dp.toPx()
drawCircle(
color = colors[index % colors.size],
radius = pieceSize / 2f,
center = Offset(x, y),
alpha = 0.85f,
)
}
}
Column(
modifier = Modifier
.background(PanelPrimary.copy(alpha = 0.94f), RoundedCornerShape(24.dp))
.padding(horizontal = 26.dp, vertical = 22.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(text = "👍", fontSize = 56.sp)
Text(text = "Level solved", color = Success, fontSize = 22.sp)
Text(text = title, color = TextSecondary, fontSize = 14.sp)
}
}
}