Programação Orientada a Objetos - Batalha Pokemon

18 minute read

Published:

1. Introdução

Você deverá implementar um Sistema Simplificado de Batalha Pokémon executado inteiramente no terminal.

Dois treinadores participarão da batalha:

Treinador 1
      |
      └── Time com 6 Pokémon

Treinador 2
      |
      └── Time com 6 Pokémon

Cada treinador deverá possuir exatamente 6 Pokémon.

Os Pokémon poderão ser apenas de três tipos:

Água
Fogo
Grama

A batalha acontece em turnos. Em seu turno, cada treinador poderá escolher entre:

1 - Atacar
2 - Trocar Pokémon

A batalha termina quando todos os 6 Pokémon de um treinador estiverem derrotados.


2. Regras de Tipos

O sistema deverá implementar as seguintes vantagens:

Água  → dano dobrado contra Fogo
Fogo  → dano dobrado contra Grama
Grama → dano dobrado contra Água

Representação:

        Água
       /    \
      ↓      ↑
    Fogo ← Grama

Mais precisamente:

AtacanteDefensorMultiplicador
ÁguaFogo×2
FogoGrama×2
GramaÁgua×2
Demais combinações—×1

Importante: neste laboratório não será utilizada resistência com dano reduzido. Quando não houver vantagem, o multiplicador será 1.


3. Estrutura Sugerida do Sistema

Implemente pelo menos as seguintes classes:

Pokemon
Time
Treinador
BatalhaPokemon
Principal

Uma organização possível:

Treinador
    ◆
    |
    └── Time
          |
          └── Pokemon[6]

BatalhaPokemon
    |
    ├── Treinador 1
    └── Treinador 2

Responsabilidades sugeridas

Pokemon

Responsável por:

  • armazenar seus próprios atributos;
  • controlar seus pontos de vida;
  • informar se está derrotado;
  • receber dano;
  • disponibilizar os dados necessários para o cálculo de ataque.

Time

Responsável por:

  • armazenar os 6 Pokémon;
  • permitir alterar a ordem da equipe antes da batalha;
  • retornar um Pokémon pela posição;
  • verificar se ainda existem Pokémon disponíveis;
  • informar quais Pokémon estão aptos para batalha.

Treinador

Responsável por:

  • armazenar o nome do treinador;
  • possuir seu Time;
  • manter qual Pokémon está atualmente em batalha;
  • trocar o Pokémon ativo.

BatalhaPokemon

Responsável por:

  • controlar os turnos;
  • solicitar as ações dos treinadores;
  • executar ataques;
  • controlar trocas;
  • verificar Pokémon derrotados;
  • verificar quando a batalha terminou.

Principal

Responsável apenas por:

  • criar os objetos principais;
  • criar os Pokémon;
  • montar os times;
  • iniciar a configuração da ordem;
  • iniciar a batalha.

Evite colocar toda a lógica do jogo dentro do método main().


4. Classe Pokemon

Cada Pokémon deverá possuir pelo menos os seguintes atributos:

private String nome;
private String tipo;
private int hpMaximo;
private int hpAtual;
private int ataque;
private int defesa;

Exemplos válidos:

PokémonTipoHPATKDEF
SquirtleÁgua1054865
PsyduckÁgua1005248
TotodileÁgua1085552
MudkipÁgua1105055
CharmanderFogo955243
VulpixFogo985045
CyndaquilFogo1005244
TorchicFogo965542
BulbasaurGrama1104949
ChikoritaGrama1124855
TreeckoGrama955540
TurtwigGrama1155060

Os valores acima são didáticos. Não precisam corresponder aos valores oficiais dos jogos.


5. Encapsulamento do Pokémon

Os atributos devem ser private.

Não permita:

pokemon.hpAtual = -1000;

Crie métodos que controlem as alterações, por exemplo:

public void receberDano(int dano) {
    // TODO
}

A implementação deverá garantir:

hpAtual nunca < 0

Também implemente:

public boolean estaDerrotado()

Resultado esperado:

true  → HP atual == 0
false → HP atual > 0

6. Constantes para os Tipos

Evite repetir Strings arbitrárias ao longo do código.

Uma possibilidade é declarar constantes na classe Pokemon:

public static final String TIPO_AGUA = "Agua";
public static final String TIPO_FOGO = "Fogo";
public static final String TIPO_GRAMA = "Grama";

Assim:

Pokemon.TIPO_AGUA

é preferível a repetir:

"Agua"

em vários pontos do programa.


7. Cálculo do Dano

Utilize a seguinte fórmula simplificada.

Passo 1 — Dano base

danoBase = ataqueAtacante - (defesaDefensor / 2)

O dano base nunca poderá ser menor que 1.

int danoBase =
    atacante.getAtaque()
    - defensor.getDefesa() / 2;

if (danoBase < 1) {
    danoBase = 1;
}

Exemplo

Squirtle (Água)
ATK = 48

Charmander (Fogo)
DEF = 43

Dano base:

48 - (43 / 2)
48 - 21
27

Como Água possui vantagem sobre Fogo:

27 × 2 = 54

8. Vantagem de Tipo

Se houver vantagem:

multiplicadorTipo = 2

Caso contrário:

multiplicadorTipo = 1

Casos de vantagem:

Água  → Fogo
Fogo  → Grama
Grama → Água

Uma assinatura possível:

private int calcularMultiplicadorTipo(
        Pokemon atacante,
        Pokemon defensor) {

    // TODO
    return 1;
}

Dica

Compare Strings com .equals():

atacante.getTipo().equals(Pokemon.TIPO_AGUA)

Não utilize == para comparar o conteúdo de String.


9. Dano Crítico e Falha Crítica

Utilize:

import java.util.Random;

O sistema deverá possuir uma pequena chance de:

5%  → Falha crítica
10% → Ataque crítico
85% → Ataque normal

Uma implementação possível começa com:

Random random = new Random();
int sorteio = random.nextInt(100);

O resultado estará entre 0 e 99.

Use:

0  até 4   → falha crítica
5  até 14  → ataque crítico
15 até 99  → ataque normal

Falha crítica

Em uma falha crítica:

dano = 0

Exiba:

Falha crítica! O ataque não acertou.

Ataque crítico

Em um ataque crítico, dobre o dano depois de aplicar a vantagem de tipo:

danoCritico = danoNormal × 2

Exemplo:

Dano base:          27
Vantagem de tipo:  ×2
Dano normal:        54
Crítico:            ×2

Dano final:        108

Portanto, um ataque com vantagem de tipo e crítico poderá causar:

4 × danoBase

10. Algoritmo Completo do Dano

1. Calcular danoBase
          ↓
2. Verificar vantagem de tipo
          ↓
3. Aplicar multiplicador de tipo
          ↓
4. Sortear evento aleatório
          ↓
      ┌───────────────┐
      │               │
Falha crítica      Crítico
 dano = 0          dano × 2
      │               │
      └───────┬───────┘
              ↓
5. Aplicar dano no defensor

Pseudocódigo

danoBase = ataque - defesa / 2;

if (danoBase < 1)
    danoBase = 1;

multiplicadorTipo = obterMultiplicador(...);

dano = danoBase * multiplicadorTipo;

sorteio = random.nextInt(100);

if (sorteio < 5)
    dano = 0;
else if (sorteio < 15)
    dano = dano * 2;

defensor.receberDano(dano);

11. Classe Time

Cada treinador possui exatamente 6 Pokémon.

Utilize:

private Pokemon[] pokemons;

No construtor:

pokemons = new Pokemon[6];

Uma possível estrutura:

public class Time {

    public static final int TAMANHO_TIME = 6;

    private Pokemon[] pokemons;
    private int quantidade;

    public Time() {
        pokemons = new Pokemon[TAMANHO_TIME];
        quantidade = 0;
    }

    public void adicionarPokemon(Pokemon pokemon) {
        // TODO
    }
}

12. Seleção da Ordem do Time

Antes da batalha, cada treinador deverá escolher a ordem dos seus Pokémon.

Exemplo inicial:

Time disponível:

1 - Squirtle
2 - Bulbasaur
3 - Charmander
4 - Psyduck
5 - Vulpix
6 - Treecko

O programa deverá pedir:

Escolha o Pokémon para a posição 1: 3
Escolha o Pokémon para a posição 2: 1
Escolha o Pokémon para a posição 3: 6
...

Resultado:

=== ORDEM DO TIME ===

1 - Charmander
2 - Squirtle
3 - Treecko
4 - ...

Cada Pokémon só poderá ser selecionado uma vez.

Dica de implementação

Uma estratégia simples é criar:

boolean[] selecionados = new boolean[6];
Pokemon[] timeOrdenado = new Pokemon[6];

Quando o usuário escolher uma posição, marque o índice correspondente:

selecionados[indice] = true;

Antes de aceitar uma nova escolha:

if (selecionados[indice]) {
    System.out.println(
        "Esse Pokémon já foi selecionado."
    );
}

Ao final, o array ordenado deverá substituir a ordem anterior.


13. Pokémon Ativo

O primeiro Pokémon do time será o Pokémon inicial da batalha.

Por exemplo:

Time:
[Charmander, Squirtle, Bulbasaur, ...]

Então:

Pokémon ativo = Charmander

O Treinador deverá manter qual posição do time está atualmente ativa.

Uma possibilidade:

private int indicePokemonAtivo;

Inicialmente:

indicePokemonAtivo = 0;

14. Classe Treinador

Estrutura sugerida:

public class Treinador {

    private String nome;
    private Time time;
    private int indicePokemonAtivo;

    public Treinador(String nome) {
        this.nome = nome;
        this.time = new Time();
        this.indicePokemonAtivo = 0;
    }

    public Pokemon getPokemonAtivo() {
        // TODO
        return null;
    }

    public void trocarPokemon(int indice) {
        // TODO
    }
}

Neste modelo:

Treinador ◆── Time

é uma composição, pois o treinador cria e controla seu time.

Enquanto:

Time ─── Pokemon

é uma associação, pois os objetos Pokemon podem ser criados independentemente e depois adicionados ao time.


15. Fluxo da Batalha

Depois que os times estiverem configurados:

=== INÍCIO DA BATALHA ===

Ash
Pokémon ativo: Charmander

Misty
Pokémon ativo: Squirtle

A batalha deve acontecer em turnos alternados:

Turno 1 → Treinador 1
Turno 2 → Treinador 2
Turno 3 → Treinador 1
Turno 4 → Treinador 2
...

16. Menu de Ações

Em cada turno:

=== TURNO DE ASH ===

Pokémon ativo:
Charmander
HP: 95 / 95

Oponente:
Squirtle
HP: 105 / 105

Escolha uma ação:

1 - Atacar
2 - Trocar Pokémon

Utilize Scanner para ler a escolha.


17. Ação: Atacar

Caso o jogador selecione:

1 - Atacar

o Pokémon ativo deve atacar o Pokémon ativo do adversário.

Exemplo:

Charmander atacou Bulbasaur!

Dano base: 28
Vantagem de tipo: Fogo > Grama (x2)

Dano causado: 56

Bulbasaur
HP: 54 / 110

Caso ocorra crítico:

ATAQUE CRÍTICO!

Dano causado: 112

Caso ocorra falha crítica:

FALHA CRÍTICA!

Charmander errou completamente o ataque.
Dano causado: 0

18. Ação: Trocar Pokémon

Caso seja selecionado:

2 - Trocar Pokémon

exiba:

=== SELECIONE UM POKÉMON ===

1 - Charmander    HP: 70/95    [ATIVO]
2 - Squirtle      HP: 105/105
3 - Bulbasaur     HP: 0/110    [DERROTADO]
4 - Psyduck       HP: 100/100
5 - Vulpix        HP: 98/98
6 - Treecko       HP: 95/95

Não permita:

  • escolher o Pokémon que já está ativo;
  • escolher um Pokémon derrotado;
  • escolher posição inválida.

Uma troca válida consome o turno.

Exemplo:

Ash trocou Charmander por Squirtle.

Depois disso, o turno passa para o adversário.


19. Pokémon Derrotado

Quando:

hpAtual == 0

o Pokémon está derrotado.

Exemplo:

Bulbasaur recebeu 54 de dano.

HP: 0 / 110

Bulbasaur foi derrotado!

Se ainda existirem Pokémon disponíveis, o treinador deve obrigatoriamente selecionar outro.

Escolha o próximo Pokémon:

Essa troca obrigatória não deve ser considerada uma ação adicional de turno.


20. Fim da Batalha

A batalha termina quando um treinador não possui mais Pokémon disponíveis.

Implemente algo semelhante a:

public boolean possuiPokemonDisponivel()

O método deverá percorrer o array e verificar se existe pelo menos um Pokémon com:

HP > 0

Quando não houver:

================================
         FIM DA BATALHA
================================

Todos os Pokémon de Misty foram derrotados.

Vencedor: Ash!

21. Estrutura do Loop Principal

Uma estrutura possível:

enquanto os dois treinadores possuírem Pokémon disponíveis:

    exibir situação atual

    executar turno do treinador atual

    se Pokémon defensor foi derrotado:
        verificar se existem Pokémon disponíveis

        se não existem:
            encerrar batalha

        caso contrário:
            solicitar troca obrigatória

    alternar treinador

Em Java:

while (treinador1.possuiPokemonDisponivel()
        && treinador2.possuiPokemonDisponivel()) {

    // TODO
}

22. Classes e Relacionamentos Esperados

+----------------+
|   Treinador    |
+----------------+
| nome           |
| time           |
| pokemonAtivo   |
+----------------+
        ◆
        |
        |
+----------------+
|      Time      |
+----------------+
| Pokemon[6]     |
+----------------+
        |
        | associação
        |
        +-----------> Pokemon
        +-----------> Pokemon
        +-----------> Pokemon
        +-----------> ...

E:

BatalhaPokemon
      |
      +── Treinador 1
      |
      └── Treinador 2

23. Código Inicial Sugerido — Pokemon.java

public class Pokemon {

    public static final String TIPO_AGUA = "Agua";
    public static final String TIPO_FOGO = "Fogo";
    public static final String TIPO_GRAMA = "Grama";

    private String nome;
    private String tipo;

    private int hpMaximo;
    private int hpAtual;

    private int ataque;
    private int defesa;

    public Pokemon(
            String nome,
            String tipo,
            int hpMaximo,
            int ataque,
            int defesa) {

        // TODO
    }

    public void receberDano(int dano) {
        // TODO
    }

    public boolean estaDerrotado() {
        // TODO
        return false;
    }

    public String getNome() {
        return nome;
    }

    public String getTipo() {
        return tipo;
    }

    public int getHpMaximo() {
        return hpMaximo;
    }

    public int getHpAtual() {
        return hpAtual;
    }

    public int getAtaque() {
        return ataque;
    }

    public int getDefesa() {
        return defesa;
    }
}

24. Código Inicial Sugerido — Time.java

public class Time {

    public static final int TAMANHO_TIME = 6;

    private Pokemon[] pokemons;
    private int quantidade;

    public Time() {
        pokemons = new Pokemon[TAMANHO_TIME];
        quantidade = 0;
    }

    public void adicionarPokemon(Pokemon pokemon) {
        // TODO
    }

    public Pokemon getPokemon(int indice) {
        // TODO
        return null;
    }

    public boolean possuiPokemonDisponivel() {
        // TODO
        return false;
    }

    public void exibirTime() {
        // TODO
    }

    public void definirOrdem() {
        // TODO
    }
}

Você pode passar um objeto Scanner como parâmetro para definirOrdem() em vez de criar vários objetos Scanner.


25. Código Inicial Sugerido — Treinador.java

public class Treinador {

    private String nome;
    private Time time;
    private int indicePokemonAtivo;

    public Treinador(String nome) {
        this.nome = nome;
        this.time = new Time();
        indicePokemonAtivo = 0;
    }

    public Pokemon getPokemonAtivo() {
        // TODO
        return null;
    }

    public void trocarPokemon(int indice) {
        // TODO
    }

    public boolean possuiPokemonDisponivel() {
        // TODO
        return false;
    }

    public String getNome() {
        return nome;
    }

    public Time getTime() {
        return time;
    }
}

26. Código Inicial Sugerido — BatalhaPokemon.java

import java.util.Random;
import java.util.Scanner;

public class BatalhaPokemon {

    private Treinador treinador1;
    private Treinador treinador2;

    private Random random;
    private Scanner scanner;

    public BatalhaPokemon(
            Treinador treinador1,
            Treinador treinador2,
            Scanner scanner) {

        this.treinador1 = treinador1;
        this.treinador2 = treinador2;

        this.scanner = scanner;
        this.random = new Random();
    }

    public void iniciar() {
        // TODO
    }

    private void executarTurno(
            Treinador atual,
            Treinador adversario) {

        // TODO
    }

    private int calcularDano(
            Pokemon atacante,
            Pokemon defensor) {

        // TODO
        return 0;
    }

    private int calcularMultiplicadorTipo(
            Pokemon atacante,
            Pokemon defensor) {

        // TODO
        return 1;
    }
}

27. Código Inicial Sugerido — Principal.java

import java.util.Scanner;

public class Principal {

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        Treinador treinador1 =
            new Treinador("Ash");

        Treinador treinador2 =
            new Treinador("Misty");

        // TODO: criar os 12 Pokemon

        // TODO: adicionar 6 Pokemon a cada time

        // TODO: permitir aos treinadores
        // escolherem a ordem dos times

        BatalhaPokemon batalha =
            new BatalhaPokemon(
                treinador1,
                treinador2,
                scanner
            );

        batalha.iniciar();

        scanner.close();
    }
}

28. Sequência Recomendada de Implementação

Não tente implementar toda a batalha de uma única vez.

Etapa 1 — Pokémon

Implemente e teste:

Pokemon
getters
receberDano()
estaDerrotado()

Etapa 2 — Time

Implemente:

adicionarPokemon()
getPokemon()
exibirTime()
possuiPokemonDisponivel()

Teste com 6 objetos.

Etapa 3 — Ordenação do time

Implemente a seleção da ordem utilizando:

Pokemon[]
boolean[]
Scanner

Teste se:

  • nenhum Pokémon pode aparecer duas vezes;
  • todos aparecem exatamente uma vez.

Etapa 4 — Treinador

Implemente:

getPokemonAtivo()
trocarPokemon()
possuiPokemonDisponivel()

Etapa 5 — Dano

Teste separadamente:

Água × Fogo
Fogo × Grama
Grama × Água
Água × Água

Antes de adicionar críticos.

Etapa 6 — Crítico e falha crítica

Adicione:

Random
5% falha
10% crítico

Execute vários ataques e observe os resultados.

Etapa 7 — Um único turno

Faça primeiro:

Treinador 1 ataca Treinador 2

sem loop.

Quando funcionar, adicione a alternância de turnos.

Etapa 8 — Batalha completa

Somente então implemente:

while (...)

até que um treinador não tenha Pokémon disponíveis.


29. Testes Mínimos Obrigatórios

Antes da entrega, teste pelo menos os seguintes cenários.

Teste 1 — Vantagem Água → Fogo

Squirtle ataca Charmander

O dano deve possuir multiplicador 2.

Teste 2 — Vantagem Fogo → Grama

Charmander ataca Bulbasaur

Multiplicador 2.

Teste 3 — Vantagem Grama → Água

Bulbasaur ataca Squirtle

Multiplicador 2.

Teste 4 — Sem vantagem

Squirtle ataca Psyduck

Multiplicador 1.

Teste 5 — Troca

Troque um Pokémon ativo por outro disponível. O turno deverá terminar.

Teste 6 — Troca inválida

Tente selecionar um Pokémon derrotado. O sistema deve recusar.

Teste 7 — Pokémon derrotado

Reduza o HP até 0. O sistema deve impedir que esse Pokémon continue lutando.

Teste 8 — Fim de batalha

Quando todos os Pokémon de um treinador chegarem a 0 HP, a batalha deverá terminar.


30. Exemplo Resumido de Execução

=================================
       BATALHA POKÉMON
=================================

Ash vs Misty

=== TURNO DE ASH ===

Ativo:
Charmander [Fogo]
HP: 95/95

Adversário:
Bulbasaur [Grama]
HP: 110/110

1 - Atacar
2 - Trocar Pokémon

Escolha: 1

Charmander atacou Bulbasaur!

Dano base: 28
Vantagem: Fogo > Grama
Multiplicador: x2

ATAQUE CRÍTICO!

Dano final: 112

Bulbasaur recebeu 112 de dano.
HP: 0/110

Bulbasaur foi derrotado!

Misty, escolha o próximo Pokémon:

1 - Squirtle  HP: 105/105
2 - Psyduck   HP: 100/100
3 - Vulpix    HP: 98/98
...

31. Dicas Importantes

Dica 1 — Uma classe, uma responsabilidade principal

Pergunte:

Quem deveria saber isso?

Exemplos:

Quem controla o HP?
→ Pokemon

Quem conhece os 6 Pokémon?
→ Time

Quem sabe qual Pokémon está ativo?
→ Treinador

Quem controla o turno?
→ BatalhaPokemon

Dica 2 — Proteja os arrays

Evite expor livremente o array interno:

public Pokemon[] getPokemons() {
    return pokemons;
}

Prefira operações específicas:

getPokemon(indice)
adicionarPokemon(...)
exibirTime()

Dica 3 — Não use números mágicos

Evite:

if (quantidade < 6)

Prefira:

public static final int TAMANHO_TIME = 6;

Para probabilidades:

public static final int CHANCE_FALHA_CRITICA = 5;
public static final int CHANCE_CRITICO = 10;

Dica 4 — Valide todas as entradas

Ao escolher uma posição, verifique:

índice válido?
Pokémon existe?
Pokémon está derrotado?
Pokémon já está ativo?

Dica 5 — Use métodos auxiliares

Se um método estiver muito grande, divida-o:

exibirStatusBatalha()
selecionarAcao()
executarAtaque()
selecionarTroca()
calcularDano()
calcularMultiplicadorTipo()

32. Requisitos Técnicos Obrigatórios

A solução deverá:

  • utilizar Java;
  • funcionar em linha de comando;
  • criar exatamente 2 treinadores;
  • criar exatamente 6 Pokémon para cada treinador;
  • utilizar apenas os tipos Água, Fogo e Grama;
  • utilizar Pokemon[] para representar o time;
  • permitir escolher a ordem dos 6 Pokémon antes da batalha;
  • impedir repetição de Pokémon durante a escolha da ordem;
  • manter um Pokémon ativo por treinador;
  • permitir as ações Atacar e Trocar Pokémon;
  • fazer a troca consumir o turno;
  • impedir a seleção de Pokémon derrotados;
  • implementar vantagens de tipo com dano ×2;
  • utilizar a fórmula de dano definida neste enunciado;
  • implementar 5% de falha crítica;
  • implementar 10% de dano crítico;
  • utilizar Random;
  • utilizar Scanner;
  • impedir HP negativo;
  • detectar Pokémon derrotados;
  • exigir troca quando o Pokémon ativo for derrotado;
  • encerrar a partida quando um time inteiro estiver derrotado;
  • utilizar encapsulamento;
  • separar a solução em múltiplas classes.

Desafios Opcionais

Após finalizar todos os requisitos obrigatórios, escolha uma extensão:

  1. Exibir uma barra de HP:
Charmander [██████----] 60/95
  1. Permitir que os treinadores informem seus nomes.
  2. Sortear quem inicia a batalha.
  3. Registrar um histórico textual da batalha em um array.
  4. Exibir ao final:
Total de turnos
Quantidade de críticos
Quantidade de falhas críticas
Pokémon que mais causou dano
  1. Permitir iniciar uma nova batalha sem reiniciar o programa.

Resultado Esperado

Ao concluir o laboratório, sua aplicação deverá demonstrar na prática como diferentes objetos podem colaborar:

Treinador
    ↓
Time
    ↓
Pokemon
    ↓
estado e comportamento

BatalhaPokemon
    ↓
coordena a interação entre os objetos

A solução deve priorizar responsabilidades claras, estado protegido e colaboração entre objetos, em vez de concentrar toda a lógica em uma única classe.