Tugas 10 - ScrambleWord

 Nama : Widian Sasi Disertasiani

NRP : 5025211024

Kelas : PPB D 

Materi : Membuat Aplikasi ScrambleWord


Github: Code

Yt: Demo




Code:

package com.example.unscramble.ui


import android.app.Activity

import androidx.compose.animation.AnimatedVisibility

import androidx.compose.animation.core.Animatable

import androidx.compose.animation.core.FastOutSlowInEasing

import androidx.compose.animation.core.LinearEasing

import androidx.compose.animation.core.RepeatMode

import androidx.compose.animation.core.animateFloat

import androidx.compose.animation.core.infiniteRepeatable

import androidx.compose.animation.core.rememberInfiniteTransition

import androidx.compose.animation.core.tween

import androidx.compose.animation.fadeIn

import androidx.compose.animation.fadeOut

import androidx.compose.animation.scaleIn

import androidx.compose.animation.scaleOut

import androidx.compose.foundation.background

import androidx.compose.foundation.border

import androidx.compose.foundation.clickable

import androidx.compose.foundation.layout.Arrangement

import androidx.compose.foundation.layout.Box

import androidx.compose.foundation.layout.Column

import androidx.compose.foundation.layout.Row

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.safeDrawingPadding

import androidx.compose.foundation.layout.size

import androidx.compose.foundation.layout.statusBarsPadding

import androidx.compose.foundation.layout.width

import androidx.compose.foundation.layout.wrapContentHeight

import androidx.compose.foundation.rememberScrollState

import androidx.compose.foundation.shape.CircleShape

import androidx.compose.foundation.text.KeyboardActions

import androidx.compose.foundation.text.KeyboardOptions

import androidx.compose.foundation.verticalScroll

import androidx.compose.material.icons.Icons

import androidx.compose.material.icons.filled.Favorite

import androidx.compose.material.icons.filled.Refresh

import androidx.compose.material.icons.filled.Star

import androidx.compose.material3.AlertDialog

import androidx.compose.material3.Button

import androidx.compose.material3.ButtonDefaults

import androidx.compose.material3.Card

import androidx.compose.material3.CardDefaults

import androidx.compose.material3.Icon

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.LaunchedEffect

import androidx.compose.runtime.collectAsState

import androidx.compose.runtime.getValue

import androidx.compose.runtime.mutableStateOf

import androidx.compose.runtime.remember

import androidx.compose.runtime.setValue

import androidx.compose.ui.Alignment

import androidx.compose.ui.Modifier

import androidx.compose.ui.draw.clip

import androidx.compose.ui.draw.rotate

import androidx.compose.ui.draw.scale

import androidx.compose.ui.graphics.Brush

import androidx.compose.ui.graphics.Color

import androidx.compose.ui.platform.LocalContext

import androidx.compose.ui.res.dimensionResource

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.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

import kotlinx.coroutines.delay


@Composable

fun GameScreen(gameViewModel: GameViewModel = viewModel()) {

    val gameUiState by gameViewModel.uiState.collectAsState()

    val mediumPadding = dimensionResource(R.dimen.padding_medium)


    // Animation states

    var showSuccessAnimation by remember { mutableStateOf(false) }

    var showErrorAnimation by remember { mutableStateOf(false) }

    val infiniteTransition = rememberInfiniteTransition(label = "sparkle")

    val sparkleRotation by infiniteTransition.animateFloat(

        initialValue = 0f,

        targetValue = 360f,

        animationSpec = infiniteRepeatable(

            animation = tween(3000, easing = LinearEasing),

            repeatMode = RepeatMode.Restart

        ), label = "sparkle_rotation"

    )


    Box(

        modifier = Modifier

            .background(

                brush = Brush.verticalGradient(

                    colors = listOf(

                        colorScheme.primaryContainer.copy(alpha = 0.3f),

                        colorScheme.surface

                    )

                )

            )

    ) {

        Column(

            modifier = Modifier

                .statusBarsPadding()

                .verticalScroll(rememberScrollState())

                .safeDrawingPadding()

                .padding(mediumPadding),

            verticalArrangement = Arrangement.Center,

            horizontalAlignment = Alignment.CenterHorizontally

        ) {

            // Enhanced App Title with animation

            Row(

                verticalAlignment = Alignment.CenterVertically,

                modifier = Modifier.padding(bottom = 16.dp)

            ) {

                Icon(

                    imageVector = Icons.Default.Star,

                    contentDescription = null,

                    tint = colorScheme.primary,

                    modifier = Modifier

                        .size(32.dp)

                        .rotate(sparkleRotation)

                )

                Spacer(modifier = Modifier.width(8.dp))

                Text(

                    text = stringResource(R.string.app_name),

                    style = typography.headlineLarge.copy(

                        fontWeight = FontWeight.Bold,

                        color = colorScheme.primary

                    ),

                )

                Spacer(modifier = Modifier.width(8.dp))

                Icon(

                    imageVector = Icons.Default.Star,

                    contentDescription = null,

                    tint = colorScheme.primary,

                    modifier = Modifier

                        .size(32.dp)

                        .rotate(-sparkleRotation)

                )

            }


            // Hearts Display - Using a default value since heartsRemaining doesn't exist

            HeartsDisplay(3)


            Spacer(modifier = Modifier.height(16.dp))


            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)

            )


            // Enhanced Action Buttons

            EnhancedActionButtons(

                onSubmit = { gameViewModel.checkUserGuess() },

                onSkip = { gameViewModel.skipWord() },

                onShuffle = {

                    // Add shuffle functionality or remove if not implemented

                    // gameViewModel.shuffleCurrentWord()

                },

                modifier = Modifier

                    .fillMaxWidth()

                    .padding(mediumPadding)

            )


            // Enhanced Game Status - Using default value since currentStreak doesn't exist

            EnhancedGameStatus(

                score = gameUiState.score,

                streak = 0, // Default value since currentStreak doesn't exist

                modifier = Modifier.padding(20.dp)

            )


            if (gameUiState.isGameOver) {

                EnhancedFinalScoreDialog(

                    score = gameUiState.score,

                    wordsGuessed = gameUiState.currentWordCount - 1,

                    onPlayAgain = { gameViewModel.resetGame() }

                )

            }

        }


        // Success Animation Overlay

        AnimatedVisibility(

            visible = showSuccessAnimation,

            enter = scaleIn() + fadeIn(),

            exit = scaleOut() + fadeOut()

        ) {

            SuccessOverlay()

        }

    }

}


@Composable

fun HeartsDisplay(heartsRemaining: Int) {

    Row(

        horizontalArrangement = Arrangement.Center,

        modifier = Modifier.fillMaxWidth()

    ) {

        repeat(3) { index ->

            val heartScale = remember { Animatable(1f) }


            LaunchedEffect(heartsRemaining) {

                if (index >= heartsRemaining && index < 3) {

                    heartScale.animateTo(

                        targetValue = 0f,

                        animationSpec = tween(300, easing = FastOutSlowInEasing)

                    )

                }

            }


            Icon(

                imageVector = Icons.Default.Favorite,

                contentDescription = "Heart ${index + 1}",

                tint = if (index < heartsRemaining) Color.Red else Color.Gray,

                modifier = Modifier

                    .size(28.dp)

                    .scale(heartScale.value)

                    .padding(horizontal = 4.dp)

            )

        }

    }

}


@Composable

fun EnhancedActionButtons(

    onSubmit: () -> Unit,

    onSkip: () -> Unit,

    onShuffle: () -> Unit,

    modifier: Modifier = Modifier

) {

    Column(

        modifier = modifier,

        verticalArrangement = Arrangement.spacedBy(12.dp),

        horizontalAlignment = Alignment.CenterHorizontally

    ) {

        // Submit Button with gradient-like effect

        Button(

            modifier = Modifier.fillMaxWidth(),

            onClick = onSubmit,

            colors = ButtonDefaults.buttonColors(

                containerColor = colorScheme.primary

            ),

            elevation = ButtonDefaults.buttonElevation(

                defaultElevation = 6.dp,

                pressedElevation = 2.dp

            )

        ) {

            Icon(

                imageVector = Icons.Default.Star,

                contentDescription = null,

                modifier = Modifier.size(20.dp)

            )

            Spacer(modifier = Modifier.width(8.dp))

            Text(

                text = stringResource(R.string.submit),

                fontSize = 16.sp,

                fontWeight = FontWeight.SemiBold

            )

        }


        // Secondary Action Buttons Row

        Row(

            modifier = Modifier.fillMaxWidth(),

            horizontalArrangement = Arrangement.spacedBy(12.dp)

        ) {

            // Shuffle Button

            OutlinedButton(

                onClick = onShuffle,

                modifier = Modifier.weight(1f),

                colors = ButtonDefaults.outlinedButtonColors(

                    contentColor = colorScheme.secondary

                )

            ) {

                Icon(

                    imageVector = Icons.Default.Refresh,

                    contentDescription = null,

                    modifier = Modifier.size(18.dp)

                )

                Spacer(modifier = Modifier.width(4.dp))

                Text(

                    text = "Shuffle",

                    fontSize = 14.sp

                )

            }


            // Skip Button

            OutlinedButton(

                onClick = onSkip,

                modifier = Modifier.weight(1f)

            ) {

                Text(

                    text = stringResource(R.string.skip),

                    fontSize = 14.sp

                )

            }

        }

    }

}


@Composable

fun EnhancedGameStatus(

    score: Int,

    streak: Int,

    modifier: Modifier = Modifier

) {

    Card(

        modifier = modifier,

        colors = CardDefaults.cardColors(

            containerColor = colorScheme.primaryContainer.copy(alpha = 0.5f)

        ),

        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)

    ) {

        Row(

            modifier = Modifier

                .fillMaxWidth()

                .padding(16.dp),

            horizontalArrangement = Arrangement.SpaceEvenly

        ) {

            // Score

            Column(horizontalAlignment = Alignment.CenterHorizontally) {

                Text(

                    text = "Score",

                    style = typography.labelMedium,

                    color = colorScheme.onPrimaryContainer.copy(alpha = 0.7f)

                )

                Text(

                    text = score.toString(),

                    style = typography.headlineMedium.copy(

                        fontWeight = FontWeight.Bold,

                        color = colorScheme.primary

                    )

                )

            }


            // Divider

            Box(

                modifier = Modifier

                    .width(1.dp)

                    .height(50.dp)

                    .background(colorScheme.outline.copy(alpha = 0.3f))

            )


            // Streak

            Column(horizontalAlignment = Alignment.CenterHorizontally) {

                Text(

                    text = "Streak",

                    style = typography.labelMedium,

                    color = colorScheme.onPrimaryContainer.copy(alpha = 0.7f)

                )

                Row(

                    verticalAlignment = Alignment.CenterVertically

                ) {

                    if (streak > 0) {

                        Icon(

                            imageVector = Icons.Default.Star,

                            contentDescription = null,

                            tint = Color(0xFFFFD700), // Gold color

                            modifier = Modifier.size(20.dp)

                        )

                        Spacer(modifier = Modifier.width(4.dp))

                    }

                    Text(

                        text = streak.toString(),

                        style = typography.headlineMedium.copy(

                            fontWeight = FontWeight.Bold,

                            color = if (streak > 0) Color(0xFFFFD700) else colorScheme.primary

                        )

                    )

                }

            }

        }

    }

}


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

    val shakeOffset = remember { Animatable(0f) }


    // Shake animation when guess is wrong

    LaunchedEffect(isGuessWrong) {

        if (isGuessWrong) {

            repeat(5) {

                shakeOffset.animateTo(

                    targetValue = 10f,

                    animationSpec = tween(50)

                )

                shakeOffset.animateTo(

                    targetValue = -10f,

                    animationSpec = tween(50)

                )

            }

            shakeOffset.animateTo(

                targetValue = 0f,

                animationSpec = tween(50)

            )

        }

    }


    Card(

        modifier = modifier,

        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),

        colors = CardDefaults.cardColors(

            containerColor = colorScheme.surface

        )

    ) {

        Column(

            verticalArrangement = Arrangement.spacedBy(mediumPadding),

            horizontalAlignment = Alignment.CenterHorizontally,

            modifier = Modifier.padding(mediumPadding)

        ) {

            // Word Count Badge

            Box(

                modifier = Modifier

                    .align(alignment = Alignment.End)

                    .clip(shapes.medium)

                    .background(

                        brush = Brush.horizontalGradient(

                            colors = listOf(

                                colorScheme.primary,

                                colorScheme.primary.copy(alpha = 0.8f)

                            )

                        )

                    )

                    .padding(horizontal = 12.dp, vertical = 6.dp)

            ) {

                Text(

                    text = stringResource(R.string.word_count, wordCount),

                    style = typography.labelLarge.copy(

                        fontWeight = FontWeight.SemiBold,

                        color = colorScheme.onPrimary

                    )

                )

            }


            Spacer(modifier = Modifier.height(8.dp))


            // Scrambled Word with individual letter animations

            ScrambledWordDisplay(

                scrambledWord = currentScrambledWord,

                isError = isGuessWrong,

                modifier = Modifier.scale(if (isGuessWrong) 0.95f else 1f)

            )


            Text(

                text = stringResource(R.string.instructions),

                textAlign = TextAlign.Center,

                style = typography.bodyLarge.copy(

                    color = colorScheme.onSurface.copy(alpha = 0.8f)

                )

            )


            // Enhanced Input Field

            OutlinedTextField(

                value = userGuess,

                singleLine = true,

                shape = shapes.large,

                modifier = Modifier

                    .fillMaxWidth()

                    .padding(top = 8.dp),

                colors = TextFieldDefaults.colors(

                    focusedContainerColor = colorScheme.surface,

                    unfocusedContainerColor = colorScheme.surface,

                    disabledContainerColor = colorScheme.surface,

                    errorContainerColor = colorScheme.errorContainer.copy(alpha = 0.1f)

                ),

                onValueChange = onUserGuessChanged,

                label = {

                    Text(

                        text = if (isGuessWrong) {

                            stringResource(R.string.wrong_guess)

                        } else {

                            stringResource(R.string.enter_your_word)

                        },

                        style = typography.bodyMedium

                    )

                },

                isError = isGuessWrong,

                keyboardOptions = KeyboardOptions.Default.copy(

                    imeAction = ImeAction.Done

                ),

                keyboardActions = KeyboardActions(

                    onDone = { onKeyboardDone() }

                )

            )

        }

    }

}


@Composable

fun ScrambledWordDisplay(

    scrambledWord: String,

    isError: Boolean,

    modifier: Modifier = Modifier

) {

    Row(

        modifier = modifier,

        horizontalArrangement = Arrangement.spacedBy(8.dp)

    ) {

        scrambledWord.forEachIndexed { index, char ->

            LetterBox(

                letter = char,

                isError = isError,

                animationDelay = index * 100L

            )

        }

    }

}


@Composable

fun LetterBox(

    letter: Char,

    isError: Boolean,

    animationDelay: Long

) {

    val scale = remember { Animatable(0f) }


    LaunchedEffect(letter) {

        delay(animationDelay)

        scale.animateTo(

            targetValue = 1f,

            animationSpec = tween(

                durationMillis = 300,

                easing = FastOutSlowInEasing

            )

        )

    }


    Box(

        modifier = Modifier

            .size(50.dp)

            .scale(scale.value)

            .clip(shapes.medium)

            .background(

                color = if (isError) {

                    colorScheme.errorContainer.copy(alpha = 0.3f)

                } else {

                    colorScheme.primaryContainer.copy(alpha = 0.5f)

                }

            )

            .border(

                width = 2.dp,

                color = if (isError) {

                    colorScheme.error.copy(alpha = 0.5f)

                } else {

                    colorScheme.primary.copy(alpha = 0.3f)

                },

                shape = shapes.medium

            )

            .clickable { /* Could add letter selection functionality */ },

        contentAlignment = Alignment.Center

    ) {

        Text(

            text = letter.uppercase(),

            style = typography.headlineSmall.copy(

                fontWeight = FontWeight.Bold,

                color = if (isError) {

                    colorScheme.error

                } else {

                    colorScheme.primary

                }

            )

        )

    }

}


@Composable

fun SuccessOverlay() {

    Box(

        modifier = Modifier

            .background(

                color = colorScheme.primary.copy(alpha = 0.1f)

            ),

        contentAlignment = Alignment.Center

    ) {

        Card(

            colors = CardDefaults.cardColors(

                containerColor = colorScheme.primaryContainer

            )

        ) {

            Column(

                horizontalAlignment = Alignment.CenterHorizontally,

                modifier = Modifier.padding(24.dp)

            ) {

                Icon(

                    imageVector = Icons.Default.Star,

                    contentDescription = null,

                    tint = Color(0xFFFFD700),

                    modifier = Modifier.size(48.dp)

                )

                Spacer(modifier = Modifier.height(8.dp))

                Text(

                    text = "Excellent!",

                    style = typography.headlineMedium.copy(

                        fontWeight = FontWeight.Bold,

                        color = colorScheme.primary

                    )

                )

            }

        }

    }

}


@Composable

private fun EnhancedFinalScoreDialog(

    score: Int,

    wordsGuessed: Int,

    onPlayAgain: () -> Unit,

    modifier: Modifier = Modifier

) {

    val activity = (LocalContext.current as Activity)


    AlertDialog(

        onDismissRequest = { },

        title = {

            Row(

                verticalAlignment = Alignment.CenterVertically,

                horizontalArrangement = Arrangement.Center,

                modifier = Modifier.fillMaxWidth()

            ) {

                Icon(

                    imageVector = Icons.Default.Star,

                    contentDescription = null,

                    tint = Color(0xFFFFD700),

                    modifier = Modifier.size(32.dp)

                )

                Spacer(modifier = Modifier.width(8.dp))

                Text(

                    text = stringResource(R.string.congratulations),

                    style = typography.headlineMedium.copy(

                        fontWeight = FontWeight.Bold

                    )

                )

            }

        },

        text = {

            Column(

                horizontalAlignment = Alignment.CenterHorizontally

            ) {

                Text(

                    text = stringResource(R.string.you_scored, score),

                    style = typography.bodyLarge,

                    textAlign = TextAlign.Center

                )

                Spacer(modifier = Modifier.height(8.dp))

                Text(

                    text = "Words guessed: $wordsGuessed",

                    style = typography.bodyMedium.copy(

                        color = colorScheme.onSurface.copy(alpha = 0.7f)

                    ),

                    textAlign = TextAlign.Center

                )

            }

        },

        modifier = modifier,

        dismissButton = {

            TextButton(

                onClick = { activity.finish() }

            ) {

                Text(text = stringResource(R.string.exit))

            }

        },

        confirmButton = {

            Button(

                onClick = onPlayAgain,

                elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp)

            ) {

                Text(text = stringResource(R.string.play_again))

            }

        }

    )

}


/*

 * Creates and shows an AlertDialog with final score.

 */

@Composable

private fun FinalScoreDialog(

    score: Int,

    onPlayAgain: () -> Unit,

    modifier: Modifier = Modifier

) {

    val activity = (LocalContext.current as Activity)


    AlertDialog(

        onDismissRequest = {

            // Dismiss the dialog when the user clicks outside the dialog or on the back

            // button. If you want to disable that functionality, simply use an empty

            // onCloseRequest.

        },

        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

Postingan populer dari blog ini

Tugas 2 - PPB D

Tugas 5 - Simple Calculator