Tugas Pertemuan 11 - Game Unscramble

 Tugas Pertemuan 11

Game Unscramble

Nama : Rycahaya Sri Hutomo
NRP : 5025201046
Kelas : Pemrograman Perangkat Bergerak B
Tahun : 2024

Mendownload Kode Awal


Kondisi awal aplikasi:
  • Kata acak tidak ditampilkan, tetapi di-hardcode ke "scrambleun" 
  • Tidak ada yang terjadi saat tombol diklik.

File Kode Awal

WordsData.kt

File ini berisi daftar kata yang digunakan dalam game, konstanta untuk jumlah maksimum kata per game, dan jumlah poin skor pemain untuk setiap kata yang benar.

MainActivity.kt

File ini sebagian besar berisi kode yang dihasilkan oleh template. Anda menampilkan composable GameScreen di blok setContent{}.

GameScreen.kt

Semua composable UI ditentukan dalam file GameScreen.kt. Bagian berikut memberikan panduan tentang beberapa fungsi composable.

GameStatus

GameStatus adalah fungsi composable yang menampilkan skor game di bagian bawah layar. Fungsi composable berisi composable teks di Card. Untuk saat ini, skor di-hardcode menjadi 0.

GameLayout

GameLayout adalah fungsi composable yang menampilkan fungsi game utama, yang mencakup kata acak, petunjuk game, dan kolom teks yang menerima tebakan pengguna.

GameScreen

Composable GameScreen berisi fungsi composable GameStatus dan GameLayout, judul game, jumlah kata, dan composable untuk tombol Submit dan Skip.

FinalScoreDialog

Composable FinalScoreDialog menampilkan dialog, yaitu jendela kecil yang memberikan permintaan kepada pengguna, dengan opsi untuk Play Again atau Exit game. Nanti dalam codelab ini, Anda akan menerapkan logika untuk menampilkan dialog ini di akhir game.

Menambahkan ViewModel

1. Buka build.gradle.kts (Module :app), scroll ke blok dependencies, lalu tambahkan dependensi berikut untuk ViewModel.

dependencies {
// other dependencies

    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
//...
}

2. Di paket ui, buat class/file Kotlin bernama GameViewModel. Perluas dari class ViewModel.


import androidx.lifecycle.ViewModel

class GameViewModel : ViewModel() {
}

3. Dalam paket ui, tambahkan class model untuk UI status yang disebut GameUiState. Jadikan class data dan tambahkan variabel untuk kata acak saat ini.


data class GameUiState(
   val currentScrambledWord: String = ""
)

StateFlow

1. Di class GameViewModel, tambahkan properti _uiState berikut.

import kotlinx.coroutines.flow.MutableStateFlow

// Game UI state
private val _uiState = MutableStateFlow(GameUiState())

Properti Pendukung

1. Di file GameViewModel.kt, tambahkan properti pendukung ke uiState yang bernama _uiState. Beri nama properti uiState dan berjenis StateFlow<GameUiState>.

import kotlinx.coroutines.flow.StateFlow

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

2. Setel uiState ke _uiState.asStateFlow().

import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

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

Menampilkan kata acak tanpa pola

1. Di GameViewModel, tambahkan properti bernama currentWord dari jenis String untuk menyimpan kata acak saat ini.
 
private lateinit var currentWord: String

2. Tambahkan metode bantuan untuk pilih kata acak dari daftar dan acaklah. Beri nama pickRandomWordAndShuffle() tanpa parameter input, lalu buat fungsi tersebut menampilkan String.

import com.example.unscramble.data.allWords

private fun pickRandomWordAndShuffle(): String {
   // Continue picking up a new random word until you get one that hasn't been used before
   currentWord = allWords.random()
   if (usedWords.contains(currentWord)) {
       return pickRandomWordAndShuffle()
   } else {
       usedWords.add(currentWord)
       return shuffleCurrentWord(currentWord)
   }
}

3. Di GameViewModel, tambahkan properti berikut setelah properti currentWord agar berfungsi sebagai kumpulan yang dapat diubah untuk menyimpan kata yang telah digunakan dalam game.

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

4. Tambahkan metode helper lain untuk mengacak kata saat ini yang disebut shuffleCurrentWord() yang menggunakan String dan menampilkan String yang diacak.

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)
}

5. Tambahkan fungsi bantuan untuk melakukan inisialisasi game yang disebut resetGame(). Gunakan fungsi ini nanti untuk memulai dan memulai ulang game. Pada fungsi ini, hapus semua kata dalam kumpulan usedWords, lakukan inisialisasi _uiState. Pilih kata baru untuk currentScrambledWord menggunakan pickRandomWordAndShuffle().

 
fun resetGame() {
   usedWords.clear()
   _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}

6. Tambahkan blok init ke GameViewModel dan panggil resetGame() dari blok tersebut.

init {
   resetGame()
}

Merancang Compose UI

Meneruskan Data

1. Pada fungsi GameScreen, teruskan argumen kedua dari jenis GameViewModel dengan nilai default viewModel().

import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun GameScreen(
   gameViewModel: GameViewModel = viewModel()
) {
   // ...
}

2. Pada fungsi GameScreen(), tambahkan variabel baru bernama gameUiState. Gunakan delegasi by dan panggil collectAsState() pada uiState.
 
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

@Composable
fun GameScreen(
   // ...
) {
   val gameUiState by gameViewModel.uiState.collectAsState()
   // ...
}

3. Teruskan gameUiState.currentScrambledWord ke composable GameLayout(). Anda menambahkan argumen di langkah selanjutnya, jadi abaikan error untuk saat ini.

GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   modifier = Modifier
       .fillMaxWidth()
       .wrapContentHeight()
       .padding(mediumPadding)
)

4. Tambahkan currentScrambledWord sebagai parameter lain ke fungsi composable GameLayout().

@Composable
fun GameLayout(
   currentScrambledWord: String,
   modifier: Modifier = Modifier
) {
}

5. Perbarui fungsi composable GameLayout() untuk menampilkan currentScrambledWord. Tetapkan parameter text kolom teks pertama di kolom ke currentScrambledWord.

@Composable
fun GameLayout(
   // ...
) {
   Column(
       verticalArrangement = Arrangement.spacedBy(24.dp)
   ) {
       Text(
           text = currentScrambledWord,
           ...
       )
    //...
    }
}

6. Jalankan dan bangun aplikasi. Anda akan melihat kata yang ejaannya diacak.



Menampilkan kata tebakan

1. Di file GameScreen.kt, dalam composable GameLayout(), setel onValueChange ke onUserGuessChanged dan onKeyboardDone() ke tindakan keyboard onDone. Anda dapat memperbaiki error tersebut di langkah berikutnya.

OutlinedTextField(
   value = "",
   singleLine = true,
   modifier = Modifier.fillMaxWidth(),
   onValueChange = onUserGuessChanged,
   label = { Text(stringResource(R.string.enter_your_word)) },
   isError = false,
   keyboardOptions = KeyboardOptions.Default.copy(
       imeAction = ImeAction.Done
   ),
   keyboardActions = KeyboardActions(
       onDone = { onKeyboardDone() }
   ),

2. Pada fungsi composable GameLayout(), tambahkan dua argumen lagi: lambda onUserGuessChanged menggunakan argumen String dan tidak menampilkan apa pun, serta onKeyboardDone tidak mengambil apa pun dan tidak menampilkan apa pun.

@Composable
fun GameLayout(
    currentScrambledWord: String,
    onUserGuessChanged: (String) -> Unit,
    onKeyboardDone: () -> Unit,
    modifier: Modifier = Modifier
) {
}

3. Pada panggilan fungsi GameLayout(), tambahkan argumen lambda untuk onUserGuessChanged dan onKeyboardDone.

GameLayout(
            currentScrambledWord = gameUiState.currentScrambledWord,
            onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
            onKeyboardDone = { },
)

4. Di file GameViewModel.kt, tambahkan metode bernama updateUserGuess() yang menggunakan argumen String, kata tebakan pengguna. Di dalam fungsi, perbarui userGuess dengan meneruskan guessedWord.

  fun updateUserGuess(guessedWord: String){
     userGuess = guessedWord
  }

5. Di file GameViewModel.kt, tambahkan properti var yang disebut userGuess. Gunakan mutableStateOf() agar Compose mengamati nilai ini dan menetapkan nilai awal ke "".
 
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

var userGuess by mutableStateOf("")
   private set

6. Di file GameScreen.kt, di dalam GameLayout(), tambahkan parameter String lain untuk userGuess. Tetapkan parameter value dari OutlinedTextField ke userGuess.

fun GameLayout(
   currentScrambledWord: String,
   userGuess: String,
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   modifier: Modifier = Modifier
) {
   Column(
       verticalArrangement = Arrangement.spacedBy(24.dp)
   ) {
       //...
       OutlinedTextField(
           value = userGuess,
           //..
       )
   }
}

7. Pada fungsi GameScreen, update panggilan fungsi GameLayout() untuk menyertakan parameter userGuess.

GameLayout(
            currentScrambledWord = gameUiState.currentScrambledWord,
            userGuess = gameViewModel.userGuess,
            onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
            onKeyboardDone = { },
//...
)

8. Bangun dan jalankan aplikasi Anda. Coba menebak dan masukkan kata. Kolom teks dapat menampilkan tebakan pengguna.


Memverifikasi kata tebakan dan memperbarui skor

1. Di GameViewModel, tambahkan metode lain bernama checkUserGuess().

2. Pada fungsi checkUserGuess(), tambahkan blok if else untuk memverifikasi apakah tebakan pengguna sama dengan currentWord. Reset userGuess ke string yang kosong.

fun checkUserGuess() {

   if (userGuess.equals(currentWord, ignoreCase = true)) {
   } else {
   }
   // Reset user guess
   updateUserGuess("")
}

3. Jika tebakan pengguna salah, tetapkan isGuessedWordWrong ke true. MutableStateFlow<T>. update() memperbarui MutableStateFlow.value menggunakan nilai yang ditentukan.
 
fun checkUserGuess() {

        if (userGuess.equals(currentWord, ignoreCase = true)) {
        } else {
            // User's guess is wrong, show an error
            _uiState.update { currentState ->
                currentState.copy(isGuessedWordWrong = true)
            }
        }
        // Reset user guess
        updateUserGuess("")
    }

4. Di class GameUiState, tambahkan Boolean yang disebut isGuessedWordWrong dan lakukan inisialisasi ke false.

data class GameUiState(
   val currentScrambledWord: String = "",
   val isGuessedWordWrong: Boolean = false,
)

Meneruskan callback peristiwa checkUserGuess() ke atas dari GameScreen ke ViewModel saat pengguna mengklik tombol Submit atau tombol selesai di keyboard.

1. Di file GameScreen.kt, di akhir fungsi composable GameScreen(), panggil gameViewModel.checkUserGuess() di dalam ekspresi lambda onClick dari tombol Submit.

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

2. Pada fungsi composable GameScreen(), update panggilan fungsi GameLayout() untuk meneruskan gameViewModel.checkUserGuess() dalam ekspresi lambda onKeyboardDone.
 
GameLayout(
            currentScrambledWord = gameUiState.currentScrambledWord,
            userGuess = gameViewModel.userGuess,
            onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
            onKeyboardDone = { gameViewModel.checkUserGuess() },
)

3. Pada fungsi composable GameLayout(), tambahkan parameter fungsi untuk Boolean, isGuessWrong. Tetapkan parameter isError dari OutlinedTextField ke isGuessWrong untuk menampilkan error di kolom teks jika tebakan pengguna salah.

fun GameLayout(
   currentScrambledWord: String,
   isGuessWrong: Boolean,
   userGuess: String,
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   modifier: Modifier = Modifier
) {
   Column(
       // ,...
       OutlinedTextField(
           // ...
           isError = isGuessWrong,
           keyboardOptions = KeyboardOptions.Default.copy(
               imeAction = ImeAction.Done
           ),
           keyboardActions = KeyboardActions(
               onDone = { onKeyboardDone() }
           ),
       )
}
}

4. Pada fungsi composable GameScreen(), update panggilan fungsi GameLayout() untuk meneruskan isGuessWrong.

fun GameLayout(
   currentScrambledWord: String,
   isGuessWrong: Boolean,
   userGuess: String,
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   modifier: Modifier = Modifier
) {
   Column(
       // ,...
       OutlinedTextField(
           // ...
           isError = isGuessWrong,
           keyboardOptions = KeyboardOptions.Default.copy(
               imeAction = ImeAction.Done
           ),
           keyboardActions = KeyboardActions(
               onDone = { onKeyboardDone() }
           ),
       )
}
}

5. Build dan jalankan aplikasi Anda. Masukkan tebakan yang salah dan klik Submit. Perhatikan bahwa kolom teks berubah menjadi merah, yang menunjukkan error.


6. Di file GameScreen.kt, dalam composable GameLayout(), perbarui parameter label kolom teks bergantung pada isGuessWrong sebagai berikut:

OutlinedTextField(
   // ...
   label = {
       if (isGuessWrong) {
           Text(stringResource(R.string.wrong_guess))
       } else {
           Text(stringResource(R.string.enter_your_word))
       }
   },
   // ...
)

7. Di file strings.xml, tambahkan string ke label error.

<string name="wrong_guess">Wrong Guess!</string>

8. Build dan jalankan aplikasi kembali. Masukkan tebakan yang salah dan klik Submit. Perhatikan label error.


Memperbarui skor dan jumlah kata

1. Di GameUiState, tambahkan variabel score dan lakukan inisialisasi ke nol.

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

2. Untuk memperbarui nilai skor, di GameViewModel, pada fungsi checkUserGuess(), di dalam kondisi if untuk saat tebakan pengguna benar, tingkatkan nilai score.

import com.example.unscramble.data.SCORE_INCREASE

fun checkUserGuess() {
   if (userGuess.equals(currentWord, ignoreCase = true)) {
       // User's guess is correct, increase the score
       val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
   } else {
       //...
   }
}

3. Di GameViewModel, tambahkan metode lain bernama updateGameState untuk memperbarui skor, menambah jumlah kata saat ini, dan memilih kata baru dari file WordsData.kt. Tambahkan Int yang bernama updatedScore sebagai parameter. Update variabel UI status game sebagai berikut:

private fun updateGameState(updatedScore: Int) {
   _uiState.update { currentState ->
       currentState.copy(
           isGuessedWordWrong = false,
           currentScrambledWord = pickRandomWordAndShuffle(),
           score = updatedScore
       )
   }
}

4. Pada fungsi checkUserGuess(), jika tebakan pengguna benar, lakukan panggilan ke updateGameState dengan skor yang telah diperbarui untuk menyiapkan game untuk putaran berikutnya.

fun checkUserGuess() {
   if (userGuess.equals(currentWord, ignoreCase = true)) {
       // User's guess is correct, increase the score
       // and call updateGameState() to prepare the game for next round
       val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
       updateGameState(updatedScore)
   } else {
       //...
   }
}

5. Tambahkan variabel lain untuk jumlah di GameUiState. Panggil currentWordCount dan lakukan inisialisasi ke 1.

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

6. Di file GameViewModel.kt, pada fungsi updateGameState(), tingkatkan jumlah kata seperti yang ditunjukkan di bawah. Fungsi updateGameState() dipanggil untuk menyiapkan game untuk putaran berikutnya.

private fun updateGameState(updatedScore: Int) {
   _uiState.update { currentState ->
       currentState.copy(
           //...
           currentWordCount = currentState.currentWordCount.inc(),
           )
   }
}

Skor kelulusan dan jumlah kata

1. Di file GameScreen.kt, pada fungsi composable GameLayout(), tambahkan jumlah kata sebagai argumen dan teruskan argumen format wordCount ke elemen teks.

fun GameLayout(
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   wordCount: Int,
   //...
) {
   //...

   Card(
       //...
   ) {
       Column(
           // ...
       ) {
           Text(
               //..
               text = stringResource(R.string.word_count, wordCount),
               style = typography.titleMedium,
               color = colorScheme.onPrimary
           )

// ...

}

2. Perbarui panggilan fungsi GameLayout() untuk menyertakan jumlah kata.

GameLayout(
   userGuess = gameViewModel.userGuess,
   wordCount = gameUiState.currentWordCount,
   //...
)

3. Pada fungsi composable GameScreen(), perbarui panggilan fungsi GameStatus() untuk menyertakan parameter score. Teruskan skor dari gameUiState.


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

4. Bangun dan jalankan aplikasi.

5. Masukkan kata tebakan, lalu klik Submit. Perhatikan skor dan jumlah kata yang diperbarui.

6. Klik Skip, dan perhatikan bahwa tidak ada yang terjadi.

7. Di file GameScreen.kt, pada fungsi composable GameScreen(), lakukan panggilan ke gameViewModel.skipWord() dalam ekspresi lambda onClick.

OutlinedButton(
   onClick = { gameViewModel.skipWord() },
   modifier = Modifier.fillMaxWidth()
) {
   //...
}

8. Di GameViewModel, tambahkan metode skipWord().

9. Di dalam fungsi skipWord(), lakukan panggilan ke updateGameState(), dengan meneruskan skor dan mereset tebakan pengguna.

fun skipWord() {
   updateGameState(_uiState.value.score)
   // Reset user guess
   updateUserGuess("")
}

10. Jalankan aplikasi Anda dan mainkan game-nya. Sekarang Anda akan dapat melewati kata.


Menangani putaran terakhir game

1. Di GameViewModel, tambahkan blok if-else dan pindahkan isi fungsi yang ada ke dalam blok else.

2. Tambahkan kondisi if untuk memastikan ukuran usedWords sama dengan MAX_NO_OF_WORDS.


import com.example.android.unscramble.data.MAX_NO_OF_WORDS

private fun updateGameState(updatedScore: Int) {
   if (usedWords.size == MAX_NO_OF_WORDS){
       //Last round in the game
   } else{
       // Normal round in the game
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               currentScrambledWord = pickRandomWordAndShuffle(),
               currentWordCount = currentState.currentWordCount.inc(),
               score = updatedScore
           )
       }
   }
}

3. Di dalam blok if, tambahkan flag Boolean isGameOver dan setel flag ke true untuk menunjukkan akhir game.

4. Perbarui score dan reset isGuessedWordWrong di dalam blok if. Kode berikut menunjukkan tampilan fungsi Anda:

import com.example.android.unscramble.data.MAX_NO_OF_WORDS

private fun updateGameState(updatedScore: Int) {
   if (usedWords.size == MAX_NO_OF_WORDS){
       //Last round in the game
   } else{
       // Normal round in the game
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               currentScrambledWord = pickRandomWordAndShuffle(),
               currentWordCount = currentState.currentWordCount.inc(),
               score = updatedScore
           )
       }
   }
}

5. Di GameUiState, tambahkan variabel Boolean isGameOver dan tetapkan ke false.
 
import com.example.android.unscramble.data.MAX_NO_OF_WORDS

private fun updateGameState(updatedScore: Int) {
   if (usedWords.size == MAX_NO_OF_WORDS){
       //Last round in the game
   } else{
       // Normal round in the game
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               currentScrambledWord = pickRandomWordAndShuffle(),
               currentWordCount = currentState.currentWordCount.inc(),
               score = updatedScore
           )
       }
   }
}

6. Jalankan aplikasi Anda dan mainkan game-nya. Anda tidak dapat memainkan lebih dari 10 kata.


Menampilkan dialog akhir game

1. Di file GameScreen.kt, di akhir fungsi composable GameScreen(), setelah blok Column, tambahkan kondisi if untuk memeriksa gameUiState.isGameOver.

2. Di blok if, tampilkan dialog pemberitahuan. Lakukan panggilan ke FinalScoreDialog() dengan meneruskan score dan gameViewModel.resetGame() untuk callback peristiwa onPlayAgain.

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

3. Dalam file GameViewModel.kt, panggil kembali fungsi resetGame(), lakukan inisialisasi _uiState, dan pilih kata baru.

fun resetGame() {
   usedWords.clear()
   _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}

4. Build dan jalankan aplikasi Anda.

5. Mainkan game hingga selesai, dan amati dialog pemberitahuan dengan opsi untuk Exit dari game atau Play Again. Coba opsi yang ditampilkan di dialog pemberitahuan.


Status dalam rotasi perangkat

1. Jalankan aplikasi dan putar beberapa kata. Ubah konfigurasi perangkat dari potret ke lanskap, atau sebaliknya.

2. Perhatikan bahwa data yang disimpan di UI status ViewModel dipertahankan selama perubahan konfigurasi.



Source Code

Referensi

Komentar

Postingan populer dari blog ini

Tugas Pertemuan 7 - Membuat Aplikasi Woof

Tugas Pertemuan 13 - Framework Flutter Namer App

Evaluasi Akhir Semester PPB B