home / art / blog / me

The spells die rolls plot

On this page I’ll walk through the code used to produce the “spell dice” plot that I posted on social media. It’s not the most exciting code I’ve ever written but it was useful as a tiny side project that I could use to teach myself targets. Note that this post doesn’t use targets to render the plot, except in the trivial sense the entire blog is built with targets. Within the post, it’s just regular R code. Speaking of which, I suppose I’d best load the packages that the plot relies on:

library(ggplot2)
library(dplyr)
library(tidyr)
library(stringr)
library(tibble)
library(purrr)
library(readr)
library(forcats)
library(ggrepel)

Read data

The spells data that I’m using here comes from the TidyTuesday D&D Spells data set. The data set was compiled by Jon Harmon, and originates in the recently released Dungeons & Dragons Free Rules (2024 edition). If you’ve played D&D before, this should be quite familiar:

spells <- read_csv("./data/spells.csv", show_col_types = FALSE)
print(spells)
#> # A tibble: 314 × 27
#>    name   level school bard  cleric druid paladin ranger sorcerer warlock wizard
#>    <chr>  <dbl> <chr>  <lgl> <lgl>  <lgl> <lgl>   <lgl>  <lgl>    <lgl>   <lgl> 
#>  1 Acid …     0 evoca… FALSE FALSE  FALSE FALSE   FALSE  TRUE     FALSE   TRUE  
#>  2 Aid        2 abjur… TRUE  TRUE   TRUE  TRUE    TRUE   FALSE    FALSE   FALSE 
#>  3 Alarm      1 abjur… FALSE FALSE  FALSE FALSE   TRUE   FALSE    FALSE   TRUE  
#>  4 Alter…     2 trans… FALSE FALSE  FALSE FALSE   FALSE  TRUE     FALSE   TRUE  
#>  5 Anima…     1 encha… TRUE  FALSE  TRUE  FALSE   TRUE   FALSE    FALSE   FALSE 
#>  6 Anima…     2 encha… TRUE  FALSE  TRUE  FALSE   TRUE   FALSE    FALSE   FALSE 
#>  7 Anima…     8 trans… FALSE FALSE  TRUE  FALSE   FALSE  FALSE    FALSE   FALSE 
#>  8 Anima…     3 necro… FALSE TRUE   FALSE FALSE   FALSE  FALSE    FALSE   TRUE  
#>  9 Anima…     5 trans… TRUE  FALSE  FALSE FALSE   FALSE  TRUE     FALSE   TRUE  
#> 10 Antil…     5 abjur… FALSE FALSE  TRUE  FALSE   FALSE  FALSE    FALSE   FALSE 
#> # ℹ 304 more rows
#> # ℹ 16 more variables: casting_time <chr>, action <lgl>, bonus_action <lgl>,
#> #   reaction <lgl>, ritual <lgl>, casting_time_long <chr>, trigger <chr>,
#> #   range <chr>, range_type <chr>, verbal_component <lgl>,
#> #   somatic_component <lgl>, material_component <lgl>,
#> #   material_component_details <chr>, duration <chr>, concentration <lgl>,
#> #   description <chr>

Tidy data

dice_dat <- spells |>
  select(name, level, description) |>
  mutate(
    dice_txt = str_extract_all(description, "\\b\\d+d\\d+\\b"),
    dice_txt = map(dice_txt, unique)
  ) |>
  unnest_longer(
    col = "dice_txt",
    values_to = "dice_txt",
    indices_to = "position"
  ) |>
  mutate(
    dice_num = dice_txt |> str_extract("\\d+(?=d)") |> as.numeric(),
    dice_die = dice_txt |> str_extract("(?<=d)\\d+") |> as.numeric(),
    dice_val = dice_num * (dice_die + 1)/2,
    dice_txt = factor(dice_txt) |> fct_reorder(dice_val)
  )

print(dice_dat)
#> # A tibble: 236 × 8
#>    name           level description dice_txt position dice_num dice_die dice_val
#>    <chr>          <dbl> <chr>       <fct>       <int>    <dbl>    <dbl>    <dbl>
#>  1 Acid Splash        0 "You creat… 1d6             1        1        6      3.5
#>  2 Acid Splash        0 "You creat… 2d6             2        2        6      7  
#>  3 Acid Splash        0 "You creat… 3d6             3        3        6     10.5
#>  4 Acid Splash        0 "You creat… 4d6             4        4        6     14  
#>  5 Alter Self         2 "You alter… 1d6             1        1        6      3.5
#>  6 Animate Objec…     5 "Objects a… 1d4             1        1        4      2.5
#>  7 Animate Objec…     5 "Objects a… 1d6             2        1        6      3.5
#>  8 Animate Objec…     5 "Objects a… 1d12            3        1       12      6.5
#>  9 Animate Objec…     5 "Objects a… 2d6             4        2        6      7  
#> 10 Animate Objec…     5 "Objects a… 2d12            5        2       12     13  
#> # ℹ 226 more rows

Make plot

palette <- hcl.colors(n = 10, palette = "PuOr")

labs <- dice_dat |>
  summarise(
    dice_txt = first(dice_txt),
    count = n(),
    .by = dice_txt
  )

pic <- ggplot(
  data = dice_dat,
  mapping = aes(
    x = dice_txt,
    fill = factor(level)
  )
) +
  geom_bar(color = "#222") +
  geom_label_repel(
    data = labs,
    mapping = aes(
      x = dice_txt,
      y = count,
      label = dice_txt
    ),
    size = 3,
    direction = "y",
    seed = 1,
    nudge_y = 4,
    color = "#ccc",
    fill = "#222",
    arrow = NULL,
    inherit.aes = FALSE
  ) +
  scale_fill_manual(
    name = "Spell level",
    values = palette
  ) +
  scale_x_discrete(
    name = "Increasing average outcome \u27a1",
    breaks = NULL,
    expand = expansion(.05)
  ) +
  scale_y_continuous(name = NULL) +
  labs(title = "Dice rolls in D&D spell descriptions by spell level") +
  theme_void() +
  theme(
    plot.background = element_rect(fill = "#222"),
    text = element_text(color = "#ccc"),
    axis.text = element_text(color = "#ccc"),
    axis.title = element_text(color = "#ccc"),
    plot.margin = unit(c(1, 1, 1, 1), units = "cm"),
    legend.position = "inside",
    legend.position.inside = c(.3, .825),
    legend.direction = "horizontal",
    legend.title.position = "top",
    legend.byrow = TRUE
  )

plot(pic)