[TDD] Desafio básico de design: Modelagem de "batalha" em um jogo de RPG

25 respostas
dreampeppers99

O desafio é bem simples, há um cenário e os testes para que vocês façam e demonstrem e/ou expliquem suas decisões de projeto.

Produto: Um jogo RPG chamado Breath of Fantasy Estória: O esquema de batalha -> Criar o esquema de batalha para o jogo.

Descrição:
A batalha é baseada em turnos, a cada momento um personagem ataca e o outro recebe o ataque. O personagem tem pontos de energia e pontos de poder. Essas duas propriedades contém números inteiros. Por exemplo, se o herói (pontos de energia:60, pontos de poder:45) ataca um inimigo (pontos de energia:60, pontos de poder:45) o inimigo terá seus pontos de energia diminuídos. O dano sofrido, ou seja, os pontos de energia perdidos pelo inimigo, dependem do fator sorte. O fator sorte é um número randômico de 0 a 100 que é dado a cada turno da batalha.
Há quatro tipos de ataques que dependem logicamente do fator sorte:
[list]Quando a sorte está em 0-3 então o ataque é Perdido -> não causa dano;[/list]
[list]Quando a sorte está em 4-70 então o ataque é Normal -> causa 1/3 de seus pontos de poder de danos;[/list]
[list]Quando a sorte está em 71-96 então o ataque é Sorte -> causa 1/3 de seus pontos de poder mais 20% desses 1/3;[/list]
[list]E quando a sorte está em 97-100 então o ataque é Crítico -> causa o dobro de um ataque normal.[/list]

[size=16][color=blue]Crie testes que provem que com os personagens acima (herói e inimigo) :[/color][/size]
Quando houver um ataque Perdido não há dano;
Quando houver um ataque Normal haverá um dano de 1/3 dos pontos de poder;
Quando houver um ataque Sorte haverá um dano de 1/3 + 20% de 1/3;
E quando houver um ataque Crítico haverá um dano de duas vezes o dano de um ataque normal.

[size=16]Minha solução foi:[/size]
public class Power {
    private int power;
    private Luck luck;
    public Power(int power,Luck luck) {
        this.power = power;
        this.luck = luck;
    }
    public int getPowerAttack() {
        return (int) ((power / 3) * luck.nextAttackLuckFactor());
    }
}

public class Energy {
    private int energy;
    public Energy(int energy){
        this.energy = energy;
    }
    public int getEnergyPoints(){
        return energy;
    }
    public void decrease(int attack) {
        energy -= attack;
    }
}

public class UnitCharacter {
    private final Energy energy;
    private final Power power;
    public UnitCharacter(final String name,final Energy energy,final Power power) {
        this.energy = energy;
        this.power = power;
    }
    public void attack(final UnitCharacter other) {
        other.energy.decrease(power.getPowerAttack());
    }

    public int getEnergyPoints() {
        return energy.getEnergyPoints();
    }
}

public interface Luck {
    double nextAttackLuckFactor();
}

public class LuckAttack implements Luck {
    private final Random random = new Random();
    private final static double MISS = 0;
    private final static double NORMAL = 1;
    private final static double LUCKY = 1.2;
    private final static double CRITICAL = 2;
    @Override
    public double nextAttackLuckFactor(){
        int randomFactor = random.nextInt(101);
        if (randomFactor > 0 & randomFactor <=3){
            return MISS;
        } else if (randomFactor > 3 & randomFactor <= 70){
            return NORMAL;
        } else if (randomFactor > 70 & randomFactor <= 96){
            return LUCKY;
        } else {
            return CRITICAL;
        }
    }
}

public class TestAttack {
    @Test
    public void validateMissAttack(){
        Luck missLuck =  new Luck() {
            @Override
            public double nextAttackLuckFactor() {
                return 0;
            }
        };
        UnitCharacter hero = new UnitCharacter("hero",new Energy(60),new Power(45,missLuck));
        UnitCharacter enemy = new UnitCharacter("enemy",new Energy(60),new Power(45,missLuck));
        hero.attack(enemy);
        Assert.assertEquals(enemy.getEnergyPoints(), 60);
    }
    @Test
    public void validateNormalAttack(){
        Luck normalLuck =  new Luck() {
            @Override
            public double nextAttackLuckFactor() {
                return 1;
            }
        };
        UnitCharacter hero = new UnitCharacter("hero",new Energy(60),new Power(45,normalLuck));
        UnitCharacter enemy = new UnitCharacter("enemy",new Energy(60),new Power(45,normalLuck));
        hero.attack(enemy);
        Assert.assertEquals(enemy.getEnergyPoints(), 45);
    }
    @Test
    public void validateLuckyAttack(){
        Luck luckyLuck =  new Luck() {
            @Override
            public double nextAttackLuckFactor() {
                return 1.2;
            }
        };
        UnitCharacter hero = new UnitCharacter("hero",new Energy(60),new Power(45,luckyLuck));
        UnitCharacter enemy = new UnitCharacter("enemy",new Energy(60),new Power(45,luckyLuck));
        hero.attack(enemy);
        Assert.assertEquals(enemy.getEnergyPoints(), 42);
    }
    @Test
    public void validateCriticalAttack(){
        Luck criticalLuck =  new Luck() {
            @Override
            public double nextAttackLuckFactor() {
                return 2;
            }
        };
        UnitCharacter hero = new UnitCharacter("hero",new Energy(60),new Power(45,criticalLuck));
        UnitCharacter enemy = new UnitCharacter("enemy",new Energy(60),new Power(45,criticalLuck));
        hero.attack(enemy);
        Assert.assertEquals(enemy.getEnergyPoints(), 30);
    }
}

Poste a sua solução!
Post original : TDD em prática desenvolvendo um jogo

25 Respostas

R

Legal esse desafio. Já está favoritado para eu fazer quando sobrar um tempo. Achou em algum site ou você mesmo que criou?

Observação: Muito bom o seu código.

dreampeppers99

RafaelViana:
Legal esse desafio. Já está favoritado para eu fazer quando sobrar um tempo. Achou em algum site ou você mesmo que criou?

Observação: Muito bom o seu código.


Obrigado pelos elogios. Eu mesmo criei, reduzi bastante minha ideia original. Sou fã de games, juntei a série Breath of Fire com Final Fantasy para nomear o “jogo” bem simples mesmo, bom pra exercitar o TDD! rsrsrsr

T

meus jogos preferidos hehehehe FF e BOF

dreampeppers99

O triste é saber que o Breath of Fire está fora dos planos da Capcom para lançar nova versão. :frowning:

T

O triste é saber que o Breath of Fire está fora dos planos da Capcom para lançar nova versão. :(
Triste mesmo. Final Fantasy já está uma *** nessa ultima versão.
Só acho que a chance de errar um ataque deve ser baseado em outros atributos, como por exemplo o “AGI” x “EVASION” do herói - inimigo.

dreampeppers99

O triste é saber que o Breath of Fire está fora dos planos da Capcom para lançar nova versão. :(
Triste mesmo. Final Fantasy já está uma *** nessa ultima versão.
Só acho que a chance de errar um ataque deve ser baseado em outros atributos, como por exemplo o “AGI” x “EVASION” do herói - inimigo.

Concordo com você sobre o Final Fantasy e sobre “errar o ataque também”, porém pensei em deixar o mais simples possível :smiley:

T

Uns 2 ou 3 anos atrás eu fiz um joguinho bem simples parecido com Final Fantasy Tactics, pena que foi em C/C++. Caso queira tenho o código em algum CD/DVD em algum local na minha casa hehehehe

Marky.Vasconcelos

Opa, amanhã eu penso melhor nisso. Gostei do desafio.

dreampeppers99

Então isso vai facilitar você no desafio.

D

Achei legal o desafio.

Fiz minha versão aqui, rapidamente. Não gostei de algumas coisas(os ifs da AttackFactory, por exemplo. Cheguei a usar o else porque um if embaixo do outro estava me dando agonia hehe). Usei TDD conforme descrito(sim, vi todos os testes falharem antes de começar a implementar), acho válido citar porque muita gente pode acabar fazendo o teste por último.

Enfim, abaixo minha solução:

Testes:

import static org.junit.Assert.assertEquals;

import org.junit.Before;
import org.junit.Test;

public class AttackTest {

	private Character playerOne;
	private Character playerTwo;

	@Before
	public void before() {
		playerOne = new Character("Player One", 60, 45);
		playerTwo = new Character("Player Two", 60, 45);
	}

	@Test
	public void testMissAttack01() {
		Attack attack = AttackFactory.getInstance(0);
		attack.attack(playerOne, playerTwo);
		assertEquals(60, playerTwo.getEnergy());
	}

	@Test
	public void testMissAttack02() {
		Attack attack = AttackFactory.getInstance(3);
		attack.attack(playerOne, playerTwo);
		assertEquals(60, playerTwo.getEnergy());
	}

	@Test
	public void testNormalAttack01() {
		Attack attack = AttackFactory.getInstance(4);
		attack.attack(playerOne, playerTwo);
		assertEquals(45, playerTwo.getEnergy());
	}
	
	@Test
	public void testNormalAttack02() {
		Attack attack = AttackFactory.getInstance(70);
		attack.attack(playerOne, playerTwo);
		assertEquals(45, playerTwo.getEnergy());
	}
	
	@Test
	public void testLuckyAttack01() {
		Attack attack = AttackFactory.getInstance(71);
		attack.attack(playerOne, playerTwo);
		assertEquals(42, playerTwo.getEnergy());
	}
	
	@Test
	public void testLuckyAttack02() {
		Attack attack = AttackFactory.getInstance(96);
		attack.attack(playerOne, playerTwo);
		assertEquals(42, playerTwo.getEnergy());
	}
	
	@Test
	public void testCriticalAttack01() {
		Attack attack = AttackFactory.getInstance(97);
		attack.attack(playerOne, playerTwo);
		assertEquals(30, playerTwo.getEnergy());
	}
	
	@Test
	public void testCriticalAttack02() {
		Attack attack = AttackFactory.getInstance(100);
		attack.attack(playerOne, playerTwo);
		assertEquals(30, playerTwo.getEnergy());
	}

}

Implementação:

public interface Attack {
	
	public void attack(Fighter attacker, Fighter target);

}
public class AttackFactory {

	private AttackFactory() {
		throw new AssertionError();
	}

	public static Attack getInstance(int lucky) {
		if (lucky >= 0 && lucky <= 3) {
			return new MissAttack();
		} else if (lucky >= 4 && lucky <= 70) {
			return new NormalAttack();
		} else if (lucky >= 71 && lucky <= 96) {
			return new LuckyAttack();
		} else if (lucky >= 97 && lucky <= 100) {
			return new CriticalAttack();
		}
		throw new IllegalArgumentException(String.format("Invalid lucky number. Expected a number between 0 and 100, but found %d", lucky));
	}

}
public class Character implements Fighter {

	private int energy;
	private int power;
	private String nick;

	public Character(String nick, int energy, int power) {
		this.nick = nick;
		this.power = power;
		this.energy = energy;
	}
	
	public String getNick() {
		return nick;
	}

	@Override
	public int getEnergy() {
		return energy;
	}

	@Override
	public void receiveDamage(int value) {
		energy -= value;
	}

	@Override
	public int getPower() {
		return power;
	}

}
public class CriticalAttack implements Attack {

	@Override
	public void attack(Fighter attacker, Fighter target) {
		int damage = attacker.getPower() / 3 * 2;
		target.receiveDamage(damage);
	}

}
public interface Fighter {
	
	public void receiveDamage(int damage);
	
	public int getPower();
	
	public int getEnergy();

}
public class LuckyAttack implements Attack {

	@Override
	public void attack(Fighter attacker, Fighter target) {
		int damage = (int) ((attacker.getPower() / 3) * 1.2);
		target.receiveDamage(damage);
	}

}
public class MissAttack implements Attack {

	@Override
	public void attack(Fighter attacker, Fighter target) {
		target.receiveDamage(0);
	}

}
public class NormalAttack implements Attack {

	@Override
	public void attack(Fighter attacker, Fighter target) {
		target.receiveDamage(attacker.getPower() / 3);
	}

}
D

Pensando aqui, eu mudaria os testes para iterar por todo o intervalo e não testar apenas as extremidades. É um pouco mais custoso, mas é o mais correto.

dreampeppers99

Gostei da sua solução. :smiley:
Você, antes, falava que não gostou do uso dos if’s para devolver a instância correta para o Attack.
Há muito tempo atrás eu estava tentando implementar um emulador de NES em java (JNesBR) e lá eu resolvi um problema parecido com esse de um modo “dispatch”.

public final void initInstructionTable() { //Register to Register Transfer. instructionSet[0xA8] = new TAYImplied(this); instructionSet[0xAA] = new TAXImplied(this); instructionSet[0xBA] = new TSXImplied(this); instructionSet[0x98] = new TYAImplied(this); instructionSet[0x8A] = new TXAImplied(this); instructionSet[0x9A] = new TXSImplied(this); //Load Register from Memory. instructionSet[0xA9] = new LDAImmediate(this); instructionSet[0xA5] = new LDAZeroPage(this); instructionSet[0xB5] = new LDAZeroPageX(this); instructionSet[0xAD] = new LDAAbsolute(this); instructionSet[0xBD] = new LDAAbsoluteIndexedX(this); //this check page change instructionSet[0xB9] = new LDAAbsoluteIndexedY(this); //this check page change instructionSet[0xA1] = new LDAIndexedIndirectX(this); instructionSet[0xB1] = new LDAIndirectIndexedY(this); //this check page change instructionSet[0xA2] = new LDXImmediate(this); instructionSet[0xA6] = new LDXZeroPage(this);

Dai quando fosse executar a instrução dado o opcode era simplesmente ir no array e pegar a instância relacionada com aquele código.

public final void step() { executeInstruction = getInstructionFrom(memory.read(programCounter)); executeInstruction.interpret(); cycles += executeInstruction.cycles(); }

Claro no caso dos ataques, que contém intervalos, sei lá talvez um map e repetindo a chave para o valor daria certo e evitaria os if’s. Mais uma vez sua solução foi muito boa.

ViniGodoy

Eu criaria um mock do gerador de números aleatórios, que só gerasse os números que eu quisesse. Como vc está testando o sistema de ataque, e não a classe random, isso tornaria os testes extremamente simples. Você poderia gerar números sequenciais e ver o que aconteceu em cada caso, garantindo assim o seu sistema com 100% de certeza.

Mas claro, para facilitar seria bom alterar um pouquinho as classes para que os randoms pudessem ser fornecidos, ou fossem criados por injeção de dependências. Caso contrário, vc seria obrigado a altera-los por reflexão.

dreampeppers99

ViniGodoy:
Eu criaria um mock do gerador de números aleatórios, que só gerasse os números que eu quisesse. Como vc está testando o sistema de ataque, e não a classe random, isso tornaria os testes extremamente simples. Você poderia gerar números sequenciais e ver o que aconteceu em cada caso, garantindo assim o seu sistema com 100% de certeza.

Mas claro, para facilitar seria bom alterar um pouquinho as classes para que os randoms pudessem ser fornecidos, ou fossem criados por injeção de dependências. Caso contrário, vc seria obrigado a altera-los por reflexão.

Eu (tentei) fiz foi isso mesmo o mock para o fator sorte!

Luck missLuck = new Luck() { @Override public double nextAttackLuckFactor() { return 0; } }; Luck normalLuck = new Luck() { @Override public double nextAttackLuckFactor() { return 1; } }; Luck luckyLuck = new Luck() { @Override public double nextAttackLuckFactor() { return 1.2; } }; Luck criticalLuck = new Luck() { @Override public double nextAttackLuckFactor() { return 2; } };

Felagund

opa, achei o desafio interessante, fiz umas bagaças aqui, ficou bem simples :stuck_out_tongue:

Classes

public class Character {
    private final String name;
    private final int energy;
    private final int power;

    public Character(String name, int energy, int power) {
        this.name = name;
        this.energy = energy;
        this.power = power;
    }

    public int getEnergy() {
        return energy;
    }

    public String getName() {
        return name;
    }

    public int getPower() {
        return power;
    }
    public int getAttack(AttackFactor factor){
        return (int) ((power / 3) * factor.getAttackFactor());   
    } 

public enum AttackFactor {

    MISS(0, 3, 0), NORMAL(4, 70, 1), LUCKY(71, 96, 1.2), CRITICAL(97, 100, 2);

    private int minimum;
    private int maximum;
    private double factor;
    private static final Random RANDOM = new Random();

    private AttackFactor(int minimum, int maximum, double factor) {
        this.minimum = minimum;
        this.maximum = maximum;
        this.factor = factor;
    }

    public double getAttackFactor() {
        return factor;
    }

    public static AttackFactor getRandomAttackFactor() {
        int randomFactor = RANDOM.nextInt(101);
        for (AttackFactor factor : AttackFactor.values()) {
            if(randomFactor >= factor.minimum && randomFactor <= factor.maximum){
                return factor;
            }
        }
        return null;
    }
}

e os testes

public class AttackTest {
    Character player;
    
    @Before
    public void setUp(){
        player = new Character("Player", 60, 45);
    }
    @Test
    public void testUserMissesAttackAnEnemy(){
        assertTrue(player.getAttack(AttackFactor.MISS) == 0);
    }
    @Test
    public void testUserAttackAnEnemy(){
        assertTrue(player.getAttack(AttackFactor.NORMAL) == 15);
    }
    @Test
    public void testUserHasLuckyAttackingAnEnemy(){
        assertTrue(player.getAttack(AttackFactor.LUCKY) == 18);
    }
    @Test
    public void testUserCriticalAttackingAnEnemy(){
        assertTrue(player.getAttack(AttackFactor.CRITICAL) == 30);
    }
}

public class AttackFactorTest {

    MyRandom myRandom = new MyRandom();

    @Before
    public void setUp() {
        try {
            Field field = AttackFactor.class.getDeclaredField("RANDOM");
            field.setAccessible(true);
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
            field.set(null, myRandom);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(AttackFactorTest.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(AttackFactorTest.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchFieldException ex) {
            Logger.getLogger(AttackFactorTest.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            Logger.getLogger(AttackFactorTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Test
    public void testReturnMissed() {
        myRandom.setRandomNumber(0);
        assertSame(AttackFactor.MISS, AttackFactor.getRandomAttackFactor());
        myRandom.setRandomNumber(1);
        assertSame(AttackFactor.MISS, AttackFactor.getRandomAttackFactor());
        myRandom.setRandomNumber(2);
        assertSame(AttackFactor.MISS, AttackFactor.getRandomAttackFactor());
        myRandom.setRandomNumber(3);
        assertSame(AttackFactor.MISS, AttackFactor.getRandomAttackFactor());
    }

    @Test
    public void testReturnNormal() {
        for (int i = 4; i < 71; i++) {
            myRandom.setRandomNumber(i);
            assertSame(AttackFactor.NORMAL, AttackFactor.getRandomAttackFactor());
        }
    }

    @Test
    public void testReturnLucky() {
        for (int i = 71; i < 97; i++) {
            myRandom.setRandomNumber(i);
            assertSame(AttackFactor.LUCKY, AttackFactor.getRandomAttackFactor());
        }
    }

    @Test
    public void testReturnCritical() {
        for (int i = 97; i < 101; i++) {
            myRandom.setRandomNumber(i);
            assertSame(AttackFactor.CRITICAL, AttackFactor.getRandomAttackFactor());
        }
    }

    private class MyRandom extends Random {

        private int randomNumber;

        public void setRandomNumber(int randomNumber) {
            this.randomNumber = randomNumber;
        }

        @Override
        public int nextInt(int n) {
            return randomNumber;
        }
    }
}

Gostei muito dos codigos do autor do topico, mas não fiz as classes separadas pra não ficar igual :stuck_out_tongue:

feliperod

Interessante o desafio! Vou olhar com mais calma depois!

dreampeppers99

Seu código ficou muito bom, mesmo com esse código alienigena rsrsrsr

modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
Felagund

Seu código ficou muito bom, mesmo com esse código alienigena rsrsrsr

modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

asuhduhasdasduhuhasuhd, reflection :), é pra poder sobrescrever a variavel Random dentro do enum assim posso simular o comportamento.
Nunca tinha usado isso, achei esse codigo no Stack Overflow :P. Até pensei em deixar aberto para definir o Random, mas em termos de Design não seria o ideal :slight_smile:

dreampeppers99

Me lembra as máscaras de bits da época dos compiladores e outros. :slight_smile:
O código ficou muito bom.

edao69

Muito interessante mesmo. Tambem gosto muito de games.Muito bom!

ViniGodoy

Mas há uma diferença aqui. A classe Luck é uma classe de negócio, portanto, precisa ser testada. A classe Random é uma classe do Java, seu comportamento é garantido. Você não deve fazer mocks para o que deve ser testado, só para o que não deve. A menos, claro, que haja um outro JUnit só para a classe Luck.

dreampeppers99

ViniGodoy:
dreampeppers99:

Eu (tentei) fiz foi isso mesmo o mock para o fator sorte!

Mas há uma diferença aqui. A classe Luck é uma classe de negócio, portanto, precisa ser testada. A classe Random é uma classe do Java, seu comportamento é garantido. Você não deve fazer mocks para o que deve ser testado, só para o que não deve. A menos, claro, que haja um outro JUnit só para a classe Luck.


Faz total sentido, eu até poderia ter criado um wrapper ou algo assim pra ser o meu Random e passar esse random para o LuckAttack como o mock, obrigado pela dica.

dreampeppers99

Interessante vai ser sua implementação Eder :smiley: tenta aí !

Edufa

Vou pensar em algo para postar, mas uma das coisas mais interessantes desta proposta é ter enfatizado o uso de testes.

dreampeppers99

Extamente testes para fazer só o necessário nada mais.

Criado 26 de abril de 2011
Ultima resposta 28 de abr. de 2011
Respostas 25
Participantes 10