AoC 2022 Day 2: Roshamboinator

Source: Rock Paper Scissors

Part 1

Given a list of Rock Paper Scissors matches with A,B,C or X,Y,Z corresponding to those plays and scoring 1,2,3 points for your play plus 0,3,6 for a loss, draw, or win, what is your total score.

Cool. Let’s over engineer it!

First, a Play and an Outcome:

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Play {
    Rock,
    Paper,
    Scissors,
}

#[derive(Copy, Clone, Debug)]
enum Outcome {
    Lose,
    Draw,
    Win,
}

Now, for Play, we want to be able to build them from the input values, get the value of any play, or determine the Outcome for a match:

impl Play {
    fn new(c: char) -> Play {
        use Play::*;

        match c {
            'A' | 'X' => Rock,
            'B' | 'Y' => Paper,
            'C' | 'Z' => Scissors,
            _ => panic!("unknown play: {:?}", c),
        }
    }

    fn value(self) -> i32 {
        use Play::*;

        match self {
            Rock => 1,
            Paper => 2,
            Scissors => 3,
        }
    }

    fn vs(self, other: Play) -> Outcome {
        use Outcome::*;
        use Play::*;

        match (self, other) {
            (a, b) if a == b => Draw,

            (Rock, Scissors) | (Scissors, Paper) | (Paper, Rock) => Win,

            _ => Lose,
        }
    }
}

The vs function is my favorite. I previously hadn’t gotten to use multiple cases in a match (with |) or guards (with if). Pretty powerful syntax there. I do love languages with solid pattern matching.

Next, we need the value for Outcome as well:

impl Outcome {
    fn value(self) -> i32 {
        use Outcome::*;
        
        match self {
            Lose => 0,
            Draw => 3,
            Win => 6,
        }
    }
}

And then we should be able to parse the input and calculate a score:

fn part1(filename: &Path) -> String {
    let mut total_score = 0;

    for line in read_lines(filename) {
        let them = Play::new(line.chars().nth(0).expect("must have 1 char per line"));
        let us = Play::new(line.chars().nth(2).expect("must have 3 chars per line"));

        total_score += us.value() + us.vs(them).value();
    }
    
    total_score.to_string()
}

It’s not a bit weird to use nth to pull out the chars, but I was having some issues with String vs &str comparisons. Chars just work.

Run it on my input:

$ ./target/release/02-roshamboinator 1 data/02.txt

13446
took 752.791µs

Cool.

Part 2

Now treat input X,Y,Z as you needing to lose, draw, or win respectively. Determine the move that gives that outcome and calculate the score as before.

We have most of this, but we do need a new constructor to load the Outcome:

impl Outcome {
    fn new(c: char) -> Outcome {
        use Outcome::*;

        match c {
            'X' => Lose,
            'Y' => Draw,
            'Z' => Win,
            _ => panic!("unknown outcome: {:?}", c)
        }
    }
}

And now we can actually determine the plays:

fn part2(filename: &Path) -> String {
    use Outcome::*;
    use Play::*;

    let mut total_score = 0;

    for line in read_lines(filename) {
        let them = Play::new(line.chars().nth(0).expect("must have 1 char per line"));
        let goal = Outcome::new(line.chars().nth(2).expect("must have 3 chars per line"));

        let us = match goal {
            Lose => match them {
                Rock => Scissors,
                Scissors => Paper,
                Paper => Rock,
            },
            Draw => them,
            Win => match them {
                Rock => Paper,
                Scissors => Rock,
                Paper => Scissors,
            },
        };

        total_score += us.value() + goal.value();
    }

    total_score.to_string()
}

It’s a bit more in teh actual part, mostly because we need to have a nested match to determine what the us play is. But once we have that, the total_score calculation is the same.

$ ./target/release/02-roshamboinator 2 data/02.txt

13509
took 662.916µs

Double cool.