From b189deba72913917607ae76f15653ee94d1538e1 Mon Sep 17 00:00:00 2001 From: Rene Luria Date: Fri, 28 Aug 2026 15:23:19 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=C3=A9cran=20de=20jeu=20avec=20pav?= =?UTF-8?q?=C3=A9=20num=C3=A9rique,=20barre=20de=20temps=20et=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/livrets/ui/GameScreen.kt | 196 +++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) diff --git a/sharedUI/src/commonMain/kotlin/com/livrets/ui/GameScreen.kt b/sharedUI/src/commonMain/kotlin/com/livrets/ui/GameScreen.kt index 70d3bb6..25cf6d2 100644 --- a/sharedUI/src/commonMain/kotlin/com/livrets/ui/GameScreen.kt +++ b/sharedUI/src/commonMain/kotlin/com/livrets/ui/GameScreen.kt @@ -1,13 +1,42 @@ package com.livrets.ui -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.livrets.game.GameConfig +import com.livrets.game.GameEngine import com.livrets.game.GameResult +import com.livrets.game.GameState import kotlinx.coroutines.CoroutineScope @Composable @@ -17,5 +46,168 @@ fun GameScreen( onFinish: (GameResult) -> Unit, onQuit: () -> Unit, ) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Jeu") } + val engine = remember(config) { GameEngine(config, scope, onFinish = onFinish) } + val state by engine.state.collectAsState() + + LaunchedEffect(engine) { engine.start() } + + var showQuitDialog by remember { mutableStateOf(false) } + if (showQuitDialog) { + AlertDialog( + onDismissRequest = { showQuitDialog = false }, + title = { Text("Quitter la partie ?") }, + text = { Text("La partie en cours sera abandonnée.") }, + confirmButton = { TextButton(onClick = onQuit) { Text("Quitter") } }, + dismissButton = { TextButton(onClick = { showQuitDialog = false }) { Text("Continuer") } }, + ) + } + + when (val s = state) { + is GameState.Playing -> PlayingContent( + engine = engine, + state = s, + progress = s.remainingMs.toFloat() / (config.secondsPerQuestion * 1000L), + onQuitRequest = { showQuitDialog = true }, + ) + + is GameState.Feedback -> FeedbackContent(s) + else -> {} + } +} + +@Composable +private fun PlayingContent( + engine: GameEngine, + state: GameState.Playing, + progress: Float, + onQuitRequest: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.safeDrawing) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + "Question ${state.questionIndex + 1} / ${state.totalQuestions}", + style = MaterialTheme.typography.titleMedium, + ) + TextButton(onClick = onQuitRequest) { Text("Quitter") } + } + + LinearProgressIndicator( + progress = { progress.coerceIn(0f, 1f) }, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + ) + + Spacer(Modifier.weight(1f)) + + Text( + text = state.question.toString(), + style = MaterialTheme.typography.displayMedium, + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.height(24.dp)) + + Text( + text = state.entry.ifEmpty { " " }, + style = MaterialTheme.typography.displayLarge, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .height(80.dp) + .background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(12.dp)), + ) + + Spacer(Modifier.weight(1f)) + + Keypad( + onDigit = engine::onDigit, + onDelete = engine::onDelete, + onValidate = engine::onValidate, + ) + } +} + +@Composable +private fun Keypad( + onDigit: (Char) -> Unit, + onDelete: () -> Unit, + onValidate: () -> Unit, +) { + val rows = listOf( + listOf("1", "2", "3"), + listOf("4", "5", "6"), + listOf("7", "8", "9"), + listOf("⌫", "0", "OK"), + ) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + for (row in rows) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + for (key in row) { + ElevatedButton( + onClick = { + when (key) { + "⌫" -> onDelete() + "OK" -> onValidate() + else -> onDigit(key.first()) + } + }, + modifier = Modifier.size(width = 96.dp, height = 64.dp), + ) { + Text(key, fontSize = 24.sp) + } + } + } + } + } +} + +@Composable +private fun FeedbackContent(state: GameState.Feedback) { + val ok = state.record.isCorrect + val green = Color(0xFF2E7D32) + val red = Color(0xFFC62828) + Column( + modifier = Modifier + .fillMaxSize() + .background(if (ok) green.copy(alpha = 0.15f) else red.copy(alpha = 0.15f)) + .windowInsetsPadding(WindowInsets.safeDrawing) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = if (ok) "✓" else "✗", + fontSize = 72.sp, + color = if (ok) green else red, + ) + Spacer(Modifier.height(16.dp)) + Text( + text = state.record.question.toString(), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + ) + if (!ok) { + Spacer(Modifier.height(8.dp)) + Text( + text = if (state.record.givenAnswer == null) "Temps écoulé" + else "Votre réponse : ${state.record.givenAnswer}", + style = MaterialTheme.typography.titleMedium, + color = red, + ) + Text( + text = "Bonne réponse : ${state.record.correctAnswer}", + style = MaterialTheme.typography.titleLarge, + color = green, + ) + } + } }