Day 2: Rock Paper Scissors

Today I’m competing in a rock, paper, scissors with a lot of elves, but I do have an “encrypted” strategy guide that knows what the other elves will throw, and has calculated the best strategy without looking suspicious. The guide is in the format

A X
B Y
C Z
...

Where the first column is what my opponent is expected to throw (A = Rock, B = Paper, C = Scissors.) The second column is the advice the book is giving. The tournament also has a scoring system based on:

Part 1 - Calculate outcome from throws

For part 1 this the strategy advice is assumed to be (X = Rock, Y = Paper, Z = Scissors.)

First I’ll define some types:

enum Move {
    Rock,
    Paper,
    Scissors,
}

type Round = (Move, Move);
type Tournament = Vec<Round>;

The parsing then needs to turn each line of the input into a round with two moves.

fn parse_strategy(strategy: &String) -> Tournament {
    strategy.lines()
            .map(parse_line)
            .collect()
}

fn parse_line(line: &str) -> Round {
    let (part_1, part_2) = line.split_once(' ').unwrap();
    (
        parse_move(part_1).unwrap(),
        parse_move(part_2).unwrap()
    )
}

fn parse_move(chr: &str) -> Option<Move> {
    match chr {
        "A" | "X" => Some(Rock),
        "B" | "Y" => Some(Paper),
        "C" | "Z" => Some(Scissors),
        _ => None
    }
}

There are some examples in the puzzle to use as test cases:

#[cfg(test)]
mod tests {
    use crate::day_2::{parse_strategy, Tournament};
    use crate::day_2::Move::{Paper, Rock, Scissors};

    fn sample_tournament() -> Tournament {
        vec![
            (Rock, Paper),
            (Paper, Rock),
            (Scissors, Scissors),
        ]
    }

    #[test]
    fn can_parse() {
        let example_guide = "A Y
B X
C Z".to_string();

        assert_eq!(
            parse_strategy(&example_guide),
            sample_tournament()
        );
    }
}

The puzzle solution is what I would score if I followed the strategy, so I need to implement the scoring system too.

fn score_tournament(tournament: &Tournament) -> u32 {
    tournament.into_iter().map(score_round).sum()
}

fn score_round(round: &Round) -> u32 {
    score_result(round) + score_move(round)
}

fn score_result(round: &Round) -> u32 {
    match round {
        (Rock, Paper) | (Paper, Scissors) | (Scissors, Rock) => 6,
        (their_move, my_move) if their_move == my_move => 3,
        (_, _) => 0
    }
}

fn score_move((_, my_move): &Round) -> u32 {
    match my_move {
        Rock => 1,
        Paper => 2,
        Scissors => 3
    }
}

And add some tests from the puzzle outcomes

#[test]
fn can_score_round() {
    assert_eq!(score_round(&(Rock, Paper)), 8);
    assert_eq!(score_round(&(Paper, Rock)), 1);
    assert_eq!(score_round(&(Scissors, Scissors)), 6);
}

#[test]
fn can_score_tournament() {
    assert_eq!(
        score_tournament(&sample_moves_tournament()),
        15
    );
}

With all that in place I can now run this with the puzzle data

pub fn run() {
    let contents = 
        fs::read_to_string("res/day-2-input").expect("Failed to read file");
    let tournament = parse_strategy(&contents);

    println!(
        "Following the guide, my score would be: {}",
        score_tournament(&tournament)
    );
}

/// Following the guide, my score would be: 13809

Part 2 - Calculate move from outcome

I now find out I assumed wrong, and the second column should have been the desired outcome (X = Loss, Y = Draw, Z = Win.)

Rather than duplicate work, I can pass the strategy for building a round from an input line into the parser. So I’ll first refactor to rename parse_line to parse_move_line, and the existing parse_strategy to accept the line parser as an argument.

fn parse_strategy(strategy: &String, syntax: fn(&str) -> Round) -> Tournament {
    strategy.lines()
            .map(syntax)
            .collect()
}
// ...
#[test]
fn can_parse() {
    let example_guide = "A Y
B X
C Z".to_string();

    assert_eq!(
        parse_strategy(&example_guide, parse_moves_line),
        sample_moves_tournament() // Also rename this function
    );
}
// ...
pub fn run() {
    let contents = 
        fs::read_to_string("res/day-2-input").expect("Failed to read file");
    let part_1_tournament = parse_strategy(&contents, parse_moves_line);

    println!(
        "Following the guide assuming moves, my score would be: {}",
        score_tournament(&part_1_tournament)
    );
}

I’ll add a type for the outcome and a parsing matcher in the style of parse_move

enum Outcome {
    Win,
    Loss,
    Draw,
}

fn parse_outcome(chr: &str) -> Option<Outcome> {
    match chr {
        "X" => Some(Loss),
        "Y" => Some(Draw),
        "Z" => Some(Win),
        _ => None
    }
}

To work with the existing scoring system, parsing a line by outcome should still return a round in the form (Move, Move). For this I’ll need to be able to map an opponents move and desired outcome to a Move.

fn resolve_outcome(their_move: Move, outcome: Outcome) -> Round {
    let my_move = match outcome {
        Loss => loss_for(their_move),
        Draw => draw_for(their_move),
        Win => win_for(their_move)
    };

    (their_move, my_move)
}

fn win_for(mv: Move) -> Move {
    match mv {
        Rock => Paper,
        Paper => Scissors,
        Scissors => Rock,
    }
}

fn draw_for(mv: Move) -> Move {
    mv
}

fn loss_for(mv: Move) -> Move {
    match mv {
        Rock => Scissors,
        Paper => Rock,
        Scissors => Paper,
    }
}

The parsing strategy for part two splits the line and delegates to the functions added above. Once that is written the tests and puzzle runner can be updated.

fn parse_outcome_line(line: &str) -> Round {
    let (part_1, part_2) = line.split_once(' ').unwrap();
    resolve_outcome(
        parse_move(part_1).unwrap(),
        parse_outcome(part_2).unwrap(),
    )
}
// ...
fn sample_outcome_tournament() -> Tournament {
    vec![
        (Rock, Rock),
        (Paper, Rock),
        (Scissors, Rock),
    ]
}

#[test]
fn can_parse() {
    let example_guide = "A Y
B X
C Z".to_string();

    assert_eq!(
        parse_strategy(&example_guide, parse_moves_line),
        sample_moves_tournament()
    );

    assert_eq!(
        parse_strategy(&example_guide, parse_outcome_line),
        sample_outcome_tournament()
    )
}
// ...
pub fn run() {
    let contents = 
        fs::read_to_string("res/day-2-input").expect("Failed to read file");
    let part_1_tournament = parse_strategy(&contents, parse_moves_line);

    println!(
        "Following the guide assuming moves, my score would be: {}",
        score_tournament(&part_1_tournament)
    );

    let part_2_tournament = parse_strategy(&contents, parse_outcome_line);

    println!(
        "Following the guide assuming outcomes, my score would be: {}",
        score_tournament(&part_2_tournament)
    );
}
// Following the guide assuming moves, your score would be: 13809
// Following the guide assuming outcomes, your score would be: 12316

Final refactor

The code is already performing well, but I did notice that the win_for, draw_for, and loss_for functions could be reused to make the match score_result a bit clearer in its intent.

fn score_result(round: &Round) -> u32 {
    match round {
        &(their_move, my_move) if win_for(their_move) == my_move => 6,
        &(their_move, my_move) if draw_for(their_move) == my_move => 3,
        (_, _) => 0
    }
}