diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df95f35..8501ec3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,4 +76,6 @@ dependencies { debugImplementation("androidx.compose.ui:ui-test-manifest") debugImplementation("androidx.compose.ui:ui-tooling") + + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") } diff --git a/app/src/main/java/com/example/unscramble/ui/GameScreen.kt b/app/src/main/java/com/example/unscramble/ui/GameScreen.kt index c526bef..f57ea4a 100644 --- a/app/src/main/java/com/example/unscramble/ui/GameScreen.kt +++ b/app/src/main/java/com/example/unscramble/ui/GameScreen.kt @@ -54,11 +54,20 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.unscramble.R import com.example.unscramble.ui.theme.UnscrambleTheme +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue + + @Composable -fun GameScreen() { +fun GameScreen(gameViewModel: GameViewModel = viewModel()) { + + val gameUiState by gameViewModel.uiState.collectAsState() val mediumPadding = dimensionResource(R.dimen.padding_medium) + + Column( modifier = Modifier .statusBarsPadding() @@ -74,6 +83,12 @@ fun GameScreen() { style = typography.titleLarge, ) GameLayout( + onUserGuessChanged = { gameViewModel.updateUserGuess(it) }, + onKeyboardDone = { gameViewModel.checkUserGuess() }, + userGuess = gameViewModel.userGuess, + wordCount = gameUiState.currentWordCount, + currentScrambledWord = gameUiState.currentScrambledWord, + isGuessWrong = gameUiState.isGuessedWordWrong, modifier = Modifier .fillMaxWidth() .wrapContentHeight() @@ -87,9 +102,17 @@ fun GameScreen() { horizontalAlignment = Alignment.CenterHorizontally ) { + if (gameUiState.isGameOver) { + FinalScoreDialog( + score = gameUiState.score, + onPlayAgain = { gameViewModel.resetGame() } + ) + } + + Button( modifier = Modifier.fillMaxWidth(), - onClick = { } + onClick = { gameViewModel.checkUserGuess() } ) { Text( text = stringResource(R.string.submit), @@ -98,7 +121,7 @@ fun GameScreen() { } OutlinedButton( - onClick = { }, + onClick = { gameViewModel.skipWord() }, modifier = Modifier.fillMaxWidth() ) { Text( @@ -108,7 +131,7 @@ fun GameScreen() { } } - GameStatus(score = 0, modifier = Modifier.padding(20.dp)) + GameStatus(score = gameUiState.score, modifier = Modifier.padding(20.dp)) } } @@ -126,9 +149,17 @@ fun GameStatus(score: Int, modifier: Modifier = Modifier) { } @Composable -fun GameLayout(modifier: Modifier = Modifier) { +fun GameLayout( + onUserGuessChanged: (String) -> Unit, + onKeyboardDone: () -> Unit, + wordCount: Int, + userGuess: String, + currentScrambledWord: String, + isGuessWrong: Boolean, + modifier: Modifier = Modifier) { val mediumPadding = dimensionResource(R.dimen.padding_medium) + Card( modifier = modifier, elevation = CardDefaults.cardElevation(defaultElevation = 5.dp) @@ -144,21 +175,24 @@ fun GameLayout(modifier: Modifier = Modifier) { .background(colorScheme.surfaceTint) .padding(horizontal = 10.dp, vertical = 4.dp) .align(alignment = Alignment.End), - text = stringResource(R.string.word_count, 0), + text = stringResource(R.string.word_count, wordCount), style = typography.titleMedium, color = colorScheme.onPrimary ) Text( - text = "scrambleun", + text = currentScrambledWord, // style = typography.displayMedium ) + Text( text = stringResource(R.string.instructions), textAlign = TextAlign.Center, style = typography.titleMedium + + ) OutlinedTextField( - value = "", + value = userGuess, singleLine = true, shape = shapes.large, modifier = Modifier.fillMaxWidth(), @@ -167,14 +201,18 @@ fun GameLayout(modifier: Modifier = Modifier) { unfocusedContainerColor = colorScheme.surface, disabledContainerColor = colorScheme.surface, ), - onValueChange = { }, - label = { Text(stringResource(R.string.enter_your_word)) }, - isError = false, + onValueChange = onUserGuessChanged, + label = { if (isGuessWrong) { + Text(stringResource(R.string.wrong_guess)) + } else { + Text(stringResource(R.string.enter_your_word)) + } }, + isError = isGuessWrong, keyboardOptions = KeyboardOptions.Default.copy( imeAction = ImeAction.Done ), keyboardActions = KeyboardActions( - onDone = { } + onDone = { onKeyboardDone() } ) ) } diff --git a/app/src/main/java/com/example/unscramble/ui/GameUiState.kt b/app/src/main/java/com/example/unscramble/ui/GameUiState.kt new file mode 100644 index 0000000..e9ba4c0 --- /dev/null +++ b/app/src/main/java/com/example/unscramble/ui/GameUiState.kt @@ -0,0 +1,10 @@ +package com.example.unscramble.ui + +data class GameUiState( + val currentScrambledWord: String ="", + val currentWordCount: Int = 1, + val isGuessedWordWrong: Boolean = false, + val score: Int = 0, + val isGameOver: Boolean = false + +) diff --git a/app/src/main/java/com/example/unscramble/ui/GameViewModel.kt b/app/src/main/java/com/example/unscramble/ui/GameViewModel.kt new file mode 100644 index 0000000..697abdc --- /dev/null +++ b/app/src/main/java/com/example/unscramble/ui/GameViewModel.kt @@ -0,0 +1,115 @@ +package com.example.unscramble.ui +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import com.example.unscramble.data.allWords + +import kotlinx.coroutines.flow.asStateFlow +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import kotlinx.coroutines.flow.update +import com.example.unscramble.data.SCORE_INCREASE +import com.example.unscramble.data.MAX_NO_OF_WORDS + + +class GameViewModel : ViewModel() { + + var userGuess by mutableStateOf("") + private set + + // Game UI state + private val _uiState = MutableStateFlow(GameUiState()) + + // Read-only state + val uiState: StateFlow = _uiState.asStateFlow() + + private lateinit var currentWord: String + private var usedWords: MutableSet = mutableSetOf() + + private fun pickRandomWordAndShuffle(): String { + currentWord = allWords.random() + if (usedWords.contains(currentWord)) { + return pickRandomWordAndShuffle() + } else { + usedWords.add(currentWord) + return shuffleCurrentWord(currentWord) + } + } + + private fun shuffleCurrentWord(word: String): String { + val tempWord = word.toCharArray() + //Scramble the word + tempWord.shuffle() + while (String(tempWord).equals(word)) { + tempWord.shuffle() + } + return String(tempWord) + } + + fun resetGame() { + usedWords.clear() + _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle()) + } + + fun updateUserGuess(guessedWord: String) { + userGuess = guessedWord + } + + + fun checkUserGuess() { + + if (userGuess.equals(currentWord, ignoreCase = true)) { + val updatedScore = _uiState.value.score.plus(SCORE_INCREASE) + updateGameState(updatedScore) + } else { + // User's guess is wrong, show an error + _uiState.update { currentState -> + currentState.copy(isGuessedWordWrong = true) + } + // Reset user guess + updateUserGuess("") + } + + + } + + private fun updateGameState(updatedScore: Int) { + if (usedWords.size == MAX_NO_OF_WORDS) { + //Last round in the game, update isGameOver to true, don't pick a new word + _uiState.update { currentState -> + currentState.copy( + isGuessedWordWrong = false, + score = updatedScore, + isGameOver = true + ) + } + } else { + // Normal round in the game + _uiState.update { currentState -> + currentState.copy( + isGuessedWordWrong = false, + currentScrambledWord = pickRandomWordAndShuffle(), + currentWordCount = currentState.currentWordCount.inc(), + score = updatedScore + ) + } + } + + + + } + + fun skipWord() { + updateGameState(_uiState.value.score) + + //Reset user guess + updateUserGuess("") + } + + + + init { + resetGame() + } +} \ No newline at end of file