Markdown monsters

Markdown monsters

Whenever I take an interest in something I think to myself, “How can I combine this with R?”

This post is the result of applying that attitude to Dungeons and Dragons.

So how would I combine D&D with R? A good start would be to have a nice data set of Dungeons and Dragons monsters, with all of their statistics, abilities and attributes. One of the core D&D rule books is the Monster Manual. I could attempt to scrape the Monster Manual but I figured that the lovely people behind D&D wouldn’t be too happy if I uploaded most of the book to GitHub!

Fortunately, Wizards of the Coast have included around 300 monsters in the Systems Reference Document (SRD) for 5th edition D&D. This is made available under the Open Gaming License Version 1.0a.

I started off trying to scrape the SRD directly, but scraping a PDF was looking to be a nightmare. Fortunately, vitusventure had already converted the SRD to markdown documents to host the content on the (rather pretty) https://5thsrd.org/. I figured it would be easier to scrape markdown files, since they are structured but simple text. Here’s an example of a monster’s “stat block” written in markdown:


name: Medusa type: monstrosity cr: 6

Medusa

Medium monstrosity, lawful evil

Armor Class 15 (natural armor)
Hit Points 127 (17d8 + 51)
Speed 30 ft.

STR DEX CON INT WIS CHA
10 (+0) 15 (+2) 16 (+3) 12 (+1) 13 (+1) 15 (+2)

Skills Deception +5, Insight +4, Perception +4, Stealth +5
Senses darkvision 60 ft., passive Perception 14
Languages Common
Challenge 6 (2,300 XP)

Petrifying Gaze. When a creature that can see the medusa’s eyes starts its turn within 30 feet of the medusa, the medusa can force it to make a DC 14 Constitution saving throw if the medusa isn’t incapacitated and can see the creature. If the saving throw fails by 5 or more, the creature is instantly petrified. Otherwise, a creature that fails the save begins to turn to stone and is restrained. The restrained creature must repeat the saving throw at the end of its next turn, becoming petrified on a failure or ending the effect on a success. The petrification lasts until the creature is freed by the greater restoration spell or other magic.
Unless surprised, a creature can avert its eyes to avoid the saving throw at the start of its turn. If the creature does so, it can’t see the medusa until the start of its next turn, when it can avert its eyes again. If the creature looks at the medusa in the meantime, it must immediately make the save.
If the medusa sees itself reflected on a polished surface within 30 feet of it and in an area of bright light, the medusa is, due to its curse, affected by its own gaze.

Actions

Multiattack. The medusa makes either three melee attacks–one with its snake hair and two with its shortsword–or two ranged attacks with its longbow.
Snake Hair. Melee Weapon Attack: +5 to hit, reach 5 ft., one creature. Hit: 4 (1d4 + 2) piercing damage plus 14 (4d6) poison damage.
Shortsword. Melee Weapon Attack: +5 to hit, reach 5 ft., one target. Hit: 5 (1d6 + 2) piercing damage.
Longbow. Ranged Weapon Attack: +5 to hit, range 150/600 ft., one target. Hit: 6 (1d8 + 2) piercing damage plus 7 (2d6) poison damage.


I put the scraped monsters in the SRD into the monsters data set and uploaded it to a quickly created monstr package. You can access the data set by installing the package with devtools::install_github("mdneuzerling/monstr").

monsters <- monstr::monsters
skimr::skim(monsters)
Name monsters
Number of rows 317
Number of columns 39
_______________________
Column type frequency:
character 10
list 1
numeric 28
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
name 0 1.00 3 25 0 317 0
type 0 1.00 3 30 0 35 0
size 0 1.00 4 10 0 6 0
alignment 0 1.00 7 40 0 16 0
ac_note 108 0.66 5 23 0 22 0
hp 0 1.00 3 11 0 168 0
senses 0 1.00 20 170 0 88 0
languages 0 1.00 1 91 0 86 0
speed 0 1.00 4 52 0 90 0
description 274 0.14 96 654 0 43 0

Variable type: list

skim_variable n_missing complete_rate n_unique min_length max_length
actions 3 0.99 305 1 10

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
cr 0 1 4.60 5.91 0 0.5 2 6 30 ▇▁▁▁▁
xp 0 1 4275.30 12436.13 0 100.0 450 2300 155000 ▇▁▁▁▁
ac 0 1 14.07 3.27 5 12.0 13 17 25 ▁▇▅▂▁
hp_avg 0 1 82.31 99.88 1 18.0 45 114 676 ▇▁▁▁▁
str 0 1 15.34 6.63 1 11.0 16 19 30 ▂▃▇▃▂
dex 0 1 12.61 3.22 1 10.0 13 15 28 ▁▅▇▁▁
con 0 1 15.16 4.50 8 12.0 14 18 30 ▇▇▅▂▁
int 0 1 7.86 5.69 1 2.0 7 12 25 ▇▅▃▂▁
wis 0 1 11.72 2.98 0 10.0 12 13 25 ▁▅▇▁▁
cha 0 1 9.79 5.76 0 5.0 8 14 30 ▇▇▅▂▁
acrobatics 0 1 1.12 1.64 -5 0.0 1 2 9 ▁▅▇▁▁
animal_handling 0 1 0.66 1.49 -5 0.0 1 1 7 ▁▁▇▁▁
arcana 0 1 -1.07 3.46 -5 -4.0 -2 1 18 ▇▅▁▁▁
athletics 0 1 2.55 3.48 -5 0.0 3 4 14 ▂▆▇▂▁
deception 0 1 -0.11 3.37 -5 -3.0 -1 2 11 ▇▅▃▂▁
history 0 1 -1.05 3.46 -5 -4.0 -2 1 13 ▇▅▂▁▁
insight 0 1 0.95 2.12 -5 0.0 1 1 10 ▁▇▂▁▁
intimidation 0 1 -0.35 2.94 -5 -3.0 -1 2 10 ▇▅▃▁▁
investigation 0 1 -1.26 2.94 -5 -4.0 -2 1 7 ▇▃▆▂▁
medicine 0 1 0.68 1.55 -5 0.0 1 1 7 ▁▁▇▁▁
nature 0 1 -1.26 2.93 -5 -4.0 -2 1 7 ▇▃▆▂▁
perception 0 1 2.87 4.02 -5 0.0 2 4 17 ▂▇▃▁▁
performance 0 1 -0.36 2.94 -5 -3.0 -1 2 10 ▇▅▃▁▁
persuasion 0 1 -0.19 3.34 -5 -3.0 -1 2 16 ▇▅▂▁▁
religion 0 1 -1.18 3.12 -5 -4.0 -2 1 15 ▇▅▁▁▁
sleight_of_hand 0 1 1.11 1.62 -5 0.0 1 2 9 ▁▅▇▁▁
stealth 0 1 2.30 2.56 -5 0.0 2 4 10 ▁▇▇▃▁
survival 0 1 0.70 1.54 -5 0.0 1 1 7 ▁▁▇▁▁

Because it’s an R crime to introduce a new data set without a ggplot, here we can see the relationship between strength and constitution, faceted by monster size:

monsters %>% 
    ggplot(aes(x = str, y = con)) + 
    geom_point() + 
    facet_wrap(. ~ size, nrow = 2)

One note before we go on: “monster” is a generic term. This data set contains bandits which, while of questionable moral character, are not necessarily monstrous. You can also find a simple frog in this data set, capable of nothing more than a ribbit. We refer to them all as “monsters”, perhaps unfairly!

Scraping line-by-line

Let’s take the Medusa monster above, loaded as a single string. I’m going to make life easier for myself by separating the string into lines. At first I tried to do this myself with strsplit, but please take my advice: use the stringi package. You’ll notice that I turn the resulting list into a single-column tibble. I won’t lie: I find manipulating lists directly difficult, so being able to use dplyr verbs makes me happy. I’m also going to remove the italics (represented in markdown by underscores) since I won’t need them here.

lines <- monster %>% 
        stringi::stri_split_lines(omit_empty = TRUE) %>% 
        unlist %>% 
        as_tibble %>% # much easier to deal with than lists
        mutate_all(trimws) %>% 
        mutate_all(function(x) gsub("_", "", x)) # remove italics
print(lines, n = nrow(lines))
#> # A tibble: 23 x 1
#>    value                                                                        
#>    <chr>                                                                        
#>  1 name: Medusa                                                                 
#>  2 type: monstrosity                                                            
#>  3 cr: 6                                                                        
#>  4 # Medusa                                                                     
#>  5 Medium monstrosity, lawful evil                                              
#>  6 **Armor Class** 15 (natural armor)                                           
#>  7 **Hit Points** 127 (17d8 + 51)                                               
#>  8 **Speed** 30 ft.                                                             
#>  9 | STR     | DEX     | CON     | INT     | WIS     | CHA     |                
#> 10 |---------|---------|---------|---------|---------|---------|                
#> 11 | 10 (+0) | 15 (+2) | 16 (+3) | 12 (+1) | 13 (+1) | 15 (+2) |                
#> 12 **Skills** Deception +5, Insight +4, Perception +4, Stealth +5               
#> 13 **Senses** darkvision 60 ft., passive Perception 14                          
#> 14 **Languages** Common                                                         
#> 15 **Challenge** 6 (2,300 XP)                                                   
#> 16 **Petrifying Gaze.** When a creature that can see the medusa's eyes starts i…
#> 17 Unless surprised, a creature can avert its eyes to avoid the saving throw at…
#> 18 If the medusa sees itself reflected on a polished surface within 30 feet of …
#> 19 ### Actions                                                                  
#> 20 **Multiattack.** The medusa makes either three melee attacks--one with its s…
#> 21 **Snake Hair.** Melee Weapon Attack: +5 to hit, reach 5 ft., one creature. H…
#> 22 **Shortsword.** Melee Weapon Attack: +5 to hit, reach 5 ft., one target. Hit…
#> 23 **Longbow.** Ranged Weapon Attack: +5 to hit, range 150/600 ft., one target.…

Scraping monster name, type and CR

The wonderful thing about these markdown files is that they have a nifty couple of lines up the top listing the name, type and challenge rating (cr) of the monster. These are marked by headings with colons, so we’ll define a function to extract the data based on that.

extract_from_colon_heading <- function(lines, heading) {
    lines %>% 
        filter(grepl(paste0(heading, ":"), value)) %>% 
        gsub(paste0(heading, ":"), "", .) %>% 
        as.character %>% 
        trimws
}

c(
    extract_from_colon_heading(lines, "name"),
    extract_from_colon_heading(lines, "type"),
    extract_from_colon_heading(lines, "cr")
)
#> [1] "Medusa"      "monstrosity" "6"

I should offer some explanations for those new to D&D! The “type” of a monster is a category like beast, undead or—in the case of the Medusa—monstrosity. The challenge rating is a rough measure of difficulty. The Medusa has a challenge rating of 6, so is a suitable encounter for 4 players with characters of level 6. Characters begin at level 1 and move up to level 20 (if the campaign lasts that long).

Scraping based on bold text

Most of the information we need is labelled by bold text, represented in markdown by double asterisks. We’ll define three functions:

  1. identify_bold_text looks for a given bold_text in a string x, and returns a Boolean value.
  2. strip_bold_text removes all bolded text from a string x, and trims white space from either end of the result.
  3. extract_from_bold_text looks through a list of lines (like the lines defined above) for a particular bold_text. It will return all text in the string except the bold_text. This function uses the two above.
identify_bold_text <- function(x, bold_text) {
    grepl(paste0("\\*\\*", bold_text, "\\*\\*"), x, ignore.case = TRUE)
}

strip_bold_text <- function(x) {
    gsub("\\*\\*(.*?)\\*\\*", "", x, ignore.case = TRUE) %>% trimws
}

extract_from_bold_text <- function(lines, bold_text) {
    lines %>% 
        filter(identify_bold_text(value, bold_text)) %>% 
        as.character %>% 
        strip_bold_text
}

extract_from_bold_text(lines, "Languages")
#> [1] "Common"

Scraping based on brackets

Some of the data we need is found in bracketed information. The extract_bracketed function returns all text inside the first set of brackets found in a string x, or returns NA if no bracketed text is found.

extract_bracketed <- function(x) {
    if (!grepl("\\(.*\\)", x)) {
        return(NA)
    } else {
        gsub(".*\\((.*?)\\).*", "\\1", x)
    }
}

lines %>% extract_from_bold_text("Armor Class") %>% extract_bracketed
#> [1] "natural armor"

A monster’s armor class (AC) determines how hard it is to hit the creature with a weapon or certain spells. The Medusa has an AC of 15. To attack the Medusa, a player will roll a 20-sided die (d20) and add certain modifiers based on their character’s skills and proficiencies. If the result is at least 15, the attack hits. The “natural armor” note means that the Medusa’s armor class is provided by thickened skin or scales, and not a separate piece of armour.

Abilities

Player characters and monsters in D&D have six ability scores that influence almost everything that they do: strength, dexterity, constitution, intelligence, wisdom and charisma. These abilities are represented by numeric scores that usually (but not always) fall between 10 and 20, with 10 being “average” and 20 being superb.

In the markdown files, these ability scores are tables. We look for the table header and find the ability scores two rows below.

ability_header <- min(which(grepl("\\| STR", lines$value), arr.ind = TRUE))
ability_text <- lines$value[ability_header + 2]
ability_vector <- ability_text %>% strsplit("\\|") %>% unlist
monster_ability <- readr::parse_number(ability_vector[!(ability_vector == "")])
names(monster_ability) <- c("STR", "DEX", "CON", "INT", "WIS", "CHA")
monster_ability    
#> STR DEX CON INT WIS CHA 
#>  10  15  16  12  13  15

Skills

Skills represent the monster’s ability to perform activities. There are 18 skills, and each skill is associated with one of the 6 ability scores.

skill_ability <- tribble(
~skill, ~ability_code_upper,
#-------|------------------
"athletics", "STR",
"acrobatics", "DEX",
"sleight_of_hand", "DEX",
"stealth", "DEX",
"arcana", "INT",
"history", "INT",
"investigation", "INT",
"nature", "INT",
"religion", "INT",
"animal_handling", "WIS",
"insight", "WIS",
"medicine", "WIS",
"perception", "WIS",
"survival", "WIS",
"deception", "CHA",
"intimidation", "CHA",
"performance", "CHA",
"persuasion", "CHA",
)

All skills begin with a roll of a d20 for an element of chance. Modifiers, which can be negative, are then added to the result to determine how well the monster did. The Medusa has a +5 bonus to Deception, which would be added to the roll.

If a skill isn’t listed in the Medusa’s stat block, she can still use it. In this case, she would rely instead on her ability scores. For example, the Medusa isn’t trained in acrobatics, but her high dexterity would give her a slight advantage nevertheless.

Modifiers can be calculated from ability scores with a simple formula, defined below. Note that modifiers can be negative. Zombies, for example, are not known for their high intelligence, and have a history modifier of -4.

modifier <- function(x) {
    floor((x - 10) / 2)
}

monster_modifiers <- monster_ability %>% 
    as.list %>% # preserves list names as column names
    as_tibble %>% 
    mutate_all(modifier) %>% # convert raw ability to modifiers
    gather(key = ability_code_upper, value = modifier) # convert to long
monster_modifiers
#> # A tibble: 6 x 2
#>   ability_code_upper modifier
#>   <chr>                 <dbl>
#> 1 STR                       0
#> 2 DEX                       2
#> 3 CON                       3
#> 4 INT                       1
#> 5 WIS                       1
#> 6 CHA                       2

We’re going to list every skill modifier for each monster. We start with the base_skills, determined solely by the monster’s ability scores.

base_skills <- skill_ability %>% 
    left_join(monster_modifiers, by = "ability_code_upper") %>% 
    select(skill, modifier)
head(base_skills, 6)
#> # A tibble: 6 x 2
#>   skill           modifier
#>   <chr>              <dbl>
#> 1 athletics              0
#> 2 acrobatics             2
#> 3 sleight_of_hand        2
#> 4 stealth                2
#> 5 arcana                 1
#> 6 history                1

Now we find the listed_skills, which are those explicitly provided in the markdown. We use the extract_from_bold_text function, and split the resulting line along the commas into a vector. The words in an element name the skill, while the number gives the modifier.

This chain of piped functions has a peculiar unlist %>% as.list, which seems to be necessary to preserve the vector names. I’d love to do without this code, since it seems very ugly!

listed_skills <- lines %>% 
    extract_from_bold_text("Skills") %>% 
    strsplit(", ") %>% 
        unlist %>%  
        lapply(function(x) {
            skill_name <- word(x)
            skill_modifier <- c(readr::parse_number(x))
            names(skill_modifier) <- tolower(skill_name)
            skill_modifier
        }) %>% 
        unlist %>% # This is 
        as.list %>% # so weird
        as_tibble %>% 
        gather(key = skill, value = modifier) %>% 
        mutate(skill = gsub(" ", "_", skill)) # keep naming conventions (underscores)
listed_skills
#> # A tibble: 4 x 2
#>   skill      modifier
#>   <chr>         <dbl>
#> 1 deception         5
#> 2 insight           4
#> 3 perception        4
#> 4 stealth           5

Finally, we combine listed_skills and base_skills, allowing listed skills to override base skills.

monster_skills <- if (length(listed_skills) == 0) {
    base_skills
} else {
    listed_skills %>% rbind(
        anti_join(base_skills, listed_skills, by = "skill")
    )
}
monster_skills <- monster_skills[match(base_skills$skill, monster_skills$skill),] # maintain skill order
head(monster_skills, 6)
#> # A tibble: 6 x 2
#>   skill           modifier
#>   <chr>              <dbl>
#> 1 athletics              0
#> 2 acrobatics             2
#> 3 sleight_of_hand        2
#> 4 stealth                5
#> 5 arcana                 1
#> 6 history                1

Monster actions

Actions are are a tough one. Take a look at the last 5 lines of the markdown:

tail(lines, 5)
#> # A tibble: 5 x 1
#>   value                                                                         
#>   <chr>                                                                         
#> 1 ### Actions                                                                   
#> 2 **Multiattack.** The medusa makes either three melee attacks--one with its sn…
#> 3 **Snake Hair.** Melee Weapon Attack: +5 to hit, reach 5 ft., one creature. Hi…
#> 4 **Shortsword.** Melee Weapon Attack: +5 to hit, reach 5 ft., one target. Hit:…
#> 5 **Longbow.** Ranged Weapon Attack: +5 to hit, range 150/600 ft., one target. …

We’re going to look for an actions h3 heading (three hashes) “Actions”. The lines that correspond to actions begin after this Actions subheading. The last action is determined by finding either:

  1. the line before the next h3 heading or, failing that,
  2. the last line.

We then have a list of lines that correspond to monster actions. We’re going to turn these lines into a named vector, in which the name of the action (taken from the bold text) corresponds to the action text.

header_rows <- which(grepl("###", lines$value), arr.ind = TRUE)
    actions_header_row <- which(lines == "### Actions", arr.ind = TRUE)[,"row"]
    if (length(actions_header_row) == 0) { # This monster has no actions
        monster_actions <- NA
    } else {
        if (max(header_rows) == actions_header_row) {
            last_action = nrow(lines) # in this case, the actions are the last lines
        } else {
            last_action <-  min(header_rows[header_rows > actions_header_row]) - 1 # the row before the heading that comes after ### Actions
        }
        action_rows <-  seq(actions_header_row + 1, last_action)
        monster_actions <- lines$value[action_rows]
        monster_actions <- monster_actions %>% purrr::map(function(x) {
            action_name <- gsub(".*\\*\\*(.*?)\\.\\*\\*.*", "\\1", x)
            action <- x %>% strip_bold_text %>% trimws
            names(action) <- action_name
            action
        }) %>% purrr::reduce(c)
    }
monster_actions
#>                                                                                                                                 Multiattack 
#> "The medusa makes either three melee attacks--one with its snake hair and two with its shortsword--or two ranged attacks with its longbow." 
#>                                                                                                                                  Snake Hair 
#>                  "Melee Weapon Attack: +5 to hit, reach 5 ft., one creature. Hit: 4 (1d4 + 2) piercing damage plus 14 (4d6) poison damage." 
#>                                                                                                                                  Shortsword 
#>                                                "Melee Weapon Attack: +5 to hit, reach 5 ft., one target. Hit: 5 (1d6 + 2) piercing damage." 
#>                                                                                                                                     Longbow 
#>              "Ranged Weapon Attack: +5 to hit, range 150/600 ft., one target. Hit: 6 (1d8 + 2) piercing damage plus 7 (2d6) poison damage."

Putting it all together

We can now put everything together into a single-row tibble:

tibble(
    name = lines %>% extract_from_colon_heading("name"),
    type = lines %>% extract_from_colon_heading("type"),
    cr = lines %>% extract_from_colon_heading("cr") %>% as.numeric,
    xp = lines %>% extract_from_bold_text("challenge") %>% extract_bracketed %>% readr::parse_number(),
    ac = lines %>% extract_from_bold_text("Armor Class") %>% readr::parse_number(),
    ac_note = lines %>% extract_from_bold_text("Armor Class") %>% extract_bracketed,
    hp_avg = lines %>% extract_from_bold_text("Hit Points") %>% readr::parse_number(),
    hp = lines %>% extract_from_bold_text("Hit Points") %>% extract_bracketed,
    str = monster_ability["STR"],
    dex = monster_ability["DEX"],
    con = monster_ability["CON"],
    int = monster_ability["INT"],
    wis = monster_ability["WIS"],
    cha = monster_ability["CHA"],
    senses = lines %>% extract_from_bold_text("Senses"),
    languages = lines %>% extract_from_bold_text("Languages"),
    speed = lines %>% extract_from_bold_text("Speed"),
    actions = monster_actions %>% list
    ) %>% 
    cbind(spread(monster_skills, skill, modifier)) %>% 
    as_tibble
#> # A tibble: 1 x 36
#>   name  type     cr    xp    ac ac_note hp_avg hp      str   dex   con   int
#>   <chr> <chr> <dbl> <dbl> <dbl> <chr>    <dbl> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Medu… mons…     6  2300    15 natura…    127 17d8…    10    15    16    12
#> # … with 24 more variables: wis <dbl>, cha <dbl>, senses <chr>,
#> #   languages <chr>, speed <chr>, actions <list>, acrobatics <dbl>,
#> #   animal_handling <dbl>, arcana <dbl>, athletics <dbl>, deception <dbl>,
#> #   history <dbl>, insight <dbl>, intimidation <dbl>, investigation <dbl>,
#> #   medicine <dbl>, nature <dbl>, perception <dbl>, performance <dbl>,
#> #   persuasion <dbl>, religion <dbl>, sleight_of_hand <dbl>, stealth <dbl>,
#> #   survival <dbl>

There are a few more fields that I haven’t covered here (size, alignment and description, for example). I’ve put the full version of the parse_monster.R script in a gist.

Of course, this is how to parse just one monster. Fortunately, the purrr package exists. Here’s how to scrape every monster:

  1. Clone vitusventure’s 5th edition SRD repository
  2. Set the /docs/gamemaster_rules/monsters directory to a variable monster_dir
  3. Run the following code:
monsters <- list.files(monster_dir, full.names = TRUE) %>% 
    purrr::map(function(x) readChar(x, file.info(x)$size)) %>% # read files as strings
    purrr::map(parse_monster) %>% 
    purrr::reduce(rbind)

What’s next

A few things are missing here:

  • Damage/condition immunities and resistances are not being scraped.
  • Monster traits, such as the ability to breathe underwater, are not being scraped. I think this is a matter of finding any bold heading that isn’t “standard” and treating it as a trait.
  • Some monsters have complicated armor classes. For example, the werewolf has an AC of “11 in humanoid form, 12 (natural armor) in wolf or hybrid form”. This doesn’t fit the template of ac and ac_note.

I’d like to incorporate the spells in the SRD, as well as some basic mechanics. Imagine being able to generate an encounter in R according to a specific party level!

Sources

The Medusa and all Dungeons and Dragons 5th edition mechanics are available in the Systems Reference Document under the Open Gaming License Version 1.0a. The monsters data set in the monstr package is available under the same license.


devtools::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value                       
#>  version  R version 4.0.0 (2020-04-24)
#>  os       Ubuntu 20.04 LTS            
#>  system   x86_64, linux-gnu           
#>  ui       X11                         
#>  language en_AU:en                    
#>  collate  en_AU.UTF-8                 
#>  ctype    en_AU.UTF-8                 
#>  tz       Australia/Melbourne         
#>  date     2020-06-13                  
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package     * version    date       lib source                              
#>  assertthat    0.2.1      2019-03-21 [1] CRAN (R 4.0.0)                      
#>  backports     1.1.7      2020-05-13 [1] CRAN (R 4.0.0)                      
#>  base64enc     0.1-3      2015-07-28 [1] CRAN (R 4.0.0)                      
#>  broom         0.5.6      2020-04-20 [1] CRAN (R 4.0.0)                      
#>  callr         3.4.3      2020-03-28 [1] CRAN (R 4.0.0)                      
#>  cellranger    1.1.0      2016-07-27 [1] CRAN (R 4.0.0)                      
#>  cli           2.0.2      2020-02-28 [1] CRAN (R 4.0.0)                      
#>  colorspace    1.4-1      2019-03-18 [1] CRAN (R 4.0.0)                      
#>  crayon        1.3.4      2017-09-16 [1] CRAN (R 4.0.0)                      
#>  DBI           1.1.0      2019-12-15 [1] CRAN (R 4.0.0)                      
#>  dbplyr        1.4.3      2020-04-19 [1] CRAN (R 4.0.0)                      
#>  desc          1.2.0      2018-05-01 [1] CRAN (R 4.0.0)                      
#>  devtools      2.3.0      2020-04-10 [1] CRAN (R 4.0.0)                      
#>  digest        0.6.25     2020-02-23 [1] CRAN (R 4.0.0)                      
#>  downlit       0.0.0.9000 2020-06-12 [1] Github (r-lib/downlit@87fb1af)      
#>  dplyr       * 0.8.5      2020-03-07 [1] CRAN (R 4.0.0)                      
#>  ellipsis      0.3.1      2020-05-15 [1] CRAN (R 4.0.0)                      
#>  evaluate      0.14       2019-05-28 [1] CRAN (R 4.0.0)                      
#>  fansi         0.4.1      2020-01-08 [1] CRAN (R 4.0.0)                      
#>  farver        2.0.3      2020-01-16 [1] CRAN (R 4.0.0)                      
#>  forcats     * 0.5.0      2020-03-01 [1] CRAN (R 4.0.0)                      
#>  fs            1.4.1      2020-04-04 [1] CRAN (R 4.0.0)                      
#>  generics      0.0.2      2018-11-29 [1] CRAN (R 4.0.0)                      
#>  ggplot2     * 3.3.0      2020-03-05 [1] CRAN (R 4.0.0)                      
#>  glue          1.4.1      2020-05-13 [1] CRAN (R 4.0.0)                      
#>  gtable        0.3.0      2019-03-25 [1] CRAN (R 4.0.0)                      
#>  haven         2.2.0      2019-11-08 [1] CRAN (R 4.0.0)                      
#>  highr         0.8        2019-03-20 [1] CRAN (R 4.0.0)                      
#>  hms           0.5.3      2020-01-08 [1] CRAN (R 4.0.0)                      
#>  htmltools     0.4.0      2019-10-04 [1] CRAN (R 4.0.0)                      
#>  httr          1.4.1      2019-08-05 [1] CRAN (R 4.0.0)                      
#>  hugodown      0.0.0.9000 2020-06-12 [1] Github (r-lib/hugodown@6812ada)     
#>  jsonlite      1.6.1      2020-02-02 [1] CRAN (R 4.0.0)                      
#>  knitr         1.28       2020-02-06 [1] CRAN (R 4.0.0)                      
#>  labeling      0.3        2014-08-23 [1] CRAN (R 4.0.0)                      
#>  lattice       0.20-41    2020-04-02 [4] CRAN (R 4.0.0)                      
#>  lifecycle     0.2.0      2020-03-06 [1] CRAN (R 4.0.0)                      
#>  lubridate     1.7.8      2020-04-06 [1] CRAN (R 4.0.0)                      
#>  magrittr      1.5        2014-11-22 [1] CRAN (R 4.0.0)                      
#>  memoise       1.1.0.9000 2020-05-09 [1] Github (hadley/memoise@4aefd9f)     
#>  modelr        0.1.6      2020-02-22 [1] CRAN (R 4.0.0)                      
#>  monstr        0.0.0.9000 2020-06-13 [1] Github (mdneuzerling/monstr@dc7e102)
#>  munsell       0.5.0      2018-06-12 [1] CRAN (R 4.0.0)                      
#>  nlme          3.1-145    2020-03-04 [4] CRAN (R 4.0.0)                      
#>  pillar        1.4.4      2020-05-05 [1] CRAN (R 4.0.0)                      
#>  pkgbuild      1.0.7      2020-04-25 [1] CRAN (R 4.0.0)                      
#>  pkgconfig     2.0.3      2019-09-22 [1] CRAN (R 4.0.0)                      
#>  pkgload       1.0.2      2018-10-29 [1] CRAN (R 4.0.0)                      
#>  prettyunits   1.1.1      2020-01-24 [1] CRAN (R 4.0.0)                      
#>  processx      3.4.2      2020-02-09 [1] CRAN (R 4.0.0)                      
#>  ps            1.3.3      2020-05-08 [1] CRAN (R 4.0.0)                      
#>  purrr       * 0.3.4      2020-04-17 [1] CRAN (R 4.0.0)                      
#>  R6            2.4.1      2019-11-12 [1] CRAN (R 4.0.0)                      
#>  Rcpp          1.0.4.6    2020-04-09 [1] CRAN (R 4.0.0)                      
#>  readr       * 1.3.1      2018-12-21 [1] CRAN (R 4.0.0)                      
#>  readxl        1.3.1      2019-03-13 [1] CRAN (R 4.0.0)                      
#>  remotes       2.1.1      2020-02-15 [1] CRAN (R 4.0.0)                      
#>  repr          1.1.0      2020-01-28 [1] CRAN (R 4.0.0)                      
#>  reprex        0.3.0      2019-05-16 [1] CRAN (R 4.0.0)                      
#>  rlang         0.4.6      2020-05-02 [1] CRAN (R 4.0.0)                      
#>  rmarkdown     2.2.3      2020-06-12 [1] Github (rstudio/rmarkdown@4ee96c8)  
#>  rprojroot     1.3-2      2018-01-03 [1] CRAN (R 4.0.0)                      
#>  rstudioapi    0.11       2020-02-07 [1] CRAN (R 4.0.0)                      
#>  rvest         0.3.5      2019-11-08 [1] CRAN (R 4.0.0)                      
#>  scales        1.1.0      2019-11-18 [1] CRAN (R 4.0.0)                      
#>  sessioninfo   1.1.1      2018-11-05 [1] CRAN (R 4.0.0)                      
#>  skimr         2.1.1      2020-04-16 [1] CRAN (R 4.0.0)                      
#>  stringi       1.4.6      2020-02-17 [1] CRAN (R 4.0.0)                      
#>  stringr     * 1.4.0      2019-02-10 [1] CRAN (R 4.0.0)                      
#>  testthat      2.3.2      2020-03-02 [1] CRAN (R 4.0.0)                      
#>  tibble      * 3.0.1      2020-04-20 [1] CRAN (R 4.0.0)                      
#>  tidyr       * 1.0.2      2020-01-24 [1] CRAN (R 4.0.0)                      
#>  tidyselect    1.0.0      2020-01-27 [1] CRAN (R 4.0.0)                      
#>  tidyverse   * 1.3.0      2019-11-21 [1] CRAN (R 4.0.0)                      
#>  usethis       1.6.1      2020-04-29 [1] CRAN (R 4.0.0)                      
#>  utf8          1.1.4      2018-05-24 [1] CRAN (R 4.0.0)                      
#>  vctrs         0.3.1      2020-06-05 [1] CRAN (R 4.0.0)                      
#>  withr         2.2.0      2020-04-20 [1] CRAN (R 4.0.0)                      
#>  xfun          0.14       2020-05-20 [1] CRAN (R 4.0.0)                      
#>  xml2          1.3.2      2020-04-23 [1] CRAN (R 4.0.0)                      
#>  yaml          2.2.1      2020-02-01 [1] CRAN (R 4.0.0)                      
#> 
#> [1] /home/mdneuzerling/R/x86_64-pc-linux-gnu-library/4.0
#> [2] /usr/local/lib/R/site-library
#> [3] /usr/lib/R/site-library
#> [4] /usr/lib/R/library