Pertemuan 13 - Word Scramble

 Naufal Khairul Rizky - 5025221127


Word Scramble - Pertemuan 13

Aplikasi ini merupakan implementasi tampilan permainan "Scramble Word" sederhana menggunakan Jetpack Compose. Pemain diminta menyusun ulang huruf acak menjadi kata yang benar. UI dirancang interaktif dengan tampilan dinamis serta feedback terhadap jawaban pemain.


1. MainActivity.kt

Saat aplikasi dijalankan, fungsi onCreate() akan dipanggil:

  • enableEdgeToEdge()
    Digunakan agar aplikasi tampil secara full screen tanpa batasan margin atau padding dari sistem UI.

  • setContent { ... }
    Memanggil fungsi ScrambleGameApp() sebagai root dari tampilan aplikasi.

  • MaterialTheme
    Mengadopsi gaya desain Material 3, membuat UI lebih modern dan konsisten.


2. ScrambleGameApp()

Fungsi @Composable utama yang membangun keseluruhan tampilan aplikasi.

a. State Management

Menggunakan remember dan mutableStateOf untuk menyimpan dan mengelola status permainan:

  • currentWord: Kata acak (sudah disusun ulang hurufnya).

  • originalWord: Kata asli yang harus ditebak oleh pemain.

  • userInput: Jawaban yang diketik oleh pemain.

  • score: Skor pemain, bertambah jika jawaban benar.

  • showResult: Boolean yang menentukan apakah feedback hasil jawaban ditampilkan.

rememberCoroutineScope() digunakan untuk animasi atau efek delay pada hasil jawaban.


b. Struktur Tata Letak UI

Menggunakan Column dan Box untuk mengatur elemen-elemen secara vertikal dan terpusat.

Komponen-Komponen:

  • Scrambled Word Display
    Menampilkan kata acak (yang hurufnya sudah dikocok) dalam huruf kapital besar, biasanya di tengah layar.
    Misalnya: "RTPAOH" (untuk kata asli "PHRAOT").

  • Input Field
    TextField memungkinkan pemain mengetik tebakan mereka.

  • Button "Check Answer"
    Jika ditekan:

    • Membandingkan userInput dengan originalWord.

    • Jika benar, score bertambah dan kata baru dimunculkan.

    • Jika salah, feedback ditampilkan.

  • Result Feedback
    Teks dinamis seperti "Benar!" atau "Coba lagi!" ditampilkan berdasarkan jawaban pemain, dengan efek animasi atau warna berbeda.


3. Logika Game dan Randomisasi Kata

  • Kata-kata diambil dari daftar (listOf), misalnya: ["ANDROID", "KOTLIN", "COMPOSE"].

  • Fungsi shuffle() digunakan untuk mengacak huruf dari kata.

  • Jawaban pemain dibandingkan dengan kata asli menggunakan .equals() atau .equalsIgnoreCase() untuk memastikan kesesuaian.

  • Setelah jawaban benar, kata baru otomatis ditampilkan (reset state).




Repository: Github

Source code: 

MainActivity.kt:

package com.example.unscramble

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.unscramble.ui.GameScreen
import com.example.unscramble.ui.theme.UnscrambleTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
UnscrambleTheme {
Surface(
modifier = Modifier.fillMaxSize(),
) {
GameScreen()
}
}
}
}
}


GameViewModel.kt:

package com.example.unscramble.ui

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.example.unscramble.data.MAX_NO_OF_WORDS
import com.example.unscramble.data.SCORE_INCREASE
import com.example.unscramble.data.allWords
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

class GameViewModel : ViewModel() {

// Game UI state
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()

var userGuess by mutableStateOf("")
private set

// Set of words used in the game
private var usedWords: MutableSet<String> = mutableSetOf()
private lateinit var currentWord: String

init {
resetGame()
}

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 {
_uiState.update { currentState ->
currentState.copy(isGuessedWordWrong = true)
}
}
updateUserGuess("")
}

fun skipWord() {
updateGameState(_uiState.value.score)
updateUserGuess("")
}

private fun updateGameState(updatedScore: Int) {
if (usedWords.size == MAX_NO_OF_WORDS){
_uiState.update { currentState ->
currentState.copy(
isGuessedWordWrong = false,
score = updatedScore,
isGameOver = true
)
}
} else{
_uiState.update { currentState ->
currentState.copy(
isGuessedWordWrong = false,
currentScrambledWord = pickRandomWordAndShuffle(),
currentWordCount = currentState.currentWordCount.inc(),
score = updatedScore
)
}
}
}

private fun shuffleCurrentWord(word: String): String {
val tempWord = word.toCharArray()
tempWord.shuffle()
while (String(tempWord) == word) {
tempWord.shuffle()
}
return String(tempWord)
}

private fun pickRandomWordAndShuffle(): String {
currentWord = allWords.random()
return if (usedWords.contains(currentWord)) {
pickRandomWordAndShuffle()
} else {
usedWords.add(currentWord)
shuffleCurrentWord(currentWord)
}
}
}


GameUIState.kt:

package com.example.unscramble.ui

data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 1,
val score: Int = 0,
val isGuessedWordWrong: Boolean = false,
val isGameOver: Boolean = false
)


GameScreen.kt:

package com.example.unscramble.ui

import android.app.Activity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.unscramble.R
import com.example.unscramble.ui.theme.UnscrambleTheme

@Composable
fun GameScreen(gameViewModel: GameViewModel = viewModel()) {
val gameUiState by gameViewModel.uiState.collectAsState()
val mediumPadding = dimensionResource(R.dimen.padding_medium)

Column(
modifier = Modifier
.statusBarsPadding()
.verticalScroll(rememberScrollState())
.safeDrawingPadding()
.padding(mediumPadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {

Text(
text = stringResource(R.string.app_name),
style = typography.titleLarge,
)
GameLayout(
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
wordCount = gameUiState.currentWordCount,
userGuess = gameViewModel.userGuess,
onKeyboardDone = { gameViewModel.checkUserGuess() },
currentScrambledWord = gameUiState.currentScrambledWord,
isGuessWrong = gameUiState.isGuessedWordWrong,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(mediumPadding)
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(mediumPadding),
verticalArrangement = Arrangement.spacedBy(mediumPadding),
horizontalAlignment = Alignment.CenterHorizontally
) {

Button(
modifier = Modifier.fillMaxWidth(),
onClick = { gameViewModel.checkUserGuess() }
) {
Text(
text = stringResource(R.string.submit),
fontSize = 16.sp
)
}

OutlinedButton(
onClick = { gameViewModel.skipWord() },
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.skip),
fontSize = 16.sp
)
}
}

GameStatus(score = gameUiState.score, modifier = Modifier.padding(20.dp))

if (gameUiState.isGameOver) {
FinalScoreDialog(
score = gameUiState.score,
onPlayAgain = { gameViewModel.resetGame() }
)
}
}
}

@Composable
fun GameStatus(score: Int, modifier: Modifier = Modifier) {
Card(
modifier = modifier
) {
Text(
text = stringResource(R.string.score, score),
style = typography.headlineMedium,
modifier = Modifier.padding(8.dp)
)

}
}

@Composable
fun GameLayout(
currentScrambledWord: String,
wordCount: Int,
isGuessWrong: Boolean,
userGuess: String,
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
modifier: Modifier = Modifier
) {
val mediumPadding = dimensionResource(R.dimen.padding_medium)

Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(mediumPadding),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(mediumPadding)
) {
Text(
modifier = Modifier
.clip(shapes.medium)
.background(colorScheme.surfaceTint)
.padding(horizontal = 10.dp, vertical = 4.dp)
.align(alignment = Alignment.End),
text = stringResource(R.string.word_count, wordCount),
style = typography.titleMedium,
color = colorScheme.onPrimary
)
Text(
text = currentScrambledWord,
style = typography.displayMedium
)
Text(
text = stringResource(R.string.instructions),
textAlign = TextAlign.Center,
style = typography.titleMedium
)
OutlinedTextField(
value = userGuess,
singleLine = true,
shape = shapes.large,
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedContainerColor = colorScheme.surface,
unfocusedContainerColor = colorScheme.surface,
disabledContainerColor = colorScheme.surface,
),
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 = { onKeyboardDone() }
)
)
}
}
}

@Composable
private fun FinalScoreDialog(
score: Int,
onPlayAgain: () -> Unit,
modifier: Modifier = Modifier
) {
val activity = (LocalContext.current as Activity)

AlertDialog(
onDismissRequest = {

},
title = { Text(text = stringResource(R.string.congratulations)) },
text = { Text(text = stringResource(R.string.you_scored, score)) },
modifier = modifier,
dismissButton = {
TextButton(
onClick = {
activity.finish()
}
) {
Text(text = stringResource(R.string.exit))
}
},
confirmButton = {
TextButton(onClick = onPlayAgain) {
Text(text = stringResource(R.string.play_again))
}
}
)
}

@Preview(showBackground = true)
@Composable
fun GameScreenPreview() {
UnscrambleTheme {
GameScreen()
}
}
















Komentar