It’s that time of year again! Just like last year, I’ll be posting my solutions to the Advent of Code puzzles. This year, I’ll be solving the puzzles in Rust. I’ll post my solutions and code to GitHub as well. After finishing last year (and 2015-2019) in Julia, I needed to spend some time with Rust again! If you haven’t given AoC a try, I encourage you to do so along with me!
Day 02 - Rock Paper Scissors
Find the problem description HERE.
The Input - First Come, First Served
Today, my suspicions from prior years was confirmed: the surprisingly deterministic nature of elf behavior belies the fact that elves are, in fact, toys constructed by Santa and managed by a (mostly) sophisticated AI. If that’s true, though, then… am I real? Well, that rabbit hole got real deep real fast. Let’s talk about input parsing!
use nom::{
character::complete::{anychar, space1},
error::Error as NomError,
sequence::separated_pair,
Finish, IResult,
};
// Today's input is a list of character pairs
pub type Input = Vec<(char, char)>;
const INPUT: &str = include_str!("../../input/02/input.txt");
/// Attempts to parse a line from the INPUT
fn parse_line(line: &str) -> Result<(char, char), NomError<&str>> {
// parses lines that contain a character, one or more spaces,
// then another character
let (_, char_pair) = separated_pair(anychar, space1, anychar)(line).finish()?;
Ok(char_pair)
}
/// Parse the INPUT
pub fn read() -> Input {
// Parse the lines into (char, char) values and return the resulting
// list. Ignores any lines that fail to parse.
INPUT.lines().flat_map(parse_line).collect()
}
Nothing revolutionary here in terms of functionality, just converting each line like "A X"
to a tuple of ('A', 'X')
. The big thing to note (and keep an eye out for on subsequent days) is the use of the nom
parser combinator crate. One of my personal goals for Advent of Code this year is to improve my text parsing skills, and I’ve chosen to get familiar with nom
. Normally, I’d lean on regular expressions, but I find that the parser combinators are a lot more_debuggable_ when parsing gets complex. So, expect to see more nom
in coming days!
Part One - Rock, Paper, Cheaters
Turns out, one traitor to his people friendly elf has the key to unlocking our chance to pitch a tent right next to the expedition’s snack storage. Excellent! All we need to do is decrypt the strategy guide (based on at least one shaky assumption) and ensure that it won’t be totally obvious how much we’re cheating at this child’s game. No problem!
/// Represents a 'shape' in a game of Rock, Paper, Scissors
#[derive(Clone, Copy)]
pub enum Shape {
Rock,
Paper,
Scissors,
}
/// Convert a `Shape` into it's score value
impl From<Shape> for u32 {
fn from(shape: Shape) -> Self {
match shape {
Shape::Rock => 1,
Shape::Paper => 2,
Shape::Scissors => 3,
}
}
}
/// Trait for converting a character from the input into a `Shape`.
/// I'm using a Trait here so that I can use the same function names
/// in the two different parts but have them behave differently, while
/// sharing some base functionality between parts.
trait TryIntoShape {
type Error;
fn try_into_shape(&self) -> Result<Shape, Self::Error>;
}
impl TryIntoShape for char {
type Error = &'static str;
/// Attempt to convert an input character into a `Shape`. Yes, we know
/// that there will be no other characters, but I like to practice good
/// input hygiene when I can.
fn try_into_shape(&self) -> Result<Shape, Self::Error> {
match self {
'A' | 'X' => Ok(Shape::Rock),
'B' | 'Y' => Ok(Shape::Paper),
'C' | 'Z' => Ok(Shape::Scissors),
_ => Err("Character cannot be converted to `Shape`!"),
}
}
}
/// Represents the outcome of a game of Rock, Paper, Scissors, from the
/// perspective of you, the player. Each variant encapsulates the shape
/// you made to achieve that outcome.
pub enum Outcome {
Win(Shape),
Draw(Shape),
Lose(Shape),
}
impl Outcome {
/// Calculate the score from a given outcome
pub fn score(&self) -> u32 {
match self {
// 6 points for winning + the points for your shape
Outcome::Win(t) => 6 + u32::from(*t),
// 3 points for a draw + the points for your shape
Outcome::Draw(t) => 3 + u32::from(*t),
// 0 points for losing + the points for your shape
Outcome::Lose(t) => u32::from(*t),
}
}
}
/// Trait for converting a pair of characters from the input into an `Outcome`.
/// Same deal as the other trait above.
trait TryIntoOutcome {
type Error;
fn try_into_outcome(&self) -> Result<Outcome, Self::Error>;
}
impl TryIntoOutcome for (char, char) {
type Error = &'static str;
#[rustfmt::skip] // I _like_ my pretty match statement below
fn try_into_outcome(&self) -> Result<Outcome, Self::Error> {
// Attempt to convert both characters into their respective `Shape`
let (ch1, ch2) = self;
let opponent = ch1.try_into_shape()?;
let player = ch2.try_into_shape()?;
// Based on the shapes, determine who won and return the `Outcome`
use Shape::*;
let result = match (opponent, player) {
(Rock, Rock) => Outcome::Draw(player),
(Rock, Paper) => Outcome::Win(player),
(Rock, Scissors) => Outcome::Lose(player),
(Paper, Rock) => Outcome::Lose(player),
(Paper, Paper) => Outcome::Draw(player),
(Paper, Scissors) => Outcome::Win(player),
(Scissors, Rock) => Outcome::Win(player),
(Scissors, Paper) => Outcome::Lose(player),
(Scissors, Scissors) => Outcome::Draw(player),
};
Ok(result)
}
}
/// Solve part one
pub fn solve(input: &Input) -> u32 {
// For each pair of characters in the input, convert each to an `Outcome`,
// calculate the score of that `Outcome`, and return the total as an `Output`.
input
.iter()
.flat_map(|pair| pair.try_into_outcome())
.map(|outcome| outcome.score())
.sum::<u32>()
}
There’s actually quite a lot going on here, so let’s break it down:
- Each letter in the input represents a
Shape
to be thrown by either an elf or you. TheShape
struct just formalized the relationship between that input letter and the shape thrown. Converting aShape
to au32
provides the score for throwing that shape (regardless of who wins). TheTryIntoShape
trait specifies how a character can be transformed into aShape
and provides error checking for invalid characters as well. - Each round of play results in an
Outcome
, where you either win, lose, or draw with your opponent. Because your score for that round is based on the outcome and the shape you threw, we encapsulate yourShape
in each variant ofOutcome
, providing all the information needed to calculate a score, which is accomplished via theOutcome.score()
method. TheTryIntoOutcome
trait facilitates converting a tuple of characters (from the input) into anOutcome
, and it can return an error in cases where an invalid letter is given. - The
TryIntoShape
andTryIntoOutcome
traits are provided this way because of my project structure. Because (spoiler alert!) the meaning of the letters changes a bit in part 2, I wanted to be able to change the some of the functionality of these objects between parts, but not all. So, I broke the parts I wanted to change out into traits, and defined these traits separately for part one and part two. This means my project crate has acrate::day02::part1::TryIntoShape
trait and acrate::day02::part2::TryIntoShape
trait that do different things. Probably not a great strategy in production code, but useful for keeping my parts separate without too much copying and pasting of code in between.
So, given what we know, we convert each line from the input into an Outcome
, score that Outcome
, and pass out the resulting total of all the scores.
Part Two - Patience is a Virtue
Yeah…we all knew that we dun goofed when we made assumptions about what was in that elf strategy plan, right? Right. Turns out, ‘X’, ‘Y’, and ‘Z’ tell us how the round should end, and it’s up to us to figure out how to make it so. No problem, just gotta change out our traits a bit.
/// Trait for converting a character from the input into a `Shape`.
/// I'm using a Trait here so that I can use the same function names
/// in the two different parts but have them behave differently, while
/// sharing some base functionality between parts.
trait TryIntoShape {
type Error;
fn try_into_shape(&self) -> Result<Shape, Self::Error>;
}
impl TryIntoShape for char {
type Error = &'static str;
/// Attempt to convert an input character into a `Shape`. This time,
/// we know that 'X', 'Y', and 'Z' do not represent shapes, so we don't
/// try to convert them.
fn try_into_shape(&self) -> Result<Shape, Self::Error> {
match self {
'A' => Ok(Shape::Rock),
'B' => Ok(Shape::Paper),
'C' => Ok(Shape::Scissors),
_ => Err("Character cannot be converted to `Shape`!"),
}
}
}
/// Trait for converting a pair of characters from the input into an `Outcome`.
/// Same deal as the other trait above.
trait TryIntoOutcome {
type Error;
fn try_into_outcome(&self) -> Result<Outcome, Self::Error>;
}
impl TryIntoOutcome for (char, char) {
type Error = &'static str;
#[rustfmt::skip] // I _still like_ my pretty match statement below
fn try_into_outcome(&self) -> Result<Outcome, Self::Error> {
// Now, we only convert the first character into a `Shape`
let (ch1, result) = self;
let opponent = ch1.try_into_shape()?;
// Using the mapping that 'X' means we lose, 'Y' means we draw, and
// 'Z' means we win, determine the outcome of the game and what shape
// you the player need to make to achieve that outcome, and return
// the `Outcome`.
use Shape::*;
match (opponent, result) {
(Rock, 'Y') => Ok(Outcome::Draw(Rock)),
(Rock, 'Z') => Ok(Outcome::Win(Paper)),
(Rock, 'X') => Ok(Outcome::Lose(Scissors)),
(Paper, 'X') => Ok(Outcome::Lose(Rock)),
(Paper, 'Y') => Ok(Outcome::Draw(Paper)),
(Paper, 'Z') => Ok(Outcome::Win(Scissors)),
(Scissors, 'Z') => Ok(Outcome::Win(Rock)),
(Scissors, 'X') => Ok(Outcome::Lose(Paper)),
(Scissors, 'Y') => Ok(Outcome::Draw(Scissors)),
(_, _) => Err("Cannot convert character pair to an Outcome!"),
}
}
}
/// Solve part two
///
/// Why yes, this is _exactly_ the same solve function as part one. Why do you
/// ask?
pub fn solve(input: &Input) -> u32 {
// For each pair of characters in the input, convert each to an `Outcome`,
// calculate the score of that `Outcome`, and return the total as an `Output`.
input
.iter()
.flat_map(|pair| pair.try_into_outcome())
.map(|outcome| outcome.score())
.sum::<u32>()
}
And, that’s all there is to it. See, I told you all I needed to change was the functionality of those traits.
Wrap Up
Honestly, the hardest part about today’s puzzle was the self-imposed need to keep my code as DRY as I reasonably could without making it harder to read or reason about. Turns out, you can’t have conflicting implementations for structs or enums, even if they’re in two modules that will never conflict, because the Rust compiler can’t be sure that they won’t ever come into conflict. Good lookin’ out, Rust compiler! So, I learned a bit about Rust module and project structure today. If I didn’t explain myself well enough, I encourage you to check out the reposo you can see exactly how my code for today is arranged. Maybe you’ll learn something too!
Top comments (0)