Home

# Day 2 (Advent of Code 2022) From the series Advent of Code 2022

## Part 1

In the day 2 challenge, we're playing Rock Papers Scissors.

We're given a strategy guide like so:

```A Y
B X
C Z
```

Left column is "their move": A means Rock, B means Paper, C means Scissors. Right column is "our move": X means Rock, Y means Paper, Z means Scissors.

Each line corresponds to a turn, and we must calculate the total score we get. Picking "Rock" gives 1 point, "Paper" gives 2 points, and "Scissors" gives 3. Losing the round gives 0 points, drawing gives 3, winning it gives 6.

Line `A Y` means they picked "Rock", we picked "Paper" (2 points), and we won (6 points), so our score goes up by 8.

Line `B X` means they picked "Paper", we picked "Rock" (1 point), and we lost, so our score goes up by 1.

Line `C Z` means we both picked "Scissors" (3 points) and it's a draw (3 points), so our score goes up by 6, for a grand total of 8 + 1 + 6 = 15.

Okay! So! First off, what will our types look like?

For every round we can do one of three moves, that sounds like a sum type, let's make an enum for it:

Rust code
```#[derive(Debug, Clone, Copy)]
enum Move {
Rock,
Paper,
Scissors,
}
```

Every round is one of our moves and one of their moves - sounds like a struct, let's make one:

Rust code
```#[derive(Debug, Clone, Copy)]
struct Round {
theirs: Move,
ours: Move,
}
```

We should be able to parse a `Move` from either ABC or XYZ, let's implement the `TryFrom` trait to convert from `char`. Not `From`, because our conversion is fallible: if we get the letter `J`, we'll error out.

Rust code
```impl TryFrom<char> for Move {
type Error = color_eyre::Report;

fn try_from(c: char) -> Result<Self, Self::Error> {
match c {
'A' | 'X' => Ok(Move::Rock),
'B' | 'Y' => Ok(Move::Paper),
'C' | 'Z' => Ok(Move::Scissors),
_ => Err(color_eyre::eyre::eyre!("not a valid move: {c:?}")),
}
}
}
```

Now we can, say, parse a `Round` given a `&str` (a single line):

Rust code
```impl FromStr for Round {
type Err = color_eyre::Report;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
let (Some(theirs), Some(' '), Some(ours), None) = (chars.next(), chars.next(), chars.next(), chars.next()) else {
return Err(color_eyre::eyre::eyre!("expected <theirs>SP<ours>EOF, got {s:?}"));
};

Ok(Self {
theirs: theirs.try_into()?,
ours: ours.try_into()?,
})
}
}
```

The FromStr trait is what we used in day 1 when we had to turn `String` / `&str` values into `u64`.

And then our `main` function can look something like this:

Rust code
```fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

for round in include_str!("input.txt")
.lines()
.map(|line| line.parse::<Round>())
{
let round = round?;
println!("{round:?}");
}

Ok(())
}
```

Just like last time, we're splitting `input.txt` into a bunch of lines, then parsing each of them into a `Round`. We now have an `Iterator<Item = Result<Round, Report>>`.

The `let round = round?;` line takes care of propagating any parsing errors, and then we just print each individual round.

Let's try it out:

Shell session
```\$ cargo run --quiet
warning: fields `theirs` and `ours` are never read
--> src/main.rs:25:5
|
24 | struct Round {
|        ----- fields in this struct
25 |     theirs: Move,
|     ^^^^^^
26 |     ours: Move,
|     ^^^^
|
= note: `#[warn(dead_code)]` on by default
= note: `Round` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis

Round { theirs: Rock, ours: Paper }
Round { theirs: Paper, ours: Rock }
Round { theirs: Scissors, ours: Scissors }
```

Let's ignore the warnings for now (we'll use that code soon enough...) and check that it matches what we expected. Instant replay please?

Line `A Y` means they picked "Rock", we picked "Paper" (2 points), and we won (6 points), so our score goes up by 8.

Line `B X` means they picked "Paper", we picked "Rock" (1 point), and we lost, so our score goes up by 1.

Line `C Z` means we both picked "Scissors" (3 points) and it's a draw (3 points), so our score goes up by 6, for a grand total of 8 + 1 + 6 = 15.

Okay, that tracks.

Next up, let's add code that determines how many points we get for picking a move:

Rust code
```impl Move {
/// How many points do we get for picking that move?
fn inherent_points(self) -> usize {
match self {
Move::Rock => 1,
Move::Paper => 2,
Move::Scissors => 3,
}
}
}
```

Also, which move beats which. Or rather... what outcome there is when matching two moves together: it's not just a boolean, there's three possible outcomes:

Rust code
```#[derive(Debug, Clone, Copy)]
enum Outcome {
Win,
Draw,
Loss,
}
```

And now we can use this to implement `Move::outcome`:

Rust code
```impl Move {
fn outcome(self, theirs: Move) -> Outcome {
match (self, theirs) {
(Move::Rock, Move::Rock) => Outcome::Draw,
(Move::Rock, Move::Paper) => Outcome::Loss,
(Move::Rock, Move::Scissors) => Outcome::Win,
(Move::Paper, Move::Rock) => Outcome::Win,
(Move::Paper, Move::Paper) => Outcome::Draw,
(Move::Paper, Move::Scissors) => Outcome::Loss,
(Move::Scissors, Move::Rock) => Outcome::Loss,
(Move::Scissors, Move::Paper) => Outcome::Win,
(Move::Scissors, Move::Scissors) => Outcome::Draw,
}
}
}
```

I don't love the look of this though, it seems like it'd be really easy for a mistake to slip in there.

Let's try something else:

Rust code
```impl Move {
fn beats(self, other: Move) -> bool {
matches!(
(self, other),
(Self::Rock, Self::Scissors)
| (Self::Paper, Self::Rock)
| (Self::Scissors, Self::Paper)
)
}

fn outcome(self, theirs: Move) -> Outcome {
if self.beats(theirs) {
Outcome::Win
} else if theirs.beats(self) {
Outcome::Loss
} else {
Outcome::Draw
}
}
}
```

Mhh yes, I like this more: the logic is encoded once, and we pray to the optimizing compiler gods that it's just as efficient as the other one.

Now we can also add an `inherent_points` method on `Outcome`:

Rust code
```impl Outcome {
fn inherent_points(self) -> usize {
match self {
Outcome::Win => 6,
Outcome::Draw => 3,
Outcome::Loss => 0,
}
}
}
```

And now, a `score` method on `Round`:

Rust code
```impl Round {
fn outcome(self) -> Outcome {
self.ours.outcome(self.theirs)
}

fn our_score(self) -> usize {
self.ours.inherent_points() + self.outcome().inherent_points()
}
}
```

Let's dump everything we know about the script (sorry, "strategy guide") we were given:

Rust code
```fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

for round in include_str!("input.txt")
.lines()
.map(|line| line.parse::<Round>())
{
let round = round?;
println!(
"{round:?}: outcome={outcome:?}, our score={our_score}",
outcome = round.outcome(),
our_score = round.our_score()
);
}

Ok(())
}
```

And now we just need to calculate the sum of scores!

Rust code
```fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

let total_score = include_str!("input.txt")
.lines()
.map(|line| line.parse::<Round>())
.map(|round| round.our_score())
.sum();

Ok(())
}
```
Shell session
```\$ cargo run --quiet
error[E0599]: no method named `our_score` found for enum `Result` in the current scope
--> src/main.rs:108:28
|
108 |         .map(|round| round.our_score())
|
note: the method `our_score` exists on the type `Round`
--> src/main.rs:81:5
|
81  |     fn our_score(self) -> usize {
|     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: consider using `Result::expect` to unwrap the `Round` value, panicking if the value is a `Result::Err`
|
108 |         .map(|round| round.expect("REASON").our_score())
|                           +++++++++++++++++

error: could not compile `day2` due to previous error
```

Oh, that's right! Parsing a `&str` into a `Round` is fallible. Well, the compiler gives a suggestion (and holy fuck I didn't see that one coming), but here's a neat trick we can use instead: `collect()` can collect into a `Result<Vec<_>, _>` (where `_` lets the compiler figure it out).

Rust code
```fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

let rounds: Vec<Round> = include_str!("input.txt")
.lines()
.map(|line| line.parse())
.collect::<Result<_, _>>()?;
let total_score: usize = rounds.iter().map(|r| r.our_score()).sum();
dbg!(total_score);

Ok(())
}
```

In fact there's several neat things going on here: the first `_` in `Result<_, _>` is inferred to be `Vec<Round>` because we specify the type in `let rounds: Vec<Round>`.

Similarly, we don't need a turbofish for `sum` because we assign it to `let total_score: usize`.

We can play around with this, settling on whichever alternative we like best:

Rust code
```fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

//             ðŸ‘‡ now inferred
let rounds: Vec<_> = include_str!("input.txt")
.lines()
//    ðŸ‘‡ thanks to this
.map(Round::from_str)
.collect::<Result<_, _>>()?;
let total_score: usize = rounds.iter().map(|r| r.our_score()).sum();
dbg!(total_score);

Ok(())
}
```

Oh, and the `dbg!` macro is "pass-through": it evaluates to whatever its argument is, but it also prints it with the file name and line number:

Shell session
```\$ cargo run --quiet
[src/main.rs:110] total_score = 15
```

But, wait... why collect into a `Vec`? We just need to sum everything.

Ah, you're right. We don't want to collect everything, because we don't need to. But then... error handling gets complicated.

It's not if we use imperative code, like so:

Rust code
```fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

let mut total_score = 0;
for round in include_str!("input.txt").lines().map(Round::from_str) {
total_score += round?.our_score();
}
dbg!(total_score);

Ok(())
}
```

...and maybe that's the version we ought to stick with.

But let's preoccupy ourself with whether we could (stick with iterators / functional style), rather than whether we really should.

Shell session
```\$ cargo add itertools
(cut)
```
Rust code
```fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

let total_score: usize = itertools::process_results(
include_str!("input.txt")
.lines()
.map(Round::from_str)
.map(|r| r.map(|r| r.our_score())),
|it| it.sum(),
)?;
dbg!(total_score);

Ok(())
}
```

Or even:

Rust code
```// ðŸ‘‹ new!
use itertools::{process_results, Itertools};

fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

let total_score: usize = itertools::process_results(
include_str!("input.txt")
.lines()
.map(Round::from_str)
// ðŸ‘‡ this is provided by `Itertools`
.map_ok(|r| r.our_score()),
|it| it.sum(),
)?;
dbg!(total_score);

Ok(())
}
```

This works just as well:

Shell session
```\$ cargo run --quiet
[src/main.rs:114] total_score = 15
```

And, running it on my puzzle input, this gets me to part two.

## Part 2

In part two, we learn that `X`, `Y` and `Z` do not, in fact, mean "Rock", "Paper" and "Scissors", but they indicate how the round must end. "X" means loss, "Y" means draw" and "Z" means win â€” it's on us to figure out what we need to play.

Before we do anything, let's re-order our `Outcome` enum so it matches that:

Rust code
```#[derive(Debug, Clone, Copy)]
enum Outcome {
Loss,
Draw,
Win,
}
```

This is completely unnecessary but it makes me very happy.

Well, as long as you're happy.

Ok, next we'll want... we'll want some helpers. Like so:

Rust code
```impl Move {
const ALL_MOVES: [Move; 3] = [Move::Rock, Move::Paper, Move::Scissors];

fn winning_move(self) -> Self {
Self::ALL_MOVES
.iter()
.copied()
.find(|m| m.beats(self))
.expect("at least one move beats us")
}

fn losing_move(self) -> Self {
Self::ALL_MOVES
.iter()
.copied()
.find(|&m| self.beats(m))
.expect("we beat at least one move")
}

fn drawing_move(self) -> Self {
self
}
}
```

So this is probably not the fastest code, but I like it a lot, because again, we've encoded the logic just once. I think it's very elegant.

Are you done congratulating yourself? Can we move on?

Listen bear, you gotta stop and smell the roses. Even if you're the one who grew them.

Uh-huh.

Code is not... written. It's discovered. I am merely the vessel through which-

MOVING ON.

Ok now, we'll want to change our `Move` parser so it only parses from "ABC" and not "XYZ":

Rust code
```impl TryFrom<char> for Move {
type Error = color_eyre::Report;

fn try_from(c: char) -> Result<Self, Self::Error> {
match c {
'A' => Ok(Move::Rock),
'B' => Ok(Move::Paper),
'C' => Ok(Move::Scissors),
_ => Err(color_eyre::eyre::eyre!("not a valid move: {c:?}")),
}
}
}
```

And then, parse `Outcome`s:

Rust code
```impl TryFrom<char> for Outcome {
type Error = color_eyre::Report;

fn try_from(c: char) -> Result<Self, Self::Error> {
match c {
'X' => Ok(Outcome::Loss),
'Y' => Ok(Outcome::Draw),
'Z' => Ok(Outcome::Win),
_ => Err(color_eyre::eyre::eyre!("not a valid outcome: {c:?}")),
}
}
}
```

We can even add a helper to `Outcome` itself, to find the move that matches, given their move:

Rust code
```impl Outcome {
fn matching_move(self, theirs: Move) -> Move {
match self {
Outcome::Win => theirs.winning_move(),
Outcome::Draw => theirs.drawing_move(),
Outcome::Loss => theirs.losing_move(),
}
}
}
```

Now, we can change `Round`'s `FromStr` implementation to parse their move, the desired outcome, and decide what our move should be:

Rust code
```impl FromStr for Round {
type Err = color_eyre::Report;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
let (Some(theirs), Some(' '), Some(outcome), None) = (chars.next(), chars.next(), chars.next(), chars.next()) else {
return Err(color_eyre::eyre::eyre!("expected <theirs>SP<outcome>EOF, got {s:?}"));
};
let theirs = Move::try_from(theirs)?;
let outcome = Outcome::try_from(outcome)?;
let ours = outcome.matching_move(theirs);

Ok(Self { theirs, ours })
}
}
```

Let's try it again with the sample input, adding a couple of well-placed `dbg!` invocations to look at how the `Round` values were parsed and what the individual score was:

Rust code
```    let total_score: usize = itertools::process_results(
include_str!("input.txt")
.lines()
.map(Round::from_str)
//    there ðŸ‘‡   ðŸ‘‡
.map_ok(|r| dbg!(dbg!(r).our_score())),
|it| it.sum(),
)?;
dbg!(total_score);
```
Shell session
```\$ cargo run
Compiling day2 v0.1.0 (/home/amos/bearcove/aoc2022/day2)
Finished dev [unoptimized + debuginfo] target(s) in 0.67s
Running `target/debug/day2`
[src/main.rs:154] r = Round {
theirs: Rock,
ours: Rock,
}
[src/main.rs:154] dbg!(r).our_score() = 4
[src/main.rs:154] r = Round {
theirs: Paper,
ours: Rock,
}
[src/main.rs:154] dbg!(r).our_score() = 1
[src/main.rs:154] r = Round {
theirs: Scissors,
ours: Rock,
}
[src/main.rs:154] dbg!(r).our_score() = 7
[src/main.rs:157] total_score = 12
```

And, with my real puzzle input, that gets us through part two!

I love it when a plan comes together :)