Since the Covid pandemic, I really got into D&D. I had never played before, but had friends who had long lasting parties and were DMs, so it was the perfect opportunity since we were all locked up in our houses.

Turns out that it didn’t last more than a few months: we started one campaign at level 1 and disbanded shortly after reaching level 6. Then I found out about Critical Role and became a huge fan (although I’ve given up on Campaign 3…) and started playing with my partner and his friends.

As you can imagine, it wasn’t too long before I started DMing myself and I eventually started watching some of Matt Colville’s Twitch lives and YouTube videos (really recommend those for DMs!). So when Matt’s company MCDM started designing their own RPG, I was pretty excited (if you want to know more about it, you can listen to the podcast The Dice Society, which covers news on the development of the game).

For me, the game changer for this new RPG is the *dice*. Everyone who plays D&D, be it 5th edition, AD&D or anything in between, is used to rolling d20s, 20-sided dice^{1}. On the other hand, MCDM’s RPG uses 2 regular, 6-sided dice for attacking, making ability checks and other situations in which your character might need to roll. As of now (March 2024), the game is still in development, so it still might change, but they made it very clear that they want to keep the 2d6 system in the final version of the game.

I know: d6s are kind of boring compared to d20s, right? Every Farkle and Yahtzee player uses them, after all. But to me, that could be a great advantage: it’s easy to understand and hit the ground running as soon as you know the basic rules, no need to go through weird dice^{2}.

And although I’ve become some sort of *dice hoarder* for the past years, I love the idea of rolling two dice, and more than that, two d6s, because of *variance*.

Let’s assume we’re rolling a d20. The possible outcomes range from 1 to 20, and tecnically they all have the same probability, 1/20 = 5%. When rolling 2d6, there is a smaller range of possibilities (2 to 12) and they are not uniformally probable (you would expect to roll 12 much less frequently than 7, for example).

That means your *modifier* has a much more significant impact on the final result than sheer luck^{3}. If you are consistently rolling some value near the mean (7), how good you are at a certain ability will more accurately translate to how successful you are in a skill check or attack. Sure, you can still fumble or do exceptionally well, but those events are less common.

I remember when I was younger and my Math teacher gave us an experiment: roll two dice 100 times and take note of the results. At the end of the class, we drew a bar plot with the results and found out that everyone drew basically the same plot.

Let’s do the same, but with the RPG context and in a larger scale. We’ll check how the outcome of a character with a dexterity bonus of +3 would roll for a skill check using different dice systems. (MCDM’s stats are not exactly the same, but let’s just assume they are for this thought exercise.)

Assuming that every side on a 20-sided die is physically the same, it’s fair to conclude that the numbers 1 through 20 have the same probability of being the outcome. Let’s imagine that we’re rolling the die 100 times and counting how many times each side appears.

Now let’s do the same, rolling the die 1000 times.

The results seem random enough and there doesn’t seem to be a clear pattern. Even if we add the modifier (+3), it’s not that great a difference. The modifier matters much less then the roll of a die.

As a game design, it makes sense that extremely good (or bad) results are rare, but it’s weird that average outcomes are equally probable.

If we were rolling only one d6, we would have a similar situation: all 6 sides have the same probability. However, by adding two dice, we invoke the **Central Limit Theorem**!

What is that, you ask? It’s one of the most important theorems in probability theory that says that, given a sample from a random variable under certain circumstances, the sample mean tends to follow a normal distribution, even if the original variable has a different distribution other than normal. More technically, the limit of a random variable X when n -> Inf is normally distributed. More on that in the next section.

For now, let’s replicate the experiment we did with the d20, this time with 2d6.

This plot shows what we’ve talked before: it’s much more common to roll a 7 than, say, a 12. The same goes for the other end of the curve, too.

Now let’s take a look at what happens if we increase the number of rolls to a thousand:

And a million:

If you are not convinced by this little experiment, the proof for the theorem can be easily found online.

I’m not going to bore you with the details because 1, this is not the place for it and 2, I’m not ashamed to admit that I don’t even remember the proof and would just copy it from someplace else anyway.

So why does a more consistent result = a better game? (To me, at least.)

Of course it’s fun to have *some* unpredictability when we’re playing, otherwise we would simply discard the dice. But it’s always frustrating when you know that your character is very good in a skill but you KEEP. ROLLING. ONES.

As a DM, it’s also hard to explain why that keeps happening in a way that makes sense narratively. “Oh, you see that the door is completely rotten but you simply can’t break it despite your Strength (20)” More especifically about D&D, it also creates some significant differences for classes such as Bard and Rogue.

I think only time will tell if MCDM’s 2d6 system works for me in the long run. But for now, I’m excited about changing things up a little and depending a little less on my misbehaving dice.

Despite being used in older editions, d20s started being the default dice starting at D&D 3rd edition.↩︎

I hate to admit it, but I still confuse my d20s and d12s on occasion.↩︎

Tom Dunn has written a great series analyzing variability which I highly recommend in The Finished Book.↩︎

If you haven’t read the previous post, I suggest you go back and do it. It’s got a little more context on Advent of Code and it’s a great way to see `{aor}`

in action!

I know said I had exams and papers to write and I wasn’t expecting to write again so soon, but I was told Day 2 was easier than Day 1, so here I am! :)

Let’s start by fetching the puzzle and input for Day 2:

```
> aor::day_start("2023-12-02", "aoc2023/")
✔ Fetched puzzle.
✔ Fetched input.
✔ Created directory aoc2023/02_cube_conundrum
✔ Wrote part 1 to aoc2023/02_cube_conundrum/puzzle.R
✔ Wrote input to aoc2023/02_cube_conundrum/input.txt
ℹ To fetch part 2, run `aor::day_continue("2023-12-02", "aoc2023/02_cube_conundrum/puzzle.R")`
```

Again, there are many ways to solve the puzzle, but I ended up using `dplyr`

and `tidyr`

. Thankfully Day 2 was easier than Day 1; all you had to do was count red, green and blue balls and check some given conditions.

For the first part of the puzzle, you had to find all posible games given a number of balls (so you had to filter out games that exceeded that).

For example, the record of a few games might look like this:

Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green

Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue

Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red

Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red

Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green

In game 1, three sets of cubes are revealed from the bag (and then put back again). The first set is 3 blue cubes and 4 red cubes; the second set is 1 red cube, 2 green cubes, and 6 blue cubes; the third set is only 2 green cubes. The Elf would first like to know which games would have been possible if the bag containedonly 12 red cubes, 13 green cubes, and 14 blue cubes?

Answer for part 1, Day 2 – click to see my solution

```
input <- "aoc2023/02_cube_conundrum/input.txt"
input |>
readr::read_delim(col_names = c("game_id", "sets"), show_col_types = FALSE) |>
dplyr::mutate(
game_id = as.numeric(stringr::str_extract(game_id, "[0-9]+")),
sets = stringr::str_split(sets, ";")
) |>
tidyr::unnest(sets) |>
dplyr::mutate(
green = as.numeric(stringr::str_extract(sets, "[0-9]+(?= green)")),
blue = as.numeric(stringr::str_extract(sets, "[0-9]+(?= blue)")),
red = as.numeric(stringr::str_extract(sets, "[0-9]+(?= red)"))
) |>
tidyr::replace_na(list(green = 0, blue = 0, red = 0)) |>
dplyr::group_by(game_id) |>
dplyr::filter(red > 12 | green > 13 | blue > 14) |>
dplyr::distinct(game_id, .keep_all = TRUE) |>
dplyr::pull(game_id) |>
setdiff(1:100, y = _) |>
sum()
```

For part 2, you had to find the mininum number of balls of each color for each game.

As you continue your walk, the Elf poses a second question: in each game you played, what is the

fewest number of cubes of each colorthat could have been in the bag to make the game possible? Again consider the example games from earlier:

Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green

Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue

In game 1, the game could have been played with as few as 4 red, 2 green, and 6 blue cubes. If any color had even one fewer cube, the game would have been impossible.

Game 2 could have been played with a minimum of 1 red, 3 green, and 4 blue cubes.

Answer for part 2, Day 2 – click to see my solution

```
input |>
readr::read_delim(col_names = c("game_id", "sets"), show_col_types = FALSE) |>
dplyr::mutate(
game_id = as.numeric(stringr::str_extract(game_id, "[0-9]+")),
sets = stringr::str_split(sets, ";")
) |>
tidyr::unnest(sets) |>
dplyr::mutate(
green = as.numeric(stringr::str_extract(sets, "[0-9]+(?= green)")),
blue = as.numeric(stringr::str_extract(sets, "[0-9]+(?= blue)")),
red = as.numeric(stringr::str_extract(sets, "[0-9]+(?= red)"))
) |>
tidyr::replace_na(list(green = 0, blue = 0, red = 0)) |>
dplyr::group_by(game_id) |>
dplyr::summarise(max_red = max(red), max_green = max(green), max_blue = max(blue)) |>
dplyr::mutate(power = max_red * max_green * max_blue) |>
dplyr::pull(power) |>
sum()
```

If you have any other ideas, feel free to tell me more on Mastodon or Bluesky.

Happy coding! <3

It’s *that* time of the year again! 🎄

I learned about Advent of Code a couple of years ago, when Caio challenged himself to complete every puzzle and post about it.

Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like. People use them as interview prep, company training, university coursework, practice problems, a speed contest, or to challenge each other. – Advent of Code

I’ve tried to solve some of them, but this time of the year is usually so hectic that I don’t think I ever got past day 6. Maybe this is the year I get to the second week? (Nevermind that I’m already late.)

I still have a full week ahead of exams and papers, so I’ll probably take it slow, but I wanted to lay the groundwork. I’m starting by installing {aor}, which is a neat R package with some useful functions to help you with Advent of Code, so you can focus on actually solving the puzzles.

```
# install.packages("devtools")
devtools::install_github("clente/aor")
```

Once installed and with the right cookie configurations (all explained in `aor`

’s readme), I can simply run

```
> aor::day_start("2023-12-01", "aoc2023/")
✔ Fetched puzzle.
✔ Fetched input.
✔ Created directory aoc2023/01_trebuchet
✔ Wrote part 1 to aoc2023/01_trebuchet/puzzle.R
✔ Wrote input to aoc2023/01_trebuchet/input.txt
ℹ To fetch part 2, run `aor::day_continue("2023-12-01", "aoc2023/01_trebuchet/puzzle.R")`
```

…and I’ll have a directory for that day’s puzzle, with a template for the code and the input text! It’s important to note that you must be logged in to get the puzzle input, as they are different across users.

Next step is solving the puzzles. I’m starting with day one.

To sum things up, the first part of day 1 is:

- Read a file with text
- Identify the first and last numbers on each line (the “calibration”) and sum them up

Every puzzle comes with a minimal example:

For example:

1abc2

pqr3stu8vwx

a1b2c3d4e5f

treb7uchet

In this example, the calibration values of these four lines are`12`

,`38`

,`15`

, and`77`

. Adding these together produces.`142`

There are many ways this can be done, but I ended up using `dplyr`

because I’m more used to it. With a little bit of regex, it was easy enough to extract the numbers that I needed to clear part 1.

Answer for part 1, Day 1 – click to see my solution

```
input <- "aoc2023/01_trebuchet/input.txt"
input |>
readr::read_csv(col_names = "input", show_col_types = FALSE) |>
dplyr::filter(input != "") |>
dplyr::mutate(
first = stringr::str_extract(input, "[0-9]"),
last = stringr::str_extract(input, "[0-9](?!.*[0-9])"),
calibration = as.numeric(paste0(first, last))
) |>
dplyr::pull(calibration) |>
sum()
```

Part 2 was a little bit trickier. The puzzle says:

Your calculation isn’t quite right. It looks like some of the digits are actually

spelled out with letters:`one`

,`two`

,`three`

,`four`

,`five`

,`six`

,`seven`

,`eight`

, and`nine`

alsocount as valid “digits”. Equipped with this new information, you now need to find the real first and last digit on each line. For example:

two1nine

eightwothree

abcone2threexyz

xtwone3four

4nineeightseven2

zoneight234

7pqrstsixteen

In this example, the calibration values are`29`

,`83`

,`13`

,`24`

,`42`

,`14`

, and`76`

. Adding these together produces.`281`

First try for part 2, Day 1 – click to see my failed attempt

My first idea was using regex to get all ocurrences of numbers *and* spelled out numbers. So my regex would look something like

`rx <- ("[0-9]|one|two|three|four|five|six|seven|eight|nine")`

Then, I could switch the spelled out numbers, paste the first and last ones and sum them up.

```
switch_numbers <- function(num) {
if (stringr::str_detect(num, "[a-z]")) {
result <- switch(
num, one = 1, two = 2, three = 3, four = 4, five = 5, six = 6, seven = 7,
eight = 8, nine = 9
)
} else {
result <- num
}
as.numeric(result)
}
input |>
readr::read_csv(col_names = "input", show_col_types = FALSE) |>
dplyr::filter(input != "") |>
dplyr::mutate(
numbers = stringr::str_extract_all(input, rx),
first = purrr::map_vec(numbers, head, n = 1),
last = purrr::map_vec(numbers, tail, n = 1),
first = purrr::map_vec(first, switch_numbers),
last = purrr::map_vec(last, switch_numbers),
calibration = as.numeric(paste0(first, last))
)|>
dplyr::pull(calibration) |>
sum()
```

*But*, turns out I was wrong. You can see the problem with `str_extract_all`

in this case:

```
> stringr::str_extract_all("threeight", rx)
[[1]]
[1] "three"
```

What I actually wanted:

```
[[1]]
[1] "three" [2] "eight"
```

The regex I was using does not take into account overlapping!

This takes us to attempt #2, where I try to take this problem into account with `stringi`

.

Second try for part 2, Day 1 – click to see my solution

```
switch_numbers <- function(num) {
if (stringr::str_detect(num, "[a-z]")) {
result <- switch(
num, one = 1, two = 2, three = 3, four = 4, five = 5, six = 6, seven = 7,
eight = 8, nine = 9
)
} else {
result <- num
}
as.numeric(result)
}
rx <- paste0("(?=([0-9]|", paste(xfun::n2w(1:9), collapse = "|"), "))")
input |>
readr::read_csv(col_names = "input", show_col_types = FALSE) |>
dplyr::filter(input != "") |>
dplyr::mutate(
numbers = stringi::stri_match_all_regex(input, rx),
numbers = purrr::map(numbers, ~magrittr::extract(.x, ,2)),
first = purrr::map_vec(numbers, head, n = 1),
last = purrr::map_vec(numbers, tail, n = 1),
first = purrr::map_vec(first, switch_numbers),
last = purrr::map_vec(last, switch_numbers),
calibration = as.numeric(paste0(first, last))
) |>
dplyr::pull(calibration) |>
sum()
```

Using a lookahead in the regex (?=) and `stringi::stri_match_all_regex`

did the trick and got me to the right answer! 🥳

If you have any other ideas, feel free to tell me more on Mastodon or Bluesky.

Happy coding! <3

I was honored to be an **Opportunity Scholar** on my first posit::conf ever, which means admission tickets, hotel and flight expeses were covered by Posit. Considering that just the admission ticket is a *lot* of money, especially if you come from a country with a weak currency, I would probably never even consider attending the conference out of my own pocket (although now that I’ve been to one, I’m dying to go again next year).

I was (quite literally) speechless for most of the event (more on that later). It was so inspiring to see other Opportunity Scholars giving talks, sharing meals with fellow R nerds, and talking about everything that R allows us to do, from awesome presentations to great dataviz and reproducible modeling. It was also so nice to find common interests with various people outside of strictly professional topics.

I don’t think I’ll be able to summarize all 4 days, but here goes nothing. The following is a short list of my personal highlights of the conference. If you want to have an overall view of Posit’s announcements, head over to Posit’s blog post where they listed their five takeaways from posit::conf(2023),

So many things are happening with Quarto! I’m especially excited to try out **typst**, which promises to be friendlier than LaTeX. I also finally started this blog and website, which had been in my plans for years.

DevOps was my choice for the first two days of workshops. It’s a shame that, because it was a two-day workshop, that meant I couldn’t do two different workshops, but I regret nothing. I learned so much (who’s intimidated by Docker now?? Not me, that’s for sure. Well, maybe just a little bit, but hey, baby steps), and once I got home, the project I was working on during the workshop evolved into this website. I bought the domain a few months back, and this was the push I needed to actually do something with it.

I’m part of the local chapter of R-Ladies in Sao Paulo but had little to no interaction with the global team. The R-Ladies meetup on the last day was a great opportunity to better understand the workings of the organization and be inspired by awesome people who do such important work within the R community. More details of the meetup are available at this blog post.

Being in a place with hundreds of people can be a little overwhelming. I found that the posit::conf Discord server was a great way to start a conversation (kudos for the foodie channel!) in a more controlled environment – while also keeping track of sticker drops!

There are also a few things that I wanted to share with other people planning to attend next year.

Yes, you, the person standing awkwardly in a corner while everyone else is mingling and seems to be having a great time. There are other people doing exactly the same. Try to approach them with some questions, maybe? Here are some basic conversation starters once you’re past the “what do you do for work” stage that are not *too* boring but also not too unrealistic:

- What talks did you like the most so far?
- What did you learn that you are excited to try?
- What’s your favorite R package?
- What is the R community like where you’re based?
- Is it your first time in {city}? Do you have any plans apart from the conf?

Talking to people who are very knowledgeable can be either intimidating or mind-opening. I had a great time talking to a much more experienced developer who explained so much about their project in a friendly way that I felt welcomed to share my thoughts and ask questions. Be mindful not to be the arrogant person (even if unconsciously) who assumes people know what you’re talking about.

By the end of the second day, I was physically and mentally exhausted. To be honest, I was recovering from a cold a few days before my flight, and was feeling great, but two days of talking non-stop, some alcohol and early mornings made me so hoarse I could barely hear myself speak.

So I decided to skip some talks (that I knew would be recorded anyway, so I could catch up later) and rested. During one of my breaks, I had a lovely time making friendship bracelets at the arts and crafts table with other people who were recharging their social batteries. I’m also not much of a morning person, so I purposefully missed breakfast on the last day to have tea and some quiet time by myself outside the hotel.

It was absolutely amazing to attend posit::conf(2023). Next year it’s going to be in Seattle and I’m already thinking about *how* I’m going to be there. Fingers crossed for getting a talk acceppted! 🤞