🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of jlacar

jlacar's solution

to D&D Character in the Java Track

Published at May 04 2019 · 0 comments
Instructions
Test suite
Solution

For a game of Dungeons & Dragons, each player starts by generating a character they can play with. This character has, among other things, six abilities; strength, dexterity, constitution, intelligence, wisdom and charisma. These six abilities have scores that are determined randomly. You do this by rolling four 6-sided dice and record the sum of the largest three dice. You do this six times, once for each ability.

Your character's initial hitpoints are 10 + your character's constitution modifier. You find your character's constitution modifier by subtracting 10 from your character's constitution, divide by 2 and round down.

Write a random character generator that follows the rules above.

For example, the six throws of four dice may look like:

  • 5, 3, 1, 6: You discard the 1 and sum 5 + 3 + 6 = 14, which you assign to strength.
  • 3, 2, 5, 3: You discard the 2 and sum 3 + 5 + 3 = 11, which you assign to dexterity.
  • 1, 1, 1, 1: You discard the 1 and sum 1 + 1 + 1 = 3, which you assign to constitution.
  • 2, 1, 6, 6: You discard the 1 and sum 2 + 6 + 6 = 14, which you assign to intelligence.
  • 3, 5, 3, 4: You discard the 3 and sum 5 + 3 + 4 = 12, which you assign to wisdom.
  • 6, 6, 6, 6: You discard the 6 and sum 6 + 6 + 6 = 18, which you assign to charisma.

Because constitution is 3, the constitution modifier is -4 and the hitpoints are 6.

Notes

Most programming languages feature (pseudo-)random generators, but few programming languages are designed to roll dice. One such language is Troll.

Setup

Go through the setup instructions for Java to install the necessary dependencies:

https://exercism.io/tracks/java/installation

Running the tests

You can run all the tests for an exercise by entering the following in your terminal:

$ gradle test

Use gradlew.bat if you're on Windows

In the test suites all tests but the first have been skipped.

Once you get a test passing, you can enable the next one by removing the @Ignore("Remove to run test") annotation.

Source

Simon Shine, Erik Schierboom https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

DnDCharacterTest.java

import org.junit.Ignore;
import org.junit.Test;

import static org.junit.Assert.*;

public class DnDCharacterTest {

    private DnDCharacter dndCharacter = new DnDCharacter();

    @Test
    public void testAbilityModifierForScore3IsNegative4() {
        assertEquals(-4, dndCharacter.modifier(3));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore4IsNegative3() {
        assertEquals(-3, dndCharacter.modifier(4));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore5IsNegative3() {
        assertEquals(-3, dndCharacter.modifier(5));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore6IsNegative2() {
        assertEquals(-2, dndCharacter.modifier(6));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore7IsNegative2() {
        assertEquals(-2, dndCharacter.modifier(7));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore8IsNegative1() {
        assertEquals(-1, dndCharacter.modifier(8));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore9IsNegative1() {
        assertEquals(-1, dndCharacter.modifier(9));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore10Is0() {
        assertEquals(0, dndCharacter.modifier(10));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore11Is0() {
        assertEquals(0, dndCharacter.modifier(11));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore12Is1() {
        assertEquals(1, dndCharacter.modifier(12));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore13Is1() {
        assertEquals(1, dndCharacter.modifier(13));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore14Is2() {
        assertEquals(2, dndCharacter.modifier(14));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore15Is2() {
        assertEquals(2, dndCharacter.modifier(15));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore16Is3() {
        assertEquals(3, dndCharacter.modifier(16));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore17Is3() {
        assertEquals(3, dndCharacter.modifier(17));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAbilityModifierForScore18Is4() {
        assertEquals(4, dndCharacter.modifier(18));
    }

    @Ignore("Remove to run test")
    @Test
    public void testRandomAbilityIsWithinRange() {
        int score = dndCharacter.ability();
        assertTrue(score > 2 && score < 19);
    }

    @Ignore("Remove to run test")
    @Test
    public void testRandomCharacterIsValid() {
        for (int i = 0; i < 1000; i++) {
            DnDCharacter character = new DnDCharacter();
            assertTrue(character.getStrength() > 2 && character.getStrength() < 19);
            assertTrue(character.getDexterity() > 2 && character.getDexterity() < 19);
            assertTrue(character.getConstitution() > 2 && character.getConstitution() < 19);
            assertTrue(character.getIntelligence() > 2 && character.getIntelligence() < 19);
            assertTrue(character.getWisdom() > 2 && character.getWisdom() < 19);
            assertTrue(character.getCharisma() > 2 && character.getCharisma() < 19);
            assertEquals(character.getHitpoints(),
                    10 + character.modifier(character.getConstitution()));
        }
    }

    @Ignore("Remove to run test")
    @Test
    public void testEachAbilityIsOnlyCalculatedOnce() {
        assertEquals(dndCharacter.getStrength(), dndCharacter.getStrength());
    }

}

src/main/java/DnDCharacter.java

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static java.util.stream.Collectors.summingInt;

class DnDCharacter {

    private enum Ability {
        STRENGTH, DEXTERITY, CONSTITUTION, INTELLIGENCE, WISDOM, CHARISMA;
    }

    private final Map<Ability, Integer> abilities;

    private final Dice dice = new Dice();

    DnDCharacter() {
        abilities = new HashMap<>();
        Arrays.stream(Ability.values()).forEach(ability ->
                abilities.put(ability, sumOflargest(3, dice.roll(4)))
        );
    }

    /** Returns the value of a random ability */
    int ability() {
        return abilities.get(Ability.values()[dice.roll() - 1]);
    }

    int modifier(int input) {
        return Math.floorDiv(input - 10, 2);
    }

    int getStrength() {
        return abilities.get(Ability.STRENGTH);
    }

    int getDexterity() {
        return abilities.get(Ability.DEXTERITY);
    }

    int getConstitution() {
        return abilities.get(Ability.CONSTITUTION);
    }

    int getIntelligence() {
        return abilities.get(Ability.INTELLIGENCE);
    }

    int getWisdom() {
        return abilities.get(Ability.WISDOM);
    }

    int getCharisma() {
        return abilities.get(Ability.CHARISMA);
    }

    int getHitpoints() {
        return 10 + modifier(getConstitution());
    }

    /* package-private access to facilitate unit testing */
    int sumOflargest(Integer n, Integer... rolls) {
        Arrays.sort(rolls, Collections.reverseOrder());
        return Arrays.stream(rolls)
                .limit(n)
                .collect(summingInt(Integer::intValue));
    }

}

class Dice {
    private static final int SIDES = 6;

    private final Random rand = new Random();

    int roll() {
        return rand.nextInt(SIDES) + 1;
    }

    Integer[] roll(int times) {
        return IntStream.range(0, times)
                .map(n -> roll())
                .boxed().toArray(Integer[]::new);
    }
}

src/test/java/DnDCharacterTest.java

import org.junit.Ignore;
import org.junit.Test;

import java.util.List;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;

public class DnDCharacterTest {

    private DnDCharacter dndCharacter = new DnDCharacter();

    @Test
    public void testAbilityModifierForScore3IsNegative4() {
        assertEquals(-4, dndCharacter.modifier(3));
    }

    @Test
    public void testAbilityModifierForScore4IsNegative3() {
        assertEquals(-3, dndCharacter.modifier(4));
    }

    @Test
    public void testAbilityModifierForScore5IsNegative3() {
        assertEquals(-3, dndCharacter.modifier(5));
    }

    @Test
    public void testAbilityModifierForScore6IsNegative2() {
        assertEquals(-2, dndCharacter.modifier(6));
    }

    @Test
    public void testAbilityModifierForScore7IsNegative2() {
        assertEquals(-2, dndCharacter.modifier(7));
    }

    @Test
    public void testAbilityModifierForScore8IsNegative1() {
        assertEquals(-1, dndCharacter.modifier(8));
    }

    @Test
    public void testAbilityModifierForScore9IsNegative1() {
        assertEquals(-1, dndCharacter.modifier(9));
    }

    @Test
    public void testAbilityModifierForScore10Is0() {
        assertEquals(0, dndCharacter.modifier(10));
    }

    @Test
    public void testAbilityModifierForScore11Is0() {
        assertEquals(0, dndCharacter.modifier(11));
    }

    @Test
    public void testAbilityModifierForScore12Is1() {
        assertEquals(1, dndCharacter.modifier(12));
    }

    @Test
    public void testAbilityModifierForScore13Is1() {
        assertEquals(1, dndCharacter.modifier(13));
    }

    @Test
    public void testAbilityModifierForScore14Is2() {
        assertEquals(2, dndCharacter.modifier(14));
    }

    @Test
    public void testAbilityModifierForScore15Is2() {
        assertEquals(2, dndCharacter.modifier(15));
    }

    @Test
    public void testAbilityModifierForScore16Is3() {
        assertEquals(3, dndCharacter.modifier(16));
    }

    @Test
    public void testAbilityModifierForScore17Is3() {
        assertEquals(3, dndCharacter.modifier(17));
    }

    @Test
    public void testAbilityModifierForScore18Is4() {
        assertEquals(4, dndCharacter.modifier(18));
    }

    @Test
    public void testRandomAbilityIsWithinRange() {
        for (int i = 0; i < 1000; i++) {
            int score = dndCharacter.ability();
            assertTrue(score > 2 && score < 19);
        }
    }

    @Test
    public void testRandomCharacterIsValid() {
        for (int i = 0; i < 1000; i++) {
            DnDCharacter character = new DnDCharacter();
            assertTrue(character.getStrength() > 2 && character.getStrength() < 19);
            assertTrue(character.getDexterity() > 2 && character.getDexterity() < 19);
            assertTrue(character.getConstitution() > 2 && character.getConstitution() < 19);
            assertTrue(character.getIntelligence() > 2 && character.getIntelligence() < 19);
            assertTrue(character.getWisdom() > 2 && character.getWisdom() < 19);
            assertTrue(character.getCharisma() > 2 && character.getCharisma() < 19);
            assertEquals(character.getHitpoints(),
                    10 + character.modifier(character.getConstitution()));
        }
    }

    @Test
    public void testEachAbilityIsOnlyCalculatedOnce() {
        assertEquals(dndCharacter.getStrength(), dndCharacter.getStrength());
    }

    @Test
    public void sumOfLargest3of4_any_order() {
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{1, 3, 5, 6}), is(14));
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{5, 3, 1, 6}), is(14));
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{6, 3, 5, 1}), is(14));
    }

    @Test
    public void sumOfLargest3of4_with_double_min_values() {
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{3, 2, 6, 2}), is(11));
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{2, 2, 6, 3}), is(11));
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{5, 4, 2, 2}), is(11));
    }

    @Test
    public void sumOfLargest3of4_with_double_max_values() {
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{1, 2, 6, 6}), is(14));
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{6, 2, 6, 1}), is(14));
        assertThat(dndCharacter.sumOflargest(3, new Integer[]{6, 6, 1, 2}), is(14));
    }

    @Test
    public void sumOfLargest3of4_with_all_same_values() {
        assertThat(dndCharacter.sumOflargest(3, new Integer[] {1, 1, 1, 1}), is(3));
        assertThat(dndCharacter.sumOflargest(3, new Integer[] {6, 6, 6, 6}), is(18));
    }

}

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?