Advent of Code 2022 — Day 2 (Elixir)

Ben Burke
8 min readDec 3, 2022

I’m a massive fan of the Elixir programming language. So I decided to solve all of the Advent of Code problems with it.

Photo by tabitha turner on Unsplash

Table of Contents | Advent of Code Day 1 | 🚧 Advent of Code Day 3 🚧

Day 2 materials can be found here: https://adventofcode.com/2022/day/2

On Day 2, our elven friends are playing Rock, Paper, Scissors, also known as Roshambo or Jankenpon, to decide who gets to sit closest to the snack storage. Incredibly relatable.

The winner is determined by points. You get 6 points for a win, 3 points for a draw, and no points for a loss. In addition, a hand of rock is worth one point, paper is worth two, and scissors is worth three.

So if you win a round, and you threw the scissors hand, you would be awarded 6 + 3 = 9 points.

The elves liked your work so much yesterday that they’re writing down their tosses so you can win! Each elf has written down their own rock paper scissors hand, and conveniently, they wrote your hand as well. The master cheat sheet they’ve given you looks like this:

A Y
B X
C Z

Rock is both A and X, Paper is both B and Y, and Scissors is C and Z. We’ll need to determine who wins in each round, sum the points, and figure out how many points we scored.

Let’s start modeling our first problem of Day 2.

There are a few ways to tackle problems like this. My first attempt used modulo 3 math to mathematically transform the [A, B, C] and [X, Y, Z] (in essence transforming [Rock, Paper, Scissors]) lists to [0, 1, 2].

For those unfamiliar with modulo math, let’s talk about it briefly. The series of unsigned integer numbers [0, 1, 2, 3, 4, 5, 6, 7, …] continues infinitely. Let’s call this set I. This means that an infinite number of numbers can be divided by 2, by 3, by 5, and so on. We can take our infinitely continuing set of integers and transform it to our problem space of [Rock, Paper, Scissors] by dividing by 3, the number of distinct outcomes, and obtaining the remainder.

In our formula f(x) = x % 3 for x in I,[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...] (or I) becomes [0, 1, 2, 0, 1, 2, 0, 1, 2, …] also repeating infinitely. Let’s model this set not with numbers, but with our Rock, Paper, and Scissors outcomes.

rock     paper    scissors 
paper scissors rock
scissors rock paper

Moving along the horizontal axis left to right, and moving along the vertical axis top to bottom, is the transformation (n + 1) % 3 in our problem space.

rock     paper    scissors
paper
scissors

This is our losing direction. If their_hand == (our hand + 1) % 3 is true, we can determine with math that our hand loses.

When we travel these axes in the opposite direction, this is the mathematical transformation (n — 1) % 3. However, we’re dealing with unsigned integers with modulo math, not signed numbers. Let’s remodel this — 1 to obtain the same remainder when divided by three.

Let’s make sure that when we subtract one, we don’t obtain a negative number. We know in our problem space that the set [0, 1, 2] repeats infinitely, so let’s use that fact to manipulate our matrix.
If the number we start with is x and we subtract one:

[0, 1, 2, 0, 1, 2, 0, 1, x, 0, 1, 2, 0, 1, 2]
(x - 1) % 3 = 1
(x + 2) % 3 = 1

What we’re really asking for is the previous number in the set, or the number two places in front of our number.
So in place of f(x) = (x — 1) % 3, let’s use f(x) = (x + 2) % 3 instead.

Remember that these numbers correspond to our list of outcomes [Rock, Paper, Scissors]. We can now say that their_hand == (our hand + 2) % 3 is true, we can determine with math that our hand wins.

Not all problems should be modeled with math. The truth is, we aren’t computers. Lots of people think that programming is about math, but it usually isn’t. Most of the required math in computer science is abstracted away from us developers, and we worry about problems like mapping how things are related. Much like our own brains. After all, code is written by brains not computers.

It’s extremely hard to extend this math to something useful for the second solution. I admit, I was stumped. I’ll explain why when we get there.

For now, we’ll attack Problem 1 differently, using the small size of our problem space to our benefit.

Elixir has structures instead of classes. Structures operate like a class, having attributes and methods that modify the structure. Unlike classes with instances, data is immutable in Elixir. When you manipulate a structure, say by setting Person.name to “Johnny Appleseed”, you’re given back an altered structure at a different place in memory. The old structure is garbage collected. So structures don’t have a concept of class instances.

Pattern matching a structure in Elixir is easy. If you’re new to the language or to my series, I’ve covered pattern matching in my Day 1 entry of this series. There I discuss pattern matching variables, literals, and lists. Here I will discuss pattern matching a structure.

Let’s finally dive into some code in lib/day02/day2.ex:

defmodule AdventOfCode.Day2 do
@loss 0
@draw 3
@win 6

def read_input() do
(__DIR__ <> "/input.txt")
|> File.read!()
|> String.split(~r/\r?\n/, trim: true)
end
end

Small sidebar about Elixir function names: read_input() and read_input(arg) with one argument are different functions, and they are referred to in Elixir terms as read_input/0 and read_input/1, respectively. The slash is the number of arguments the function has and is called the function’s arity. I will be referring to a lot of functions in this series, so starting in this Day 2 entry I’ll be writing them like this from now on.

We copy read_input/1 from our Day 1 entry, and we change the repetition in our String.split/3 regular expression from matching \r\n\r\n and \n\n to match \n or \r\n instead. (\r?\n){2} becomes \r?\n.

Next, let’s model our Rock, Paper, and Scissors hands in a Hand structure:

defmodule AdventOfCode.Day2.Hand do
defstruct [:val, :points, :beats, :loses_to]

def new(:rock) do
%AdventOfCode.Day2.Hand{
val: :rock,
points: 1,
beats: :scissors,
loses_to: :paper
}
end

def new(:paper) do
%AdventOfCode.Day2.Hand{
val: :paper,
points: 2,
beats: :rock,
loses_to: :scissors
}
end

def new(:scissors) do
%AdventOfCode.Day2.Hand{
val: :scissors,
points: 1,
beats: :paper,
loses_to: :rock
}
end

def from_character("A"), do: new(:rock)
def from_character("B"), do: new(:paper)
def from_character("C"), do: new(:scissors)
def from_character("X"), do: new(:rock)
def from_character("Y"), do: new(:paper)
def from_character("Z"), do: new(:scissors)
end

Elixir modules can live in any directory or any file name, so long as the code is located in the lib/ folder. You can put these modules in different files, and for larger projects, it’s recommended. But this module is specific to this problem so I’m just gonna put them in the same file.

Notice that our pattern matches in new/1 create a structure with all the information we need to determine the winner and loser. We were able to model our hand outcomes in this way because our outcomes are limited. For a larger set of outcomes, you’d need to model the shape of the structure differently to account for that size.

Let’s go back to our Day2 module and solve problem 1 with our new structures:

alias AdventOfCode.Day2.Hand

def solution1 do
read_input()
|> Stream.map(&calculate_hand_score/1)
|> Enum.sum()
end

def calculate_hand_score(line) do
[their_hand, my_hand] = String.split(line, " ")
compare_hands(
Hand.from_character(their_hand),
Hand.from_character(my_hand)
)
end

def compare_hands(their_hand, my_hand) do
cond do
my_hand.val == their_hand.beats -> my_hand.points + @loss
my_hand.val == their_hand.val -> my_hand.points + @draw
my_hand.val == their_hand.loses_to -> my_hand.points + @win
end
end

alias AdventOfCode.Day2.Hand shortens our structure and module name to just Hand. We create both hands from the input line using our Hand.from_character/1 function in calculate_hand_score/1, giving us all the information needed to determine the outcome of the round. compare_hands/2 gives us the score for each round.

The cond macro allows us to list many boolean evaluations in sequence, and the first to evaluate to true will be the return value of cond. Again Elixir gives us a tool to skip the if statements.

Let’s compile our code in our terminal:

iex -S mix

And execute our function to print out the solution to Day 2 Problem 1.

AdventOfCode.Day2.solution1

When I solve a problem, I like to write solutions that are extensible. Usually the two parts of the problem are related, and if you design your code smartly you can set yourself up for quick and minimal changes to solve Problem 2. It was here that my original solution using modulo math failed.

Let’s take another look at our [0, 1, 2], [Rock, Paper, Scissors] sets as a list of [loss,draw,win]. If our hand is 0 (Rock), then 1 is who we lose to (Paper), 2 (Scissors) is who we win to, and 3 or 0is a draw.

This can be modeled as a set, too. Given a number or hand outcome, the next three numbers, i.e +1 and +2 and +3, will be the game outcome set of [loss, win, draw]. Remember that the position in the set is not important, but the position relative to our hand is.

It’s here in Problem 2 that we map the X , Y , and Z characters from our input file to the outcomes loss, draw, win. If we extend our above [loss, win, draw] set infinitely in a vertical and horizontal axis, we notice a few things about our set based on [Rock, Paper, Scissors]:

loss win  draw loss win  draw loss win  draw loss win  draw
win draw loss win draw loss win draw loss win draw loss
draw loss win draw loss win draw loss win draw loss win
loss win draw loss win draw loss win draw loss win draw
win draw loss win draw loss win draw loss win draw loss

First, we notice that to produce the set [loss, draw, win] as the problem maps X, Y, and Z to, we have to perform a different manipulation to the set positions to extract what the hand is. This melted my brain and at this point I decided to use structures on this limited outcome set.

Lots of problems in computer science can be modeled with math, but that doesn’t mean they should be.

Choosing to let go of my previous failure gave life to this solution to Problem 2:

defmodule AdventOfCode.Day2.Hand do
def from_strategy(%AdventOfCode.Day2.Hand{beats: loser}, "X") do
new(loser)
end
def from_strategy(%AdventOfCode.Day2.Hand{val: draw}, "Y") do
new(draw)
end
def from_strategy(%AdventOfCode.Day2.Hand{loses_to: winner}, "Z") do
new(winner)
end
end

defmodule AdventOfCode.Day2 do
def solution2 do
read_input()
|> Stream.map(&calculate_strategy_score/1)
|> Enum.sum()
end

def calculate_strategy_score(line) do
[their_hand, strategy] = String.split(line, " ")
their_hand = Hand.from_character(their_hand)

compare_hands(
their_hand,
Hand.from_strategy(their_hand, strategy)
)
end
end

Thanks to from_character/1 and our structure, we have everything we need from a single hand to know who loses and wins. We pattern match in from_strategy/1 to get the value we care about for that function clause of “X” losing, "Y" tying, and “Z” winning. Then we construct the appropriate hand with new/1.

Once again, we compile the code and get our solution:

AdventOfCode.Day2.solution2

For more entries of Advent of Code and other topics, please follow me on Medium at https://talesoffullstack.medium.com/

Your support helps me decide whether this content is helpful and informative, and your feedback also helps me create better, more in-depth articles. If you liked this article, please consider leaving a clap or a comment!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Ben Burke
Ben Burke

Written by Ben Burke

Tales of Full Stack (TOFS) is a technical blog of my professional experiences as a full stack software developer. I write in Elixir, Go, Python, and Typescript.

No responses yet

Write a response