diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 0000000..6df7e9c --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,12 @@ +# Project Guidelines + +## Project Structure +This is a Kotlin Multiplatform project with Compose Multiplatform that includes: +* `composeApp` - Shared Kotlin code with Compose UI +* `iosApp` - iOS application + +## Building the Project +When building this project, Junie should use the following Gradle task: +``` +:shared:compileDebugSources +``` diff --git a/README.md b/README.md index 7ad5846..8bdf46e 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,4 @@ Any updates to `boardStatus` or `boardGuesses` will trigger our SwiftUI UI to be * StarWars (https://github.com/joreilly/StarWars) * WordMasterKMP (https://github.com/joreilly/WordMasterKMP) * Chip-8 (https://github.com/joreilly/chip-8) +* FirebaseAILogicKMPSample (https://github.com/joreilly/FirebaseAILogicKMPSample) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index b00f9a5..8c2bad6 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -31,10 +31,6 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - - kotlinOptions { - jvmTarget = "17" - } } diff --git a/androidApp/src/main/java/dev/johnoreilly/wordmaster/androidApp/MainActivity.kt b/androidApp/src/main/java/dev/johnoreilly/wordmaster/androidApp/MainActivity.kt index 39907ab..9c354c8 100644 --- a/androidApp/src/main/java/dev/johnoreilly/wordmaster/androidApp/MainActivity.kt +++ b/androidApp/src/main/java/dev/johnoreilly/wordmaster/androidApp/MainActivity.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBar import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -29,8 +30,17 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type import dev.johnoreilly.wordmaster.shared.LetterStatus import dev.johnoreilly.wordmaster.shared.WordMasterService import dev.johnoreilly.wordmaster.androidApp.theme.WordMasterTheme @@ -55,7 +65,7 @@ fun MainLayout() { Scaffold( topBar = { WordMasterTopAppBar("WordMaster KMP") } ) { innerPadding -> - WordMasterView(Modifier.padding(innerPadding)) + WordMasterView(Modifier.padding(innerPadding).imePadding()) } } @@ -71,18 +81,20 @@ fun WordMasterView(padding: Modifier) { val boardGuesses by wordMasterService.boardGuesses.collectAsState() val boardStatus by wordMasterService.boardStatus.collectAsState() + val revealedAnswer by wordMasterService.revealedAnswer.collectAsState() + val lastGuessCorrect by wordMasterService.lastGuessCorrect.collectAsState() val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } - Row(padding.fillMaxSize().padding(16.dp), horizontalArrangement = Center) { + Row(padding.fillMaxSize().padding(16.dp), horizontalArrangement = Center, verticalAlignment = Alignment.CenterVertically) { Column(horizontalAlignment = Alignment.CenterHorizontally) { for (guessAttempt in 0 until WordMasterService.MAX_NUMBER_OF_GUESSES) { - Row(horizontalArrangement = Arrangement.SpaceBetween) { + Row(horizontalArrangement = Arrangement.Center) { for (character in 0 until WordMasterService.NUMBER_LETTERS) { Column( - Modifier.padding(4.dp).background(Color.White).border(1.dp, Color.Black), + Modifier.padding(4.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -100,14 +112,64 @@ fun WordMasterView(padding: Modifier) { character, it.uppercase() ) - focusManager.moveFocus(FocusDirection.Next) + if (it.isNotEmpty() && character < WordMasterService.NUMBER_LETTERS - 1) { + // Only move within the same row; don't advance to the next row until the guess is submitted + focusManager.moveFocus(FocusDirection.Next) + } } }, - modifier = modifier, + modifier = modifier.onKeyEvent { + if (it.type == KeyEventType.KeyUp && (it.key == Key.Enter || it.key == Key.NumPadEnter)) { + if (guessAttempt == wordMasterService.currentGuessAttempt) { + var filled = true + for (c in 0 until WordMasterService.NUMBER_LETTERS) { + if (boardGuesses[guessAttempt][c].isEmpty()) { filled = false; break } + } + if (filled) { + wordMasterService.checkGuess() + // After submitting a guess, move focus to the next row's first cell + focusManager.moveFocus(FocusDirection.Next) + return@onKeyEvent true + } + } + } + false + } + .border(1.dp, Color.Black.copy(alpha = 0.6f), androidx.compose.foundation.shape.RoundedCornerShape(10.dp)), + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Characters, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + if (guessAttempt == wordMasterService.currentGuessAttempt) { + var filled = true + for (c in 0 until WordMasterService.NUMBER_LETTERS) { + if (boardGuesses[guessAttempt][c].isEmpty()) { filled = false; break } + } + if (filled) { + wordMasterService.checkGuess() + // After submitting a guess, move focus to the next row's first cell + focusManager.moveFocus(FocusDirection.Next) + } + } + } + ), textStyle = TextStyle(fontSize = 14.sp, textAlign = TextAlign.Center), + shape = androidx.compose.foundation.shape.RoundedCornerShape(10.dp), colors = TextFieldDefaults.colors( + focusedTextColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]), unfocusedTextColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]), + disabledTextColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]), + cursorColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]), + focusedContainerColor = mapLetterStatusToBackgroundColor(boardStatus[guessAttempt][character]), unfocusedContainerColor = mapLetterStatusToBackgroundColor(boardStatus[guessAttempt][character]), + disabledContainerColor = mapLetterStatusToBackgroundColor(boardStatus[guessAttempt][character]), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, ), ) @@ -121,9 +183,28 @@ fun WordMasterView(padding: Modifier) { } Spacer(Modifier.height(16.dp)) + + if (revealedAnswer != null) { + Text( + text = "Answer: $revealedAnswer", + style = TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface) + ) + Spacer(Modifier.height(12.dp)) + } + Row(horizontalArrangement = Arrangement.Center) { Button(onClick = { - wordMasterService.checkGuess() + // Only submit and advance focus if the current row is filled + val current = wordMasterService.currentGuessAttempt + var filled = true + for (c in 0 until WordMasterService.NUMBER_LETTERS) { + if (boardGuesses[current][c].isEmpty()) { filled = false; break } + } + if (filled) { + wordMasterService.checkGuess() + // Move focus to next row's first cell + focusManager.moveFocus(FocusDirection.Next) + } }) { Text("Guess") } @@ -135,6 +216,23 @@ fun WordMasterView(padding: Modifier) { Text("New Game") } } + + if (lastGuessCorrect) { + androidx.compose.material3.AlertDialog( + onDismissRequest = { /* keep dialog until OK pressed */ }, + title = { Text("You win!") }, + text = { Text("Great job guessing the word.") }, + confirmButton = { + Button(onClick = { + wordMasterService.resetGame() + // Re-focus first cell after reset + focusRequester.requestFocus() + }) { + Text("OK") + } + } + ) + } } } diff --git a/compose-desktop/src/main/kotlin/main.kt b/compose-desktop/src/main/kotlin/main.kt index 2c1595d..b829194 100644 --- a/compose-desktop/src/main/kotlin/main.kt +++ b/compose-desktop/src/main/kotlin/main.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color.Companion.White import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign @@ -29,7 +30,7 @@ import dev.johnoreilly.wordmaster.shared.LetterStatus fun main() = singleWindowApplication( title = "WordMaster KMP", - state = WindowState(size = DpSize(320.dp, 500.dp)) + state = WindowState(size = DpSize(460.dp, 700.dp)) ) { WordMasterView() } @@ -41,16 +42,41 @@ fun WordMasterView() { val boardGuesses by wordMasterService.boardGuesses.collectAsState() val boardStatus by wordMasterService.boardStatus.collectAsState() + val revealedAnswer by wordMasterService.revealedAnswer.collectAsState() + val lastGuessCorrect by wordMasterService.lastGuessCorrect.collectAsState() val focusManager = LocalFocusManager.current - val focusRequester = remember { FocusRequester() } + // FocusRequesters for the first cell of each row to allow precise focusing after submission + val rowFirstCellRequesters = remember { List(WordMasterService.MAX_NUMBER_OF_GUESSES) { FocusRequester() } } - Row(Modifier.fillMaxSize().padding(16.dp)) { + // Ensure focus shifts to the first cell of the current row after a guess submission/recomposition + val currentAttempt = wordMasterService.currentGuessAttempt + LaunchedEffect(currentAttempt) { + if (currentAttempt in 0 until WordMasterService.MAX_NUMBER_OF_GUESSES) { + // Post-recomposition, request focus on the first cell of the active row + rowFirstCellRequesters[currentAttempt].requestFocus() + } + } + + Box(Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) { - Column(Modifier.onKeyEvent { + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.onKeyEvent { if (it.key == Key.Enter) { - wordMasterService.checkGuess() - true + // Submit only if current row is fully filled + val current = wordMasterService.currentGuessAttempt + var filled = true + for (c in 0 until WordMasterService.NUMBER_LETTERS) { + if (boardGuesses[current][c].isEmpty()) { filled = false; break } + } + if (filled) { + wordMasterService.checkGuess() + // Move focus explicitly to next row’s first cell + val nextRow = current + 1 + if (nextRow < WordMasterService.MAX_NUMBER_OF_GUESSES) { + rowFirstCellRequesters[nextRow].requestFocus() + } + true + } else false } else if (it.key == Key.Backspace) { focusManager.moveFocus(FocusDirection.Previous) true @@ -63,61 +89,112 @@ fun WordMasterView() { for (character in 0 until WordMasterService.NUMBER_LETTERS) { Column( - Modifier.padding(4.dp).background(White).border(1.dp, Black), + Modifier.padding(4.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - val modifier = if (guessAttempt == 0 && character == 0) { - Modifier.width(50.dp).focusRequester(focusRequester) - } else { - Modifier.width(50.dp) + var modifier = Modifier + .padding(2.dp) + .width(64.dp) + .height(64.dp) + if (character == 0) { + modifier = modifier.focusRequester(rowFirstCellRequesters[guessAttempt]) } TextField( + enabled = guessAttempt == wordMasterService.currentGuessAttempt, value = boardGuesses[guessAttempt][character], onValueChange = { - if (it.length <= 1 && guessAttempt == wordMasterService.currentGuessAttempt) { - wordMasterService.setGuess( - guessAttempt, - character, - it.uppercase() - ) - focusManager.moveFocus(FocusDirection.Next) + if (guessAttempt == wordMasterService.currentGuessAttempt) { + val capped = it.take(1).uppercase() + val current = boardGuesses[guessAttempt][character] + if (capped != current) { + wordMasterService.setGuess( + guessAttempt, + character, + capped + ) + if (capped.isNotEmpty() && character < WordMasterService.NUMBER_LETTERS - 1) { + // Advance within the row only + focusManager.moveFocus(FocusDirection.Next) + } + } } }, - modifier = modifier, + modifier = modifier.border(1.dp, Black.copy(alpha = 0.6f), RoundedCornerShape(10.dp)), + singleLine = true, textStyle = TextStyle(fontSize = 20.sp, textAlign = TextAlign.Center), colors = TextFieldDefaults.textFieldColors( textColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]), backgroundColor = mapLetterStatusToBackgroundColor(boardStatus[guessAttempt][character]), + disabledTextColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]), unfocusedIndicatorColor = Transparent, - focusedIndicatorColor = Transparent + focusedIndicatorColor = Transparent, + disabledIndicatorColor = Transparent, ) ) - DisposableEffect(Unit) { - focusRequester.requestFocus() - onDispose { } + if (guessAttempt == 0 && character == 0) { + DisposableEffect(Unit) { + rowFirstCellRequesters[0].requestFocus() + onDispose { } + } } } } } } - Row { + Spacer(Modifier.height(16.dp)) + + if (revealedAnswer != null) { + Text("Answer: ${'$'}revealedAnswer", style = TextStyle(fontSize = 18.sp)) + Spacer(Modifier.height(12.dp)) + } + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { Button(onClick = { - wordMasterService.checkGuess() + // Only submit if current row is filled + val current = wordMasterService.currentGuessAttempt + var filled = true + for (c in 0 until WordMasterService.NUMBER_LETTERS) { + if (boardGuesses[current][c].isEmpty()) { filled = false; break } + } + if (filled) { + wordMasterService.checkGuess() + // Move focus explicitly to next row’s first cell + val nextRow = current + 1 + if (nextRow < WordMasterService.MAX_NUMBER_OF_GUESSES) { + rowFirstCellRequesters[nextRow].requestFocus() + } + } }) { Text("Guess") } Spacer(Modifier.width(16.dp)) Button(onClick = { wordMasterService.resetGame() - focusRequester.requestFocus() + rowFirstCellRequesters[0].requestFocus() }) { Text("New Game") } } + + if (lastGuessCorrect) { + AlertDialog( + onDismissRequest = {}, + title = { Text("You win!") }, + text = { Text("Great job guessing the word.") }, + confirmButton = { + Button(onClick = { + wordMasterService.resetGame() + rowFirstCellRequesters[0].requestFocus() + }) { + Text("OK") + } + } + ) + } } } @@ -126,8 +203,8 @@ fun WordMasterView() { fun mapLetterStatusToBackgroundColor(letterStatus: LetterStatus): Color { return when (letterStatus) { LetterStatus.UNGUESSED -> White - LetterStatus.CORRECT_POSITION -> Color(0xFF008000) - LetterStatus.INCORRECT_POSITION -> Color(0xFF09B870C) + LetterStatus.CORRECT_POSITION -> Color(0xFF2E7D32) + LetterStatus.INCORRECT_POSITION -> Color(0xFF9B870C) LetterStatus.NOT_IN_WORD -> Gray } } @@ -136,7 +213,7 @@ fun mapLetterStatusToTextColor(letterStatus: LetterStatus): Color { return when (letterStatus) { LetterStatus.UNGUESSED -> Black LetterStatus.CORRECT_POSITION -> White - LetterStatus.INCORRECT_POSITION -> White + LetterStatus.INCORRECT_POSITION -> Black LetterStatus.NOT_IN_WORD -> White } } diff --git a/gradle.properties b/gradle.properties index a06dffb..fd28f15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,15 @@ #Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8 -XX:+UseParallelGC +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true +org.gradle.daemon=true +org.gradle.unsafe.configuration-cache=true kotlin.code.style=official android.useAndroidX=true kotlin.native.binary.memoryModel=experimental -kotlin.native.binary.freezing=disabled kotlin.mpp.stability.nowarn=true kotlin.mpp.androidSourceSetLayoutVersion=2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75cefb9..cd4ea06 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,17 +4,19 @@ ksp = "2.2.0-2.0.2" kotlinx-coroutines = "1.10.2" -agp = "8.11.1" +agp = "8.12.0" android-compileSdk = "36" android-minSdk = "24" android-targetSdk = "36" androidx-activityCompose = "1.10.1" -androidxComposeBom = "2025.07.00" +androidxComposeBom = "2025.08.00" compose-plugin = "1.8.2" kmp-nativecoroutines = "1.0.0-ALPHA-45" -okio = "3.15.0" +okio = "3.16.0" +junit = "4.13.2" [libraries] +junit = { module = "junit:junit", version.ref = "junit" } kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 87cb60d..dbf75c6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Jan 21 13:46:25 GMT 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 4d28b82..c582e61 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -4,55 +4,146 @@ import KMPNativeCoroutinesAsync struct ContentView: View { @StateObject private var viewModel = ViewModel() + + // Focus handling for per-cell focus movement + private struct FocusPos: Hashable { let row: Int; let col: Int } + @FocusState private var focusedPos: FocusPos? + @State private var showWinAlert: Bool = false var body: some View { NavigationView { - VStack { - ForEach(0 ..< viewModel.getMaxNumberGuesses()) { guessNumber in - HStack { - ForEach(0 ..< viewModel.getMaxNumberLetters()) { character in - + VStack(spacing: 16) { + ForEach(0 ..< viewModel.getMaxNumberGuesses(), id: \.self) { guessNumber in + HStack(spacing: 8) { + ForEach(0 ..< viewModel.getMaxNumberLetters(), id: \.self) { character in let guessBinding = Binding( get: { viewModel.getGuess(guessAttempt: guessNumber, character: character) }, - set: { - if ($0.count <= 1) { - viewModel.setGuess(guessAttempt: guessNumber, character: character, guess: $0) + set: { newValue in + // Force uppercase and limit to first character + let upper = newValue.uppercased() + let capped = String(upper.prefix(1)) + + if capped != viewModel.getGuess(guessAttempt: guessNumber, character: character) { + viewModel.setGuess(guessAttempt: guessNumber, character: character, guess: capped) + + // Move focus to the next cell when a single character is entered + if !capped.isEmpty { + let nextCol = character + 1 + if nextCol < viewModel.getMaxNumberLetters() { + // Advance to next column in same row + DispatchQueue.main.async { + focusedPos = FocusPos(row: guessNumber, col: nextCol) + } + } else { + // Optionally keep focus or move to next row's first cell; we'll keep it here + } + } } } ) - - TextField("", text: guessBinding, onCommit: { - - }) - .frame(maxWidth: 40, alignment: .center) - .padding([.trailing, .leading], 10) - .padding([.vertical], 15) - .lineLimit(1) - .multilineTextAlignment(.center) - .border(.black) - .background(viewModel.getLetterStatusBackgroundColor(guessAttempt: guessNumber, character: character)) + TextField("", text: guessBinding) + .textInputAutocapitalization(.characters) + .disableAutocorrection(true) + .font(.system(size: 20, weight: .semibold, design: .monospaced)) + .multilineTextAlignment(.center) + .frame(width: 56, height: 56) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(viewModel.getLetterStatusBackgroundColor(guessAttempt: guessNumber, character: character)) + ) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.black.opacity(0.6), lineWidth: 1) + ) + .focused($focusedPos, equals: FocusPos(row: guessNumber, col: character)) + .submitLabel(.done) + .onSubmit { + // If current row is fully filled, trigger Guess + if guessNumber == viewModel.getCurrentGuessAttempt() { + let maxLetters = viewModel.getMaxNumberLetters() + var filled = true + for col in 0.. Int { @@ -47,6 +69,9 @@ class ViewModel: ObservableObject { return Int(WordMasterService.companion.NUMBER_LETTERS) } + func getCurrentGuessAttempt() -> Int { + return Int(wordMasterService.currentGuessAttempt) + } func setGuess(guessAttempt: Int, character: Int, guess: String) { wordMasterService.setGuess(guessAttempt: Int32(guessAttempt), character: Int32(character), guess: guess) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index b81e2bb..f107282 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -29,13 +29,6 @@ kotlin { implementation(libs.kotlinx.coroutines.test) implementation(kotlin("test")) } - - val androidUnitTest by getting { - dependencies { - implementation(kotlin("test-junit")) - implementation("junit:junit:4.13.2") - } - } } } @@ -43,7 +36,6 @@ android { namespace = "dev.johnoreilly.wordmaster.shared" compileSdk = libs.versions.android.compileSdk.get().toInt() - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { minSdk = libs.versions.android.minSdk.get().toInt() } diff --git a/shared/src/androidMain/AndroidManifest.xml b/shared/src/androidMain/AndroidManifest.xml deleted file mode 100644 index de749ac..0000000 --- a/shared/src/androidMain/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/johnoreilly/wordmaster/shared/WordMasterService.kt b/shared/src/commonMain/kotlin/dev/johnoreilly/wordmaster/shared/WordMasterService.kt index 367a138..12b07f4 100644 --- a/shared/src/commonMain/kotlin/dev/johnoreilly/wordmaster/shared/WordMasterService.kt +++ b/shared/src/commonMain/kotlin/dev/johnoreilly/wordmaster/shared/WordMasterService.kt @@ -33,6 +33,12 @@ class WordMasterService(wordsFilePath: String) { @NativeCoroutines val boardStatus: MutableStateFlow>> = MutableStateFlow>>(arrayListOf()) + @NativeCoroutines + val revealedAnswer: MutableStateFlow = MutableStateFlow(null) + + @NativeCoroutines + val lastGuessCorrect: MutableStateFlow = MutableStateFlow(false) + init { println("wordsFilePath = $wordsFilePath") @@ -44,6 +50,8 @@ class WordMasterService(wordsFilePath: String) { currentGuessAttempt = 0 answer = validWords.random().uppercase() println("answer! = $answer") + revealedAnswer.value = null + lastGuessCorrect.value = false // set default values for guesses/letter status info val newBoardStatus = arrayListOf>() @@ -86,7 +94,16 @@ class WordMasterService(wordsFilePath: String) { currentStatusCopy[currentGuessAttempt] = status boardStatus.value = currentStatusCopy + val isCorrect = status.all { it == CORRECT_POSITION } + if ( isCorrect ) { + lastGuessCorrect.value = true + } currentGuessAttempt++ + + // Reveal the answer if all guesses are completed and the word wasn't guessed + if (!isCorrect && currentGuessAttempt >= MAX_NUMBER_OF_GUESSES) { + revealedAnswer.value = answer + } } }