Give me Monsters! (Part 2)
25 Jul 2018Time to start creating monsters! We will begin with the Abilities section of the Stat Block.
First, what are Abilities? Every creature is described by 6 Ability Scores, which describe by a number from 1 to 20 (or possibly more) how able the creature is, across 6 dimensions:
- STR (Strength)
- DEX (Dexterity)
- CON (Constitution)
- INT (Intelligence)
- WIS (Wisdom)
- CHA (Charisma)
Ability scores have a dual usage: as raw scores, and as Ability Modifiers, which indicate a bonus or malus for that ability (see the rules for details). As an example, the Goblin has an INT score of 10, which is perfectly average and gives a modifier of 0; his DEX is 14, giving him a +2 bonus, and a CHA of 8, with a malus of -1.
We need to store 6 values, one for each Ability Score. That seems like a good case for a record:
type Scores = {
STR: int
DEX: int
CON: int
INT: int
WIS: int
CHA: int
}
Modifiers
How would we go about getting the corresponding modifiers?
There are 2 parts to the problem: converting a score to a modifier, and getting the modifier for each ability. We could of course mechanically transcribe the rules, and convert along these lines:
let scoreToModifier (score:int) =
if score <= 1 then -5
elif score <= 3 then -4
elif score <= 5 then -3
// more of it, omitted for brevity
This is a bit silly, however: there is a clear pattern here, with modifiers moving by 1 as scores move by 2. So instead, we’ll go for a compact, albeit perhaps less immediately readable version:
let scoreToModifier score =
(score / 2) - 5
|> min 10
|> max -5
As an aside, D&D conveniently uses the rule “always round down”, which happens to fit quite nicely with the way integer operations are handled in .NET.
To extract modifiers from Ability Scores, we would like to be able to request the modifier for any Ability. This sounds like a good case for a discriminated union:
type Ability =
| STR
| DEX
| CON
| INT
| WIS
| CHA
Armed with this, all we need now is to write a function, which, given an Ability, will retrieve the correct score, and convert it to a modifier:
let modifier scores ability =
match ability with
| STR -> scores.STR
| DEX -> scores.DEX
| CON -> scores.CON
| INT -> scores.INT
| WIS -> scores.WIS
| CHA -> scores.CHA
|> scoreToModifier
We can replicate the Goblin case now:
let goblin = {
STR = 8
DEX = 14
CON = 10
INT = 10
WIS = 8
CHA = 8
}
modifier goblin STR // -1
Basic Markdown rendering
One of the reasons I want to create my own model of monsters, is to easily create cards for them, formatted the way I want. Let’s take a stab at that, formatting the Abilities using Markdown.
Using a table to display the Abilities seems reasonable; we would represent our Goblin along these lines:
STR | DEX | CON | INT | WIS | CHA
:---: | :---: | :---: | :---: | :---: | :---:
8 | 14 | 10 | 10 | 8 | 8
-1 | +2 | 0 | 0 | -1 | -1
To achieve this, we need to iterate over the list of abilities, in a predictable order, extract and format the score and the modifier, and join them with a column separator, |
. We’ll need a couple of small things here. First, we need a list to iterate on. Then, we cannot access the score for a specific Ability yet. Finally, we would like to be explicit about modifiers, and preprend a +
sign in front of positive multipliers, for easier reading.
Let’s get to work. First, let’s write a score
function, and refactor modifier
accordingly:
let score scores ability =
match ability with
| STR -> scores.STR
| DEX -> scores.DEX
| CON -> scores.CON
| INT -> scores.INT
| WIS -> scores.WIS
| CHA -> scores.CHA
let modifier scores ability =
ability
|> score scores
|> scoreToModifier
Let’s also define a canonical list of Abilities:
let abilities = [ STR; DEX; CON; INT; WIS; CHA ]
… and attack the markdown part:
[<RequireQualifiedAccess>]
module Markdown =
let signed (value) =
if value > 0
then sprintf "+%i" value
else sprintf "%i" value
let abilities (scores:Scores) =
[
abilities
|> List.map (sprintf "%A")
|> String.concat " | "
abilities
|> List.map (fun _ -> ":---:")
|> String.concat " | "
abilities
|> List.map (score scores >> sprintf "%i")
|> String.concat " | "
abilities
|> List.map (modifier scores >> signed)
|> String.concat " | "
]
|> String.concat " \n"
And we are done - we can now run this:
goblin |> Markdown.abilities
… which produces the following:
STR | DEX | CON | INT | WIS | CHA |
---|---|---|---|---|---|
8 | 14 | 10 | 10 | 8 | 8 |
-1 | +2 | 0 | 0 | -1 | -1 |
Going a bit further
We have enough here to represent a creature’s abilities, and could stop here. However, the representation of an Adventurer or Monster by its abilities scores hides a small issue, namely that raw scores are sometimes modified by score bonuses.
Let me provide two examples. In the case of Adventurers, some races come with Ability Score Increases. For instance, any Elf will receive a +2
bonus on its DEX
score. In the case of Monsters, it is common to create “variants” of a Monster by modifying their standard Ability Scores. For instance, one could create a “Goblin Boss” (Monster Manual, p166), which has a STR
of 10 instead of 8, and a CHA
of 10 instead of 8.
If we bake both the original score and the bonus together into the Ability Score, we loose some information which could be valuable when modifying a creature.
How could we avoid that? The approach I took was to separate explicitly these two parts, along these lines:
type ScoreBonus = {
Ability: Ability
Bonus: int
}
type Abilities = {
Scores: Scores
Bonuses: ScoreBonus list
}
let score abilities ability =
let baseScore =
let scores = abilities.Scores
match ability with
| STR -> scores.STR
| DEX -> scores.DEX
| CON -> scores.CON
| INT -> scores.INT
| WIS -> scores.WIS
| CHA -> scores.CHA
let bonuses =
abilities.Bonuses
|> List.sumBy (fun bonus ->
if bonus.Ability = ability
then bonus.Bonus
else 0)
baseScore + bonuses
This allows me then to do things like this, where I can define a Goblin Boss as “A Goblin, with a STR and CHA bonuses”:
let goblin = {
Scores = {
STR = 8
DEX = 14
CON = 10
INT = 10
WIS = 8
CHA = 8
}
Bonuses = [ ]
}
let goblinBoss = {
goblin with
Bonuses = [
{ Ability = STR; Bonus = 2 }
{ Ability = CHA; Bonus = 2 }
]
}
In other words, I can now create a template for a canonical Monster, and create a variant, by specifying what changes should be applied to the original.
I will likely revisit this aspect of the model at a later point, because conceptually, there is something different about racial abilities and modified monsters. In the second case, I could apply any modification I want, whereas in the first, the bonus is set by the rules, and explicitly depends on the creature race. However, this will do for now, and we will leave it there!
If you want to take a look at the code in a more convenient manner, the current state of affairs is here on GitHub. Let me know if you have questions or comments, and next time, we’ll probably dig into Hit Points and modeling dice rolls and how to represent them as expressions!