exercism

Exercism - D&D Character

This post shows you how to get D&D Character exercise of Exercism.

Stevinator Stevinator
15 min read
SHARE
exercism dart flutter dandd-character

Preparation

Before we click on our next exercise, let’s see what concepts of DART we need to consider

D&D Character Exercise

So we need to use the following concepts.

Factory Constructors

Factory constructors allow you to create instances with custom logic. They can return instances of the class or even instances of subclasses. They’re perfect for creating objects with complex initialization.

class DndCharacter {
  final int strength;
  final int dexterity;
  
  // Private constructor
  DndCharacter._({required this.strength, required this.dexterity});
  
  // Factory constructor - creates instance with random values
  factory DndCharacter.create() {
    return DndCharacter._(
      strength: _rollAbility(),
      dexterity: _rollAbility(),
    );
  }
  
  static int _rollAbility() {
    // Roll 4d6, drop lowest, sum top 3
    return 14; // Simplified
  }
}

void main() {
  // Use factory constructor
  DndCharacter character = DndCharacter.create();
  print(character.strength); // Random value
}

Private Constructors

Private constructors (starting with _) can only be called from within the same library. They’re used to prevent direct instantiation, forcing use of factory constructors or static methods.

class DndCharacter {
  final int strength;
  
  // Private constructor - cannot be called from outside
  DndCharacter._({required this.strength});
  
  // Factory constructor - only way to create instances
  factory DndCharacter.create() {
    return DndCharacter._(strength: 14);
  }
}

void main() {
  // OK - use factory
  DndCharacter c1 = DndCharacter.create();
  
  // Error - private constructor
  // DndCharacter c2 = DndCharacter._(strength: 14);
}

Random Number Generation

The Random class from dart:math generates random numbers. It’s essential for simulating dice rolls.

import 'dart:math';

void main() {
  final random = Random();
  
  // Generate random integer from 0 to 5
  int value = random.nextInt(6);
  print(value); // 0, 1, 2, 3, 4, or 5
  
  // Roll 6-sided die (1-6)
  int dieRoll = random.nextInt(6) + 1;
  print(dieRoll); // 1, 2, 3, 4, 5, or 6
  
  // Roll multiple dice
  List<int> rolls = List.generate(4, (_) => random.nextInt(6) + 1);
  print(rolls); // [3, 5, 2, 6]
}

List generate() Method

The List.generate() method creates a list by calling a function for each index. It’s perfect for generating multiple dice rolls.

void main() {
  // Generate list of 4 random dice rolls
  final random = Random();
  List<int> rolls = List.generate(4, (_) => random.nextInt(6) + 1);
  print(rolls); // [3, 5, 2, 6]
  
  // Generate list with index
  List<int> squares = List.generate(5, (i) => i * i);
  print(squares); // [0, 1, 4, 9, 16]
}

List sort() Method

The sort() method sorts a list in place in ascending order. It’s used to order dice rolls so we can drop the lowest.

void main() {
  List<int> rolls = [5, 3, 1, 6];
  
  // Sort in ascending order
  rolls.sort();
  print(rolls); // [1, 3, 5, 6]
  
  // Use for dice: sort to find lowest
  List<int> dice = [5, 3, 1, 6];
  dice.sort(); // [1, 3, 5, 6]
  // Drop lowest (first element), sum rest
  int sum = dice.skip(1).reduce((a, b) => a + b);
  print(sum); // 14 (3 + 5 + 6)
}

Iterable skip() Method

The skip() method skips the first n elements of an iterable. It’s used to drop the lowest die roll.

void main() {
  List<int> rolls = [1, 3, 5, 6];
  
  // Skip first element (lowest)
  var topThree = rolls.skip(1);
  print(topThree.toList()); // [3, 5, 6]
  
  // Use with reduce to sum
  int sum = rolls.skip(1).reduce((a, b) => a + b);
  print(sum); // 14
}

Extension Methods

Extension methods allow you to add new functionality to existing types without modifying their source code. They’re perfect for adding utility methods like sum to iterables.

extension IntExtensions on int {
  // Add method to int
  String get withSign => this >= 0 ? '+$this' : '$this';
}

extension ListIntExtensions on Iterable<int> {
  // Add sum property to iterables of int
  int get sum => reduce((a, b) => a + b);
}

void main() {
  // Use extension on int
  int modifier = 3;
  print(modifier.withSign); // '+3'
  
  int negative = -4;
  print(negative.withSign); // '-4'
  
  // Use extension on iterable
  List<int> numbers = [3, 5, 6];
  int total = numbers.sum;
  print(total); // 14
}

Getters

Getters are properties that compute and return a value. They’re perfect for calculated properties like ability modifiers.

class DndCharacter {
  final int strength;
  
  DndCharacter(this.strength);
  
  // Getter - calculates modifier
  int get strengthModifier => ((strength - 10) / 2).floor();
  
  // Getter - uses another getter
  int get hitpoints => 10 + strengthModifier;
}

void main() {
  DndCharacter character = DndCharacter(14);
  print(character.strengthModifier); // 2
  print(character.hitpoints); // 12
}

Static Methods

Static methods belong to the class itself, not to instances. They can be called without creating an object. They’re perfect for utility functions.

class DndCharacter {
  // Static method - can be called on class
  static int modifier(int score) {
    return ((score - 10) / 2).floor();
  }
  
  // Static method for rolling ability
  static int ability() {
    // Roll 4d6, drop lowest, sum
    return 14;
  }
}

void main() {
  // Call static method without instance
  int mod = DndCharacter.modifier(14);
  print(mod); // 2
  
  int ability = DndCharacter.ability();
  print(ability); // 14
}

Floor Division

Floor division rounds down to the nearest integer. In Dart, you can use (a / b).floor() or a ~/ b for integer division. It’s used for calculating ability modifiers.

void main() {
  // Floor division
  double result = (14 - 10) / 2; // 2.0
  int floor = result.floor(); // 2
  
  // Integer division (also floors)
  int intDiv = (14 - 10) ~/ 2; // 2
  
  // Negative example
  int negative = (8 - 10) ~/ 2; // -1 (not -2)
  
  // Use for modifiers
  int modifier(int score) => ((score - 10) / 2).floor();
  print(modifier(14)); // 2
  print(modifier(8)); // -1
}

@override Annotation

The @override annotation indicates that a method overrides a method from a superclass. It’s used when overriding toString().

class DndCharacter {
  final int strength;
  
  DndCharacter(this.strength);
  
  @override
  String toString() {
    return 'DndCharacter(strength: $strength)';
  }
}

void main() {
  DndCharacter character = DndCharacter(14);
  print(character); // DndCharacter(strength: 14)
  print(character.toString()); // Same output
}

Initializer Lists

Initializer lists allow you to initialize fields before the constructor body runs. They’re perfect for calculated fields like hitpoints.

class DndCharacter {
  final int constitution;
  final int hitpoints;
  
  // Initializer list calculates hitpoints
  DndCharacter(this.constitution) 
    : hitpoints = 10 + modifier(constitution);
  
  static int modifier(int score) => ((score - 10) / 2).floor();
}

void main() {
  DndCharacter character = DndCharacter(14);
  print(character.hitpoints); // 12 (10 + 2)
}

Introduction

After weeks of anticipation, you and your friends get together for your very first game of Dungeons & Dragons (D&D). Since this is the first session of the game, each player has to generate a character to play with. The character’s abilities are determined by rolling 6-sided dice, but where are the dice? With a shock, you realize that your friends are waiting for you to produce the dice; after all it was your idea to play D&D! Panicking, you realize you forgot to bring the dice, which would mean no D&D game. As you have some basic coding skills, you quickly come up with a solution: you’ll write a program to simulate dice rolls.

What is Dungeons & Dragons?

Dungeons & Dragons (commonly abbreviated as D&D or DnD) is a fantasy tabletop role-playing game (RPG) originally designed by Gary Gygax and Dave Arneson. The game was first published in 1974 by Tactical Studies Rules, Inc. (TSR). It has been published by Wizards of the Coast (now a subsidiary of Hasbro) since 1997. The game was derived from miniature wargames, with a variation of the 1971 game Chainmail serving as the initial rule system. D&D’s publication is commonly recognized as the beginning of modern role-playing games and the role-playing game industry.

D&D departs from traditional wargaming by allowing each player to create their own character to play instead of a military formation. These characters embark upon adventures within a fantasy setting. A Dungeon Master (DM) serves as the game’s referee and storyteller, while maintaining the setting in which the adventures occur, and playing the role of the inhabitants of the game world, also referred to as non-player characters (NPCs).

— Wikipedia

Instructions

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 recording 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 above rules.

Example

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.

How do we generate a D&D character?

To generate a D&D character:

  1. Roll ability scores: For each of the 6 abilities (strength, dexterity, constitution, intelligence, wisdom, charisma):

    • Roll 4 six-sided dice (4d6)
    • Sort the rolls
    • Drop the lowest roll
    • Sum the remaining 3 rolls
    • Assign to the ability
  2. Calculate modifiers: For each ability score:

    • Modifier = floor((score - 10) / 2)
    • Example: score 14 → (14-10)/2 = 2
    • Example: score 8 → (8-10)/2 = -1
  3. Calculate hitpoints:

    • Hitpoints = 10 + constitution modifier
    • Example: constitution 14 (modifier +2) → 10 + 2 = 12
    • Example: constitution 8 (modifier -1) → 10 + (-1) = 9

The key insight is using the “4d6 drop lowest” method to generate ability scores, which gives a range of 3-18 with a bell curve distribution (most scores around 10-12).

Solution

import 'dart:math';

class DndCharacter {
  final int strength;
  final int dexterity;
  final int constitution;
  final int intelligence;
  final int wisdom;
  final int charisma;
  final int hitpoints;

  DndCharacter._({
    required this.strength,
    required this.dexterity,
    required this.constitution,
    required this.intelligence,
    required this.wisdom,
    required this.charisma,
  }) : hitpoints = 10 + modifier(constitution);

  factory DndCharacter.create() {
    return DndCharacter._(
      strength: ability(),
      dexterity: ability(),
      constitution: ability(),
      intelligence: ability(),
      wisdom: ability(),
      charisma: ability(),
    );
  }

  static int modifier(int score) => ((score - 10) / 2).floor();

  static int ability() => DiceRoller.roll4d6DropLowest();

  // Getters for modifiers
  int get strengthModifier => modifier(strength);
  int get dexterityModifier => modifier(dexterity);
  int get constitutionModifier => modifier(constitution);
  int get intelligenceModifier => modifier(intelligence);
  int get wisdomModifier => modifier(wisdom);
  int get charismaModifier => modifier(charisma);

  @override
  String toString() {
    return '''
DndCharacter:
  Strength: $strength (${strengthModifier.withSign})
  Dexterity: $dexterity (${dexterityModifier.withSign})
  Constitution: $constitution (${constitutionModifier.withSign})
  Intelligence: $intelligence (${intelligenceModifier.withSign})
  Wisdom: $wisdom (${wisdomModifier.withSign})
  Charisma: $charisma (${charismaModifier.withSign})
  Hitpoints: $hitpoints
''';
  }
}

class DiceRoller {
  static final _random = Random();

  static int rollD6() => _random.nextInt(6) + 1;

  static int roll4d6DropLowest() {
    final rolls = List.generate(4, (_) => rollD6());
    rolls.sort();
    return rolls.skip(1).sum;
  }

  static List<int> rollMultiple(int count, int sides) {
    return List.generate(count, (_) => _random.nextInt(sides) + 1);
  }
}

extension IntExtensions on int {
  String get withSign => this >= 0 ? '+$this' : '$this';
}

extension ListIntExtensions on Iterable<int> {
  int get sum => reduce((a, b) => a + b);
  int get average => sum ~/ length;
}

Let’s break down the solution:

  1. import 'dart:math' - Import Random:

    • Imports Random class for generating random numbers
    • Used for simulating dice rolls
  2. class DndCharacter - Main character class:

    • Encapsulates a D&D character with all abilities
    • Stores ability scores and calculated hitpoints
  3. final int strength, dexterity, ... - Ability scores:

    • Six ability scores as final fields
    • Cannot be changed after creation
    • Range: 3-18 (from 4d6 drop lowest)
  4. final int hitpoints - Hitpoints:

    • Calculated from constitution modifier
    • Initial hitpoints = 10 + constitution modifier
  5. DndCharacter._({...}) - Private constructor:

    • Private constructor (starts with _)
    • Takes all ability scores as required named parameters
    • Cannot be called directly from outside
  6. : hitpoints = 10 + modifier(constitution) - Initializer list:

    • Calculates hitpoints before constructor body runs
    • Uses static modifier() method
    • Example: constitution 14 → modifier 2 → hitpoints 12
  7. factory DndCharacter.create() - Factory constructor:

    • Public factory constructor
    • Only way to create characters from outside
    • Generates random ability scores
  8. strength: ability(), dexterity: ability(), ... - Generate abilities:

    • Calls ability() static method for each ability
    • Each call generates a new random score (3-18)
  9. static int modifier(int score) => ((score - 10) / 2).floor() - Modifier calculation:

    • Static method for calculating ability modifiers
    • Formula: floor((score - 10) / 2)
    • Expression-bodied method (concise syntax)
    • Example: 14 → (14-10)/2 = 2
    • Example: 8 → (8-10)/2 = -1
  10. static int ability() => DiceRoller.roll4d6DropLowest() - Ability generation:

    • Static method that generates one ability score
    • Delegates to DiceRoller class
    • Returns value from 3-18
  11. int get strengthModifier => modifier(strength) - Modifier getters:

    • Getter properties for each ability modifier
    • Calculates modifier on demand
    • Example: strength 14 → strengthModifier 2
  12. @override String toString() - String representation:

    • Overrides toString() for readable output
    • Shows all abilities with modifiers
    • Uses withSign extension for formatted modifiers
  13. class DiceRoller - Dice rolling utility:

    • Encapsulates dice rolling logic
    • Uses static methods (no instance needed)
  14. static final _random = Random() - Random instance:

    • Static final Random instance
    • Shared across all dice rolls
    • Initialized once
  15. static int rollD6() => _random.nextInt(6) + 1 - Roll one die:

    • Rolls a single 6-sided die
    • nextInt(6) returns 0-5, add 1 for 1-6
    • Expression-bodied method
  16. static int roll4d6DropLowest() - Roll ability score:

    • Implements the 4d6 drop lowest method
    • Generates 4 dice rolls
    • Sorts to find lowest
    • Drops lowest and sums remaining
  17. final rolls = List.generate(4, (_) => rollD6()) - Generate rolls:

    • Creates list of 4 dice rolls
    • List.generate(4, ...) creates 4 elements
    • Each element is result of rollD6()
    • Example: [5, 3, 1, 6]
  18. rolls.sort() - Sort rolls:

    • Sorts rolls in ascending order
    • Modifies list in place
    • Example: [5, 3, 1, 6] → [1, 3, 5, 6]
  19. return rolls.skip(1).sum - Drop lowest and sum:

    • skip(1) skips first element (lowest)
    • sum extension property sums remaining
    • Example: [1, 3, 5, 6] → skip(1) → [3, 5, 6] → sum = 14
  20. extension IntExtensions on int - Int extension:

    • Adds withSign property to int
    • Formats numbers with sign (+ or -)
    • Example: 2 → ‘+2’, -4 → ‘-4’
  21. String get withSign => this >= 0 ? '+$this' : '$this' - Sign formatting:

    • Adds ’+’ prefix for positive/zero
    • Keeps ’-’ for negative
    • Used in toString() output
  22. extension ListIntExtensions on Iterable<int> - Iterable extension:

    • Adds sum and average properties
    • Makes summing lists more readable
  23. int get sum => reduce((a, b) => a + b) - Sum property:

    • Sums all elements in iterable
    • Uses reduce() to accumulate values
    • Example: [3, 5, 6].sum → 14
  24. int get average => sum ~/ length - Average property:

    • Calculates average (bonus utility)
    • Uses integer division (~/)

The solution elegantly generates D&D characters using factory constructors, static methods, and extension methods. The “4d6 drop lowest” method creates realistic ability scores with a bell curve distribution, and the modifier system follows standard D&D rules.


A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉

Visit My YouTube Channel
Stevinator

Stevinator

Stevinator is a software engineer passionate about clean code and best practices. Loves sharing knowledge with the developer community.