From 95e3caafa93d33161f9371a0262e06ffdc818595 Mon Sep 17 00:00:00 2001 From: Simone Mapelli Date: Sun, 28 Dec 2025 12:44:11 +0100 Subject: [PATCH 1/4] Add memory flash memory challenge --- .../core/presentation/navigation/NavGraph.kt | 16 ++ .../presentation/navigation/ScreenRoutes.kt | 3 + .../domain/model/MemoryFlashChallenge.kt | 8 + .../domain/model/MemoryFlashConfig.kt | 7 + .../GenerateMemoryFlashChallengeUseCase.kt | 13 ++ .../UpdateMemoryFlashScoreUseCase.kt | 21 ++ .../components/MemoryFlashChallengeCard.kt | 72 ++++++ .../components/MemoryFlashHeader.kt | 70 ++++++ .../presentation/state/MemoryFlashUiState.kt | 24 ++ .../presentation/ui/MemoryFlashGameScreen.kt | 134 +++++++++++ .../presentation/ui/MemoryFlashViewModel.kt | 215 ++++++++++++++++++ .../feat_home/data/local/GamesStaticData.kt | 4 + .../feat_home/domain/model/GameTypes.kt | 4 + .../domain/use_cases/GetGameScoresUseCase.kt | 1 + .../feat_home/presentation/ui/HomeScreen.kt | 8 + .../data/local/db/MathBrainerDatabase.kt | 2 +- .../domain/model/GameScores.kt | 4 +- app/src/main/res/values/strings.xml | 14 ++ 18 files changed, 618 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/model/MemoryFlashChallenge.kt create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/model/MemoryFlashConfig.kt create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/use_cases/GenerateMemoryFlashChallengeUseCase.kt create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/use_cases/UpdateMemoryFlashScoreUseCase.kt create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashHeader.kt create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/state/MemoryFlashUiState.kt create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashGameScreen.kt create mode 100644 app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/navigation/NavGraph.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/navigation/NavGraph.kt index fda6d02..b1ac792 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/navigation/NavGraph.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/navigation/NavGraph.kt @@ -14,6 +14,7 @@ import eu.indiewalkabout.mathbrainer.feat_credits.presentation.ui.GameCreditsScr import eu.indiewalkabout.mathbrainer.feat_games.feat_count_items.presentation.ui.CountObjectsGameScreen import eu.indiewalkabout.mathbrainer.feat_games.feat_enigma.presentation.ui.EnigmaGameScreen import eu.indiewalkabout.mathbrainer.feat_games.feat_falling_op.presentation.ui.FallingOpsGameScreen +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.ui.MemoryFlashGameScreen import eu.indiewalkabout.mathbrainer.feat_games.feat_math_op_double.presentation.ui.DoubleNumberGameScreen import eu.indiewalkabout.mathbrainer.feat_games.feat_math_op_write.presentation.ui.MathWriteGameScreen import eu.indiewalkabout.mathbrainer.feat_games.feat_math_random_operation.presentation.ui.RandomOperationGameScreen @@ -120,6 +121,21 @@ fun NavGraph( ) } + // --- Memory Flash Game --- + composable( + route = ScreenRoutes.MemoryFlashGame.route, + arguments = listOf( + navArgument("highScore") { type = NavType.IntType } + ) + ) { backStackEntry -> + val highScore = backStackEntry.arguments?.getInt("highScore") ?: 0 + + MemoryFlashGameScreen( + initialHighScore = highScore, + onBack = { navController.popBackStack() } + ) + } + // --- Number Order Game --- composable( route = ScreenRoutes.NumberOrderGame.route, diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/navigation/ScreenRoutes.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/navigation/ScreenRoutes.kt index 64280ab..38d33db 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/navigation/ScreenRoutes.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/navigation/ScreenRoutes.kt @@ -22,6 +22,9 @@ sealed class ScreenRoutes(val route: String) { object RandomOperationGame : ScreenRoutes("random_op_game/{highScore}") { fun createRoute(highScore: Int = 0): String = "random_op_game/$highScore" } + object MemoryFlashGame : ScreenRoutes("memory_flash_game/{highScore}") { + fun createRoute(highScore: Int = 0): String = "memory_flash_game/$highScore" + } object CountObjectsGame : ScreenRoutes("count_objects_game/{highScore}") { fun createRoute(highScore: Int = 0): String { return "count_objects_game/$highScore" diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/model/MemoryFlashChallenge.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/model/MemoryFlashChallenge.kt new file mode 100644 index 0000000..7fcc5fd --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/model/MemoryFlashChallenge.kt @@ -0,0 +1,8 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.model + +data class MemoryFlashChallenge( + val sequence: List +) { + val answer: String = sequence.joinToString(separator = "") + val displaySequence: String = sequence.joinToString(separator = " ") +} diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/model/MemoryFlashConfig.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/model/MemoryFlashConfig.kt new file mode 100644 index 0000000..1aa0d1b --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/model/MemoryFlashConfig.kt @@ -0,0 +1,7 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.model + +data class MemoryFlashConfig( + val level: Int, + val length: Int, + val maxDigit: Int +) diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/use_cases/GenerateMemoryFlashChallengeUseCase.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/use_cases/GenerateMemoryFlashChallengeUseCase.kt new file mode 100644 index 0000000..cc5e994 --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/use_cases/GenerateMemoryFlashChallengeUseCase.kt @@ -0,0 +1,13 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.use_cases + +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.model.MemoryFlashChallenge +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.model.MemoryFlashConfig +import javax.inject.Inject +import kotlin.random.Random + +class GenerateMemoryFlashChallengeUseCase @Inject constructor() { + operator fun invoke(config: MemoryFlashConfig): MemoryFlashChallenge { + val digits = MutableList(config.length) { Random.nextInt(0, config.maxDigit + 1) } + return MemoryFlashChallenge(sequence = digits) + } +} diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/use_cases/UpdateMemoryFlashScoreUseCase.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/use_cases/UpdateMemoryFlashScoreUseCase.kt new file mode 100644 index 0000000..fbc54b6 --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/domain/use_cases/UpdateMemoryFlashScoreUseCase.kt @@ -0,0 +1,21 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.use_cases + +import eu.indiewalkabout.mathbrainer.feat_statistics.domain.model.GameScores.Companion.emptyScores +import eu.indiewalkabout.mathbrainer.feat_statistics.domain.repository.MathBrainerRepository +import javax.inject.Inject +import kotlin.math.max +import kotlinx.coroutines.flow.firstOrNull + +class UpdateMemoryFlashScoreUseCase @Inject constructor( + private val repository: MathBrainerRepository +) { + suspend operator fun invoke(sessionScore: Int) { + val currentScores = repository.observeGameScores().firstOrNull() ?: emptyScores + val updatedScores = currentScores.copy( + memory_flash_game_score = max(currentScores.memory_flash_game_score, sessionScore), + id = 0, + global_score = currentScores.global_score + sessionScore + ) + repository.insertGameScores(updatedScores) + } +} diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt new file mode 100644 index 0000000..8495791 --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt @@ -0,0 +1,72 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import eu.indiewalkabout.mathbrainer.R +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.state.MemoryFlashUiState + +@Composable +fun MemoryFlashChallengeCard(state: MemoryFlashUiState) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.memory_flash_instruction), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + + val sequenceText = when { + state.isSequenceVisible -> state.visibleSequence + state.revealedSequence != null -> state.revealedSequence + else -> stringResource(id = R.string.memory_flash_hidden_sequence) + } + + Text( + text = sequenceText, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold + ) + + if (!state.isSequenceVisible && state.revealedSequence == null) { + Text( + text = stringResource(id = R.string.memory_flash_prompt), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + } + } +} diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashHeader.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashHeader.kt new file mode 100644 index 0000000..44ec56e --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashHeader.kt @@ -0,0 +1,70 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.indiewalkabout.mathbrainer.R +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.state.MemoryFlashUiState + +@Composable +fun MemoryFlashHeader(state: MemoryFlashUiState) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.score_label, state.score), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource( + id = R.string.high_score_label, + state.highScore ?: 0 + ), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.level_label, state.level), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(id = R.string.lives_label, state.lives), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Text( + text = stringResource( + id = R.string.memory_flash_challenges_label, + state.challengesCompleted, + state.challengesPerLevel + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/state/MemoryFlashUiState.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/state/MemoryFlashUiState.kt new file mode 100644 index 0000000..4ee5929 --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/state/MemoryFlashUiState.kt @@ -0,0 +1,24 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.state + +data class MemoryFlashUiState( + val score: Int = 0, + val highScore: Int? = null, + val level: Int = 1, + val lives: Int = 3, + val visibleSequence: String = "", + val isSequenceVisible: Boolean = true, + val inputValue: String = "", + val feedback: Feedback? = null, + val isGameOver: Boolean = false, + val challengesCompleted: Int = 0, + val challengesPerLevel: Int = INITIAL_CHALLENGES_PER_LEVEL, + val revealedSequence: String? = null, + val isReadyForNext: Boolean = false +) { + enum class Feedback { SUCCESS, FAILURE } + + companion object { + const val INITIAL_CHALLENGES_PER_LEVEL = 3 + const val MAX_CHALLENGES_PER_LEVEL = 6 + } +} diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashGameScreen.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashGameScreen.kt new file mode 100644 index 0000000..22c18c6 --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashGameScreen.kt @@ -0,0 +1,134 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.ui + +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import eu.indiewalkabout.mathbrainer.R +import eu.indiewalkabout.mathbrainer.core.presentation.components.GameOverDialog +import eu.indiewalkabout.mathbrainer.core.presentation.components.ResultBanner +import eu.indiewalkabout.mathbrainer.core.presentation.components.keyboard.Keypad +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.components.MemoryFlashChallengeCard +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.components.MemoryFlashHeader +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.state.MemoryFlashUiState + +@Composable +fun MemoryFlashGameScreen( + initialHighScore: Int = 0, + onBack: () -> Unit, + viewModel: MemoryFlashViewModel = hiltViewModel() +) { + LaunchedEffect(initialHighScore) { + viewModel.startGame(initialHighScore) + } + + val state by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = { + viewModel.onQuitGame() + onBack() + }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = stringResource(id = R.string.navigate_back), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + Spacer(modifier = Modifier.padding(4.dp)) + Column { + Text( + text = stringResource(id = R.string.memory_flash_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = stringResource(id = R.string.memory_flash_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimary + ) + } + } + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + MemoryFlashHeader(state = state) + MemoryFlashChallengeCard(state = state) + + when (state.feedback) { + MemoryFlashUiState.Feedback.SUCCESS -> ResultBanner( + text = stringResource(id = R.string.memory_flash_success), + color = MaterialTheme.colorScheme.primary + ) + + MemoryFlashUiState.Feedback.FAILURE -> ResultBanner( + text = stringResource(id = R.string.memory_flash_failure), + color = MaterialTheme.colorScheme.error + ) + + null -> Spacer(modifier = Modifier.height(0.dp)) + } + + if (state.isReadyForNext && !state.isGameOver) { + Button( + onClick = { viewModel.onNextChallenge() }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.next_challenge)) + } + } + + Keypad( + inputValue = state.inputValue, + onDigitPressed = { digit -> viewModel.onDigitPressed(digit) }, + onDelete = { viewModel.onDelete() }, + onSubmit = { viewModel.submitAnswer() }, + isCompact = true + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } + + if (state.isGameOver) { + GameOverDialog(onDismiss = { + viewModel.onQuitGame() + onBack() + }) + } +} diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt new file mode 100644 index 0000000..7c90dc2 --- /dev/null +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt @@ -0,0 +1,215 @@ +package eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.model.MemoryFlashChallenge +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.model.MemoryFlashConfig +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.use_cases.GenerateMemoryFlashChallengeUseCase +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.use_cases.UpdateMemoryFlashScoreUseCase +import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.state.MemoryFlashUiState +import eu.indiewalkabout.mathbrainer.feat_home.domain.model.GameTypes +import eu.indiewalkabout.mathbrainer.feat_statistics.domain.use_cases.UpdateGameStatsUseCase +import javax.inject.Inject +import kotlin.math.max +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class MemoryFlashViewModel @Inject constructor( + private val generateMemoryFlashChallengeUseCase: GenerateMemoryFlashChallengeUseCase, + private val updateMemoryFlashScoreUseCase: UpdateMemoryFlashScoreUseCase, + private val updateGameStatsUseCase: UpdateGameStatsUseCase +) : ViewModel() { + + private var challengesCompletedInternal = 0 + private var challengesPerLevelInternal = MemoryFlashUiState.INITIAL_CHALLENGES_PER_LEVEL + private var isScorePersisted = false + private var currentChallenge: MemoryFlashChallenge? = null + + private val _uiState = MutableStateFlow(MemoryFlashUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun startGame(initialHighScore: Int = 0) { + isScorePersisted = false + challengesCompletedInternal = 0 + challengesPerLevelInternal = MemoryFlashUiState.INITIAL_CHALLENGES_PER_LEVEL + + _uiState.update { + it.copy( + highScore = initialHighScore.takeIf { score -> score > 0 }, + score = 0, + level = 1, + lives = 3, + feedback = null, + isGameOver = false, + challengesCompleted = 0, + challengesPerLevel = MemoryFlashUiState.INITIAL_CHALLENGES_PER_LEVEL, + revealedSequence = null, + isReadyForNext = false, + visibleSequence = "", + isSequenceVisible = true, + inputValue = "" + ) + } + + viewModelScope.launch { launchNewChallenge() } + } + + fun onDigitPressed(digit: Int) { + if (_uiState.value.isGameOver || _uiState.value.isReadyForNext || _uiState.value.isSequenceVisible) return + _uiState.update { current -> + current.copy(inputValue = (current.inputValue + digit.toString()).take(12)) + } + } + + fun onDelete() { + if (_uiState.value.isGameOver || _uiState.value.isReadyForNext || _uiState.value.isSequenceVisible) return + _uiState.update { current -> + val newValue = if (current.inputValue.isNotEmpty()) current.inputValue.dropLast(1) else "" + current.copy(inputValue = newValue) + } + } + + fun submitAnswer() { + if (_uiState.value.isGameOver || _uiState.value.isReadyForNext || _uiState.value.isSequenceVisible) return + val challenge = currentChallenge ?: return + val attempt = _uiState.value.inputValue + + if (attempt == challenge.answer) { + handleSuccess(challenge) + } else { + handleFailure(challenge) + } + } + + fun onNextChallenge() { + if (_uiState.value.isGameOver || !_uiState.value.isReadyForNext) return + viewModelScope.launch { launchNewChallenge() } + } + + fun onQuitGame() { + persistScoreIfNeeded() + } + + private suspend fun launchNewChallenge() { + val config = buildConfig() + val challenge = generateMemoryFlashChallengeUseCase(config) + currentChallenge = challenge + + _uiState.update { + it.copy( + visibleSequence = challenge.displaySequence, + inputValue = "", + feedback = null, + revealedSequence = null, + isReadyForNext = false, + isSequenceVisible = true + ) + } + + viewModelScope.launch { + delay(SEQUENCE_REVEAL_DURATION_MS) + _uiState.update { current -> current.copy(isSequenceVisible = false) } + } + } + + private fun handleSuccess(challenge: MemoryFlashChallenge) { + val newScore = _uiState.value.score + challenge.sequence.size * SCORE_PER_DIGIT + val shouldLevelUp = challengesCompletedInternal + 1 >= challengesPerLevelInternal + + val (nextLevel, nextChallengesPerLevel) = if (shouldLevelUp) { + challengesCompletedInternal = 0 + promoteLevel(_uiState.value.level) + } else { + challengesCompletedInternal++ + _uiState.value.level to challengesPerLevelInternal + } + + _uiState.update { + it.copy( + feedback = MemoryFlashUiState.Feedback.SUCCESS, + score = newScore, + highScore = max(it.highScore ?: 0, newScore), + challengesCompleted = challengesCompletedInternal, + challengesPerLevel = nextChallengesPerLevel, + level = nextLevel, + revealedSequence = challenge.displaySequence, + isReadyForNext = true, + isSequenceVisible = true + ) + } + } + + private fun handleFailure(challenge: MemoryFlashChallenge) { + val remainingLives = _uiState.value.lives - 1 + _uiState.update { + it.copy( + lives = remainingLives, + feedback = MemoryFlashUiState.Feedback.FAILURE, + revealedSequence = challenge.displaySequence, + isReadyForNext = remainingLives > 0, + isSequenceVisible = true + ) + } + + if (remainingLives <= 0) { + onGameOver() + } + } + + private fun promoteLevel(currentLevel: Int): Pair { + val nextLevel = currentLevel + 1 + val nextChallengesPerLevel = (MemoryFlashUiState.INITIAL_CHALLENGES_PER_LEVEL + (nextLevel / 2)) + .coerceAtMost(MemoryFlashUiState.MAX_CHALLENGES_PER_LEVEL) + challengesPerLevelInternal = nextChallengesPerLevel + return nextLevel to nextChallengesPerLevel + } + + private fun onGameOver() { + _uiState.update { it.copy(isGameOver = true, isReadyForNext = false) } + persistScoreIfNeeded() + } + + private fun persistScoreIfNeeded() { + if (isScorePersisted) return + isScorePersisted = true + val finalScore = _uiState.value.score + viewModelScope.launch { + updateGameStatsUseCase( + gameId = GameTypes.MEMORY_FLASH.id, + sessionScore = finalScore, + isWin = finalScore > 0, + lastLevel = _uiState.value.level + ) + if (finalScore > 0) { + updateMemoryFlashScoreUseCase(finalScore) + } + } + } + + private fun buildConfig(): MemoryFlashConfig { + val level = _uiState.value.level + val length = (BASE_SEQUENCE_LENGTH + level / 2).coerceAtMost(MAX_SEQUENCE_LENGTH) + val maxDigit = (BASE_MAX_DIGIT + level).coerceAtMost(MAX_DIGIT) + + return MemoryFlashConfig( + level = level, + length = length, + maxDigit = maxDigit + ) + } + + companion object { + private const val BASE_SEQUENCE_LENGTH = 3 + private const val MAX_SEQUENCE_LENGTH = 8 + private const val BASE_MAX_DIGIT = 4 + private const val MAX_DIGIT = 9 + private const val SCORE_PER_DIGIT = 10 + private const val SEQUENCE_REVEAL_DURATION_MS = 1800L + } +} diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/data/local/GamesStaticData.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/data/local/GamesStaticData.kt index 8b3651d..c4e76e6 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/data/local/GamesStaticData.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/data/local/GamesStaticData.kt @@ -63,6 +63,10 @@ val gamesDefinitionsList = listOf( titleRes = R.string.game_card_choose_random_operation_text , // R.string.random_operations_title, // descriptionRes = R.string.random_operations_description, ), + GameDefinition( + id = "memory_flash", + titleRes = R.string.game_card_memory_flash_text, + ), GameDefinition( id = "falling_ops", titleRes = R.string.game_card_falling_ops_text, diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/domain/model/GameTypes.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/domain/model/GameTypes.kt index 1da1c82..e69f22e 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/domain/model/GameTypes.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/domain/model/GameTypes.kt @@ -67,6 +67,10 @@ enum class GameTypes( id = "random", scoreField = { it.random_op_game_score } ), + MEMORY_FLASH( + id = "memory_flash", + scoreField = { it.memory_flash_game_score } + ), SEQUENCE_COMPLETE( id = "sequence_complete", scoreField = { it.sequence_complete_game_score } diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/domain/use_cases/GetGameScoresUseCase.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/domain/use_cases/GetGameScoresUseCase.kt index 1b6f751..8cd831d 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/domain/use_cases/GetGameScoresUseCase.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/domain/use_cases/GetGameScoresUseCase.kt @@ -27,6 +27,7 @@ class GetGameScoresUseCase @Inject constructor( random_op_game_score = 0, count_objects_game_score = 0, number_order_game_score = 0, + memory_flash_game_score = 0, sequence_complete_game_score = 0, falling_ops_game_score = 0, enigma_game_score = 0 diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/presentation/ui/HomeScreen.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/presentation/ui/HomeScreen.kt index b3790a7..10ae4a1 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/presentation/ui/HomeScreen.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_home/presentation/ui/HomeScreen.kt @@ -163,6 +163,14 @@ fun HomeScreen( ) } + GameTypes.MEMORY_FLASH -> { + navController.navigate( + ScreenRoutes.MemoryFlashGame.createRoute( + highScore = game.highScore ?: 0 + ) + ) + } + GameTypes.DOUBLE_NUMBER -> { navController.navigate( ScreenRoutes.DoubleNumberGame.createRoute( diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/data/local/db/MathBrainerDatabase.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/data/local/db/MathBrainerDatabase.kt index b9fc4bc..597ac09 100755 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/data/local/db/MathBrainerDatabase.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/data/local/db/MathBrainerDatabase.kt @@ -15,7 +15,7 @@ import eu.indiewalkabout.mathbrainer.feat_statistics.domain.model.GameStats GameStatistics::class, GameStats::class ], - version = 8, + version = 9, exportSchema = true ) @TypeConverters(DateConverter::class) diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/domain/model/GameScores.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/domain/model/GameScores.kt index fc774bd..8e428db 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/domain/model/GameScores.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/domain/model/GameScores.kt @@ -24,6 +24,7 @@ data class GameScores( val random_op_game_score: Int, val count_objects_game_score: Int, val number_order_game_score: Int, + val memory_flash_game_score: Int, val sequence_complete_game_score: Int, val falling_ops_game_score: Int, val enigma_game_score: Int @@ -45,9 +46,10 @@ data class GameScores( random_op_game_score = 0, count_objects_game_score = 0, number_order_game_score = 0, + memory_flash_game_score = 0, sequence_complete_game_score = 0, falling_ops_game_score = 0, - enigma_game_score = 0 + enigma_game_score = 0 ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5326fbf..7325b38 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,6 +54,19 @@ Random Operations Solve random math operations. Choose the operation that makes the equation true. + Memory Flash + Memorize the digits, then type them back. + Memorize this sequence while it is visible. + Enter the sequence from memory to score points. + ? ? ? + Great recall! Get ready for the next pattern. + That pattern was different. Try the next one. + Round %1$d of %2$d + Next challenge + Score: %1$d + Best: %1$d + Level %1$d + Lives: %1$d Falling Ops Clear the falling operations before they reach the keypad. Type results to stop the fall. @@ -124,6 +137,7 @@ : \n 2x \n 1 2 4 ?\n + ✦ ✦ ✦\n ↓ \n+ – x : △ □ ?\n From d8b00723c1733d1c1140ecf8bd93a1861d85133e Mon Sep 17 00:00:00 2001 From: simone Date: Sun, 28 Dec 2025 15:54:29 +0100 Subject: [PATCH 2/4] feat: improve dealy for memorizing time --- .../presentation/ui/MemoryFlashViewModel.kt | 18 +++++++++++++----- .../presentation/ui/StatisticScreen.kt | 8 ++++++++ app/src/main/res/values/strings.xml | 10 +++++++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt index 7c90dc2..77e984d 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt @@ -10,14 +10,14 @@ import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.domain.use_cas import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.state.MemoryFlashUiState import eu.indiewalkabout.mathbrainer.feat_home.domain.model.GameTypes import eu.indiewalkabout.mathbrainer.feat_statistics.domain.use_cases.UpdateGameStatsUseCase -import javax.inject.Inject -import kotlin.math.max import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.math.max @HiltViewModel class MemoryFlashViewModel @Inject constructor( @@ -96,7 +96,7 @@ class MemoryFlashViewModel @Inject constructor( persistScoreIfNeeded() } - private suspend fun launchNewChallenge() { + private fun launchNewChallenge() { val config = buildConfig() val challenge = generateMemoryFlashChallengeUseCase(config) currentChallenge = challenge @@ -113,7 +113,9 @@ class MemoryFlashViewModel @Inject constructor( } viewModelScope.launch { - delay(SEQUENCE_REVEAL_DURATION_MS) + val sequenceLength = challenge.sequence.size + val revealDuration = calculateRevealDuration(sequenceLength) + delay(revealDuration) _uiState.update { current -> current.copy(isSequenceVisible = false) } } } @@ -210,6 +212,12 @@ class MemoryFlashViewModel @Inject constructor( private const val BASE_MAX_DIGIT = 4 private const val MAX_DIGIT = 9 private const val SCORE_PER_DIGIT = 10 - private const val SEQUENCE_REVEAL_DURATION_MS = 1800L + private const val BASE_DELAY_PER_DIGIT_MS = 300L // Base delay per digit in milliseconds + private const val MIN_TOTAL_DELAY_MS = 900L // Minimum total delay + + private fun calculateRevealDuration(sequenceLength: Int): Long { + val calculatedDelay = sequenceLength * BASE_DELAY_PER_DIGIT_MS + return maxOf(calculatedDelay, MIN_TOTAL_DELAY_MS) + } } } diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/presentation/ui/StatisticScreen.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/presentation/ui/StatisticScreen.kt index 4d37716..6dea959 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/presentation/ui/StatisticScreen.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_statistics/presentation/ui/StatisticScreen.kt @@ -183,6 +183,14 @@ fun StatisticScreen( ) } + GameTypes.MEMORY_FLASH -> { + navController.navigate( + ScreenRoutes.MemoryFlashGame.createRoute( + highScore = game.highScore ?: 0 + ) + ) + } + null -> navController.navigate(ScreenRoutes.Home.route) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7325b38..fb656a8 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,10 @@ Highscore: %1$d No high score yet Played: %1$d + Played: %1$d + Wins: %1$d \nLosses: %2$d + Win %%: %1$d + Best level: %1$d Wins: %1$d \nLosses: %2$d Win %%: %1$d Best level: %1$d @@ -58,7 +62,7 @@ Memorize the digits, then type them back. Memorize this sequence while it is visible. Enter the sequence from memory to score points. - ? ? ? + \? ? ? Great recall! Get ready for the next pattern. That pattern was different. Try the next one. Round %1$d of %2$d @@ -137,9 +141,9 @@ : \n 2x \n 1 2 4 ?\n - ✦ ✦ ✦\n + ✦ ✦ ✦\n ↓ \n+ – x : - △ □ ?\n + △ □ ?\n Rule: %1$s Missing number: %1$d From 38feb4307964e47d5aa838cd0ebd770912999fde Mon Sep 17 00:00:00 2001 From: simone Date: Sun, 28 Dec 2025 17:24:18 +0100 Subject: [PATCH 3/4] feat: set input field --- .../components/keyboard/Keypad.kt | 6 --- .../components/MemoryFlashChallengeCard.kt | 51 +++++++++++++++++-- .../presentation/ui/MemoryFlashGameScreen.kt | 2 +- .../ui/SequenceCompleteGameScreen.kt | 2 +- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/components/keyboard/Keypad.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/components/keyboard/Keypad.kt index 333522a..04467b3 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/components/keyboard/Keypad.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/core/presentation/components/keyboard/Keypad.kt @@ -78,12 +78,6 @@ fun Keypad( } } } - /*Spacer(modifier = Modifier.Companion.height(8.dp)) - Text( - text = stringResource(id = R.string.current_input, inputValue.ifEmpty { "-" }), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - )*/ } } } diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt index 8495791..7c28873 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt @@ -8,15 +8,22 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import eu.indiewalkabout.mathbrainer.R @@ -58,11 +65,45 @@ fun MemoryFlashChallengeCard(state: MemoryFlashUiState) { ) if (!state.isSequenceVisible && state.revealedSequence == null) { - Text( - text = stringResource(id = R.string.memory_flash_prompt), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + // Input field for the answer + TextField( + value = state.inputValue, + onValueChange = {}, + readOnly = true, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + textStyle = MaterialTheme.typography.headlineMedium.copy( + textAlign = TextAlign.Center, + color = if (state.feedback != null) { + when (state.feedback) { + MemoryFlashUiState.Feedback.SUCCESS -> MaterialTheme.colorScheme.primary + MemoryFlashUiState.Feedback.FAILURE -> MaterialTheme.colorScheme.error + } + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ), + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + cursorColor = Color.Transparent + ), + visualTransformation = VisualTransformation.None, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.None + ), + placeholder = { + Text( + text = stringResource(id = R.string.memory_flash_prompt), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } ) } diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashGameScreen.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashGameScreen.kt index 22c18c6..4143084 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashGameScreen.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashGameScreen.kt @@ -119,7 +119,7 @@ fun MemoryFlashGameScreen( onDigitPressed = { digit -> viewModel.onDigitPressed(digit) }, onDelete = { viewModel.onDelete() }, onSubmit = { viewModel.submitAnswer() }, - isCompact = true + isCompact = false ) Spacer(modifier = Modifier.height(8.dp)) } diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_sequence_complete/presentation/ui/SequenceCompleteGameScreen.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_sequence_complete/presentation/ui/SequenceCompleteGameScreen.kt index dc36e95..1039ed3 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_sequence_complete/presentation/ui/SequenceCompleteGameScreen.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_sequence_complete/presentation/ui/SequenceCompleteGameScreen.kt @@ -116,7 +116,7 @@ fun SequenceCompleteGameScreen( Spacer(modifier = Modifier.height(0.dp)) } -Keypad( + Keypad( inputValue = state.inputValue, onDigitPressed = { digit -> viewModel.onDigitPressed(digit) }, onDelete = { viewModel.onDelete() }, From 479b14c560ca3eb85ba2336d5b3a14bef8f50503 Mon Sep 17 00:00:00 2001 From: simone Date: Sun, 28 Dec 2025 19:23:50 +0100 Subject: [PATCH 4/4] fix: textfield input layout & c. --- .../components/MemoryFlashChallengeCard.kt | 12 +- .../components/MemoryFlashHeader.kt | 104 ++++++++++-------- .../presentation/ui/MemoryFlashViewModel.kt | 1 + 3 files changed, 67 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt index 7c28873..bbd7500 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashChallengeCard.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -36,6 +35,9 @@ fun MemoryFlashChallengeCard(state: MemoryFlashUiState) { colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { + // Define height constants at the top of the Column scope + val textFieldHeight = 50.dp + Column( modifier = Modifier .fillMaxWidth() @@ -72,8 +74,8 @@ fun MemoryFlashChallengeCard(state: MemoryFlashUiState) { readOnly = true, modifier = Modifier .fillMaxWidth() - .wrapContentHeight(), - textStyle = MaterialTheme.typography.headlineMedium.copy( + .height(textFieldHeight), + textStyle = MaterialTheme.typography.titleMedium.copy( textAlign = TextAlign.Center, color = if (state.feedback != null) { when (state.feedback) { @@ -105,8 +107,10 @@ fun MemoryFlashChallengeCard(state: MemoryFlashUiState) { ) } ) + } else { + // Invisible placeholder with the same height as TextField + Spacer(modifier = Modifier.height(textFieldHeight)) } - Spacer(modifier = Modifier.height(4.dp)) } } diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashHeader.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashHeader.kt index 44ec56e..e0541e6 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashHeader.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/components/MemoryFlashHeader.kt @@ -4,67 +4,79 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.indiewalkabout.mathbrainer.R +import eu.indiewalkabout.mathbrainer.feat_games.feat_math_op_write.presentation.components.LevelProgressBar import eu.indiewalkabout.mathbrainer.feat_games.feat_memory_flash.presentation.state.MemoryFlashUiState @Composable fun MemoryFlashHeader(state: MemoryFlashUiState) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + modifier = Modifier.fillMaxWidth() ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - Text( - text = stringResource(id = R.string.score_label, state.score), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource( - id = R.string.high_score_label, - state.highScore ?: 0 - ), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.level_label, state.level), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = stringResource(id = R.string.level_label, state.level), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = stringResource(id = R.string.lives_label, state.lives), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - } + LevelProgressBar( + currentProgress = state.challengesCompleted, + totalSegments = state.challengesPerLevel, + segmentColor = MaterialTheme.colorScheme.primary, + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier + .weight(1f) + .padding(horizontal = 16.dp) + .height(8.dp) + ) - Text( - text = stringResource( - id = R.string.memory_flash_challenges_label, - state.challengesCompleted, - state.challengesPerLevel - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Text( + text = stringResource(id = R.string.lives_label, state.lives), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.score_label, state.score), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource( + id = R.string.high_score_label, + state.highScore ?: 0 + ), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + } } } diff --git a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt index 77e984d..d5961b1 100644 --- a/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt +++ b/app/src/main/java/eu/indiewalkabout/mathbrainer/feat_games/feat_memory_flash/presentation/ui/MemoryFlashViewModel.kt @@ -214,6 +214,7 @@ class MemoryFlashViewModel @Inject constructor( private const val SCORE_PER_DIGIT = 10 private const val BASE_DELAY_PER_DIGIT_MS = 300L // Base delay per digit in milliseconds private const val MIN_TOTAL_DELAY_MS = 900L // Minimum total delay + private const val MAX_CHALLENGES_PER_LEVEL = 6 private fun calculateRevealDuration(sequenceLength: Int): Long { val calculatedDelay = sequenceLength * BASE_DELAY_PER_DIGIT_MS