📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.

⚔️ P20: Alliance Battle (Máquinas de Estado Finitas & Testes JUnit 5)

Bem-vindo à nona etapa da Fase 7: Arquitetura Avançada & Qualidade de Código! ⚔️

Neste projeto prático, você aprenderá como construir uma Máquina de Estados Finita (Finite State Machine - FSM) utilizando o poder das sealed class do Kotlin combinadas com a arquitetura MVI (Model-View-Intent) e o padrão de fluxo unidirecional de dados (UDF - Unidirectional Data Flow).

Com a temática de uma arena de batalha medieval mística (Alliance Battle contra um Dragão Ancestral), você aprenderá como eliminar estados inválidos e inconsistentes do aplicativo em tempo de compilação, implementar uma engine de regras pura e escrever testes unitários automatizados com JUnit 5 (Jupiter) para validar 100% dos cálculos matemáticos de combate!

Fluxo Completo de Telas do Simulador de Batalha


📱 Galeria de Telas da Aplicação

1. Estado Idle (Espera) 2. Turno Ativo (Combate FSM) 3. Estado Vitória (Terminal)
Tela 1 - Estado Idle Tela 2 - Combate Ativo Tela 3 - Vitória
Arena aguardando o início do duelo. Transições atômicas entre jogador e IA inimiga. Estado terminal seguro com recompensa de EXP.

✅ Pré-requisitos e Continuidade

Antes de iniciar este laboratório, certifique-se de ter compreendido:


🎯 Objetivos de Aprendizagem

Ao concluir este projeto autoguiado, você será capaz de:

  1. Modelar Máquinas de Estado com sealed class: Representar todos os estados possíveis da aplicação de forma exaustiva e fortemente tipada.
  2. Eliminar Estados Inválidos (Make Illegal States Unrepresentable): Impedir que a UI entre em estados concorrentes ou logicamente incorretos (ex: jogador agir quando está morto).
  3. Desacoplar Lógica de Negócio em Engines Puros: Isolar cálculos matemáticos e regras de jogo (BattleEngine) do framework Android.
  4. Criar Suítes de Testes Automatizados com JUnit 5: Escrever testes de unidade declarativos, testando limites de dano, absorção de armadura e transições de estado.
  5. Implementar IA Inimiga Assíncrona com Coroutines: Simular o contra-ataque do dragão com transições temporizadas sem congelar a interface.

🏗️ Diagrama de Transição de Estados (FSM)

stateDiagram-v2
    [*] --> Idle: App Inicializado
    Idle --> PlayerTurn: Clicar em "ENTRAR NA ARENA"
    
    PlayerTurn --> EnemyTurn: Jogador Ataca ou Cura (EnemyHP > 0)
    PlayerTurn --> Victory: Ataque zera o EnemyHP (EnemyHP <= 0)
    
    EnemyTurn --> PlayerTurn: Contra-ataque do Dragão (PlayerHP > 0)
    EnemyTurn --> Defeat: Golpe do Dragão zera PlayerHP (PlayerHP <= 0)
    
    Victory --> PlayerTurn: Clicar em "JOGAR NOVAMENTE"
    Defeat --> PlayerTurn: Clicar em "TENTAR NOVAMENTE"

🛡️ Boas Práticas de Engenharia e Prevenção de Estados Inconsistentes

[!IMPORTANT] O Princípio de “Tornar Estados Ilegais Irrepresentáveis”: Em arquiteturas legadas com múltiplos booleanos soltos (var isLoading = false, var isGameOver = false, var isPlayerTurn = true), é comum o app entrar em deadlocks ou combinações impossíveis de flags.

Nossa Estratégia de Blindagem Arquitetural:

  1. Enumeração Fechada com sealed class: O compilador do Kotlin garante exaustividade no when (state) sem necessidade de blocos else genéricos perigosos.
  2. Imutabilidade Atômica: Os objetos de estado são data class com propriedades val, garantindo que uma transição de estado seja sempre um novo snapshot imutável.
  3. Testabilidade Total via JUnit 5: Regras de combate são 100% puras (funções que recebem entradas e devolvem saídas determinísticas sem side effects).
  4. Higiene de Versionamento: Arquivo .gitignore configurado para isolar caches locais.

📖 Dicionário Técnico do Projeto

Termo / Componente O que é e para que serve?
FSM (Finite State Machine) Modelo computacional que define um número finito de estados e regras determinísticas de transição entre eles.
Sealed Class Hierarquia restrita de classes do Kotlin onde todas as subclasses diretas são conhecidas em tempo de compilação.
Unidirectional Data Flow (UDF) Padrão onde o estado desce pela árvore de componentes e os eventos de interação sobem de forma unidirecional.
JUnit 5 (Jupiter) Framework padrão da indústria para execução de testes unitários modernos em Java e Kotlin.
Pure Function Função matemática cujo resultado depende exclusivamente de seus parâmetros, sem efeitos colaterais externos.

🛠️ Passo a Passo de Implementação

🚀 Passo 1: Criando o Novo Projeto no Android Studio

  1. Abra o Android Studio e clique em New Project (ou File > New > New Project).
  2. Selecione o template Empty Activity (com o ícone do Jetpack Compose 🌌).
  3. Preencha as configurações do projeto:
    • Name: Simulador de Batalha (ou android_p20_batalha_states)
    • Package name: br.com.curso.batalha
    • Save location: Pasta do seu projeto no repositório
    • Language: Kotlin
    • Minimum SDK: API 24 ("Nougat"; Android 7.0) ou superior
    • Build configuration language: Groovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)
  4. Clique em Finish e aguarde o Gradle sincronizar.

📦 Passo 2: Configurando Dependências e JUnit 5 no Gradle (build.gradle)

Abra app > build.gradle:

android {
    testOptions {
        unitTests.all {
            useJUnitPlatform()
        }
    }
}

dependencies {
    implementation platform('androidx.compose:compose-bom:2024.02.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    implementation 'androidx.activity:activity-compose:1.8.2'
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'

    // JUnit 5 (Jupiter) para testes unitários
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.1'
}

📄 Passo 3: Manifesto do Aplicativo (AndroidManifest.xml)

Abra app > src > main > AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:label="Simulador de Batalha"
        android:supportsRtl="true"
        android:theme="@style/Theme.CartaoTreinador">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

🛡️ Passo 4: Modelagem da Máquina de Estados (BattleState)

Abra app > src > main > java > br > com > curso > batalha > MainActivity.kt:

sealed class BattleState {
    data object Idle : BattleState()
    data class PlayerTurn(val playerHp: Int, val enemyHp: Int, val log: String, val turno: Int) : BattleState()
    data class EnemyTurn(val playerHp: Int, val enemyHp: Int, val log: String, val turno: Int) : BattleState()
    data class Victory(val totalTurns: Int, val expGained: Int) : BattleState()
    data class Defeat(val killerMove: String) : BattleState()
}

⚔️ Passo 5: Engine Matemático de Combate Puro (BattleEngine)

object BattleEngine {
    fun calcularDano(ataque: Int, defesa: Int, multiplicador: Float = 1.0f): Int {
        val danoBruto = ataque - (defesa / 2)
        val danoMinimo = if (danoBruto < 1) 1 else danoBruto
        return (danoMinimo * multiplicador).toInt()
    }

    fun aplicarCura(hpAtual: Int, cura: Int, maxHp: Int = 100): Int {
        return (hpAtual + cura).coerceAtMost(maxHp)
    }

    fun avaliarResultadoCombate(playerHp: Int, enemyHp: Int, turnos: Int): BattleState? {
        return when {
            enemyHp <= 0 -> BattleState.Victory(totalTurns = turnos, expGained = 250)
            playerHp <= 0 -> BattleState.Defeat(killerMove = "Sopro Incendiário do Dragão")
            else -> null
        }
    }
}

🧪 Passo 6: Suíte de Testes Unitários com JUnit 5 (BattleEngineTest.kt)

Abra app > src > test > java > br > com > curso > batalha > BattleEngineTest.kt:

class BattleEngineTest {

    @Test
    fun `dano comum deve reduzir metade da defesa do inimigo`() {
        val dano = BattleEngine.calcularDano(ataque = 20, defesa = 10)
        assertEquals(15, dano)
    }

    @Test
    fun `dano minimo deve ser sempre 1 mesmo se a defesa for extremamente alta`() {
        val dano = BattleEngine.calcularDano(ataque = 5, defesa = 50)
        assertEquals(1, dano)
    }

    @Test
    fun `cura nao deve ultrapassar o limite maximo de HP de 100`() {
        val hpFinal = BattleEngine.aplicarCura(hpAtual = 90, cura = 25, maxHp = 100)
        assertEquals(100, hpFinal)
    }

    @Test
    fun `calcular proximo estado deve retornar Vitoria quando o HP do inimigo zera`() {
        val proximoEstado = BattleEngine.avaliarResultadoCombate(playerHp = 45, enemyHp = 0, turnos = 3)
        assertTrue(proximoEstado is BattleState.Victory)
    }
}

🎮 Passo 7: Interface Reativa e Polimórfica no Compose

@Composable
fun BatalhaScreen() {
    var state by remember { mutableStateOf<BattleState>(BattleState.Idle) }

    when (val s = state) {
        is BattleState.Idle -> { /* Renderiza Card de Entrada na Arena */ }
        is BattleState.PlayerTurn, is BattleState.EnemyTurn -> { /* Renderiza Arena com Barras de HP e Botões */ }
        is BattleState.Victory -> { /* Renderiza Tela de Vitória com EXP */ }
        is BattleState.Defeat -> { /* Renderiza Tela de Derrota */ }
    }
}

🔍 Guia de Diagnóstico & Resolução de Problemas (Troubleshooting)

Sintoma Observado Causa Provável Como Resolver
Test framework not found ao rodar ./gradlew test O bloco unitTests.all { useJUnitPlatform() } não foi configurado no build.gradle. Adicione a instrução useJUnitPlatform() dentro de android.testOptions para habilitar o JUnit 5.
O HP do jogador ultrapassa 100 após curar Falta do limitador de teto na função de cura. Use .coerceAtMost(maxHp) na função aplicarCura().
O turno do inimigo executa instantaneamente sem dar tempo de ler A chamada de contra-ataque não foi encapsulada com delay() em uma corrotina. Utilize coroutineScope.launch { delay(1200); ... } para simular o tempo de resposta da IA.
Erro de compilação when expression must be exhaustive Um novo estado foi adicionado na sealed class, mas não foi tratado no when. Trate o novo estado explicitamente para manter o código seguro e previsível.

🏆 Desafios e Upgrades (Mão na Massa!)

  1. 🎲 Sistema de Acertos Críticos com Probabilidade: Adicione uma rolagem de dados aleatória com 20% de chance de aplicar dano duplo (Critical Strike).
  2. 🛡️ Postura Defensiva (Shield Guard): Crie um novo comando que dobre a defesa do jogador no turno seguinte em vez de atacar.
  3. 📜 Histórico Completo de Turnos em Lista: Armazene os logs de todos os turnos anteriores e exiba-os em um LazyColumn expansível.

📖 Gabarito Oficial de Código (Para Conferência)

Disponível em: app/src/main/java/br/com/curso/batalha/MainActivity.kt.


🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd proj_aplicacoes_full_stack/projetos/android_p20_batalha_states

2. Execute a aplicação e a suíte de testes unitários

./gradlew test
./gradlew build
# ou abrir no Android Studio e clicar em Run (Shift+F10)

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code ou Android Studio, abra o projeto diretamente pela pasta proj_aplicacoes_full_stack/projetos/android_p20_batalha_states.