Advent of Code 2022, Day 2 -- Rock Paper Scissors
We made it to day two of Advent of Code!
Today's puzzle is harder than yesterday's, and it's only going to get more difficult from here.
Full code for Day 2 can be found on Github.
The Day 2 problem can be found at adventofcode.com/2022/day/2
Jump to:
Here's the full code that I ended up with, pre-refactor:
use std::fs;
use std::collections::HashMap;
use std::str::SplitTerminator;
fn main() {
let contents = fs::read_to_string("input.txt")
.unwrap_or("".to_string());
let arr = contents.split_terminator("\n");
let values = HashMap::from([
('X', 1), // Rock is worth 1
('Y', 2), // Paper is worth 2
('Z', 3), // Scissors is worth 3
]);
strategy_1(arr.clone(), values.clone());
strategy_2(arr, values);
}
fn strategy_1(arr: SplitTerminator<&str>, values: HashMap<char, i32>) {
let scores = HashMap::from([
('A', HashMap::from([ // Rock
('X', 3), // Rock draws
('Y', 6), // Paper wins
('Z', 0), // Scissors loses
]),
),
('B', HashMap::from([ // Paper
('X', 0), // Rock loses
('Y', 3), // Paper draws
('Z', 6), // Scissors wins
]),
),
('C', HashMap::from([ // Scissors
('X', 6), // Rock wins
('Y', 0), // Paper loses
('Z', 3), // Scissors draws
]),
),
]);
let mut total = 0;
for round in arr {
let opponent = round.chars().nth(0).unwrap();
let player = round.chars().nth(2).unwrap();
let score = scores[&opponent][&player];
total += score + values[&player];
}
println!("{:?}", total);
}
fn strategy_2(arr: SplitTerminator<&str>, values: HashMap<char, i32>) {
let selections = HashMap::from([
('A', HashMap::from([ // Rock
('X', 'Z'), // To lose, select Scissors
('Y', 'X'), // To draw, select Rock
('Z', 'Y'), // To win, select Paper
]),
),
('B', HashMap::from([ // Paper
('X', 'X'), // To lose, select Rock
('Y', 'Y'), // To draw, select Paper
('Z', 'Z'), // To win, select Scissors
]),
),
('C', HashMap::from([ // Scissors
('X', 'Y'), // To lose, select Paper
('Y', 'Z'), // To draw, select Scissors
('Z', 'X'), // To win, select Rock
]),
),
]);
let scores = HashMap::from([
('X', 0), // Loss is worth 0
('Y', 3), // Draw is worth 3
('Z', 6), // Win is worth 6
]);
let mut total = 0;
for round in arr {
let opponent = round.chars().nth(0).unwrap();
let player = round.chars().nth(2).unwrap();
let selection = selections[&opponent][&player];
total += scores[&player] + values[&selection];
}
println!("{:?}", total);
}
Part 1
This challenge involves a lot of string parsing. First, I split the contents of the input into a vector with .split_terminator()
, which I just learned. I was getting an empty string at the end of the vector with the regular .split()
method, and .split_terminator()
removes that empty string.
let arr = contents.split_terminator("\n");
Then, I parsed each line of the vector by splitting it into characters and taking the first character as the opponent's choice, and the third character as the player's choice.
for round in arr {
let opponent = round.chars().nth(0).unwrap();
let player = round.chars().nth(2).unwrap();
}
The scores are calculated with HashMaps within a HashMap. For each opponent choice, there's a HashMap of player choices and point values. For example, if the opponent chooses rock and the player chooses paper, the player wins 6 points.
There's also a HashMap of point values for the various choices as well. That HashMap lives in the main()
function since it's used in both parts of this challenge.
let score = scores[&opponent][&player];
total += score + values[&player];
The score is selected from the 2-dimensional HashMap, then added to the value of the player's choice, which is then added to the running total.
Part 2
Part 2 is very similar to part 1, except the HashMap of opponent's choices returns what the player should choose, as opposed to the score. Then, that result is sent to get the point value of the player's choice. Since the game's outcome is already provided, a simple HashMap of scores provides the score.
It's really only one more step. In part 1, you have to consult a HashMap of scores and a HashMap of values. In part 2, you consult a HashMap of choices, which then is used with the HashMap of values, and also a HashMap of scores.
Refactor
Looking on the Advent of Code subreddit, I saw a few interesting solutions that used enums to substitute the characters for more readable options. Since I haven't used enums very much, I decided to try refactoring with them.
I decided to create an enum called Choice, which holds the three options, Rock, Paper, and Scissors. Then, I created a second enum called Strategy, with Lose, Draw, and Win. Both enums have implementations that will match the characters to the various options.
Ultimately, the enums don't affect the code logic at all, and actually makes the code much more verbose. However, the enums do make it easier to reason with the logic in the code. Instead of comparing vague characters in a string, you can actually see how the game logic works with the enums.
Here's the full refactored code:
use std::fs;
use std::collections::HashMap;
use std::str::SplitTerminator;
#[derive(Hash, Eq, PartialEq, Clone)]
enum Choice {
Rock,
Paper,
Scissors,
}
impl Choice {
fn get_opponent_choice(letter: char) -> Result<Self, String> {
match letter {
'A' => Result::Ok(Self::Rock),
'B' => Result::Ok(Self::Paper),
'C' => Result::Ok(Self::Scissors),
_ => Result::Err(String::from("Invalid opponent choice")),
}
}
fn get_player_choice(letter: char) -> Result<Self, String> {
match letter {
'X' => Result::Ok(Self::Rock),
'Y' => Result::Ok(Self::Paper),
'Z' => Result::Ok(Self::Scissors),
_ => Result::Err(String::from("Invalid player choice")),
}
}
}
#[derive(Hash, Eq, PartialEq)]
enum Strategy {
Lose,
Draw,
Win,
}
impl Strategy {
fn get_strategy(letter: char) -> Result<Self, String> {
match letter {
'X' => Result::Ok(Self::Lose),
'Y' => Result::Ok(Self::Draw),
'Z' => Result::Ok(Self::Win),
_ => Result::Err(String::from("Invalid strategy")),
}
}
}
fn main() {
let contents = fs::read_to_string("input.txt").unwrap_or("".to_string());
let arr = contents.split_terminator("\n");
let values = HashMap::from([
(Choice::Rock, 1),
(Choice::Paper, 2),
(Choice::Scissors, 3),
]);
strategy_1(arr.clone(), values.clone());
strategy_2(arr, values);
}
fn strategy_1(arr: SplitTerminator<&str>, values: HashMap<Choice, i32>) {
let scores = HashMap::from([
(Choice::Rock, HashMap::from([
(Choice::Rock, 3), // Rock draws
(Choice::Paper, 6), // Paper wins
(Choice::Scissors, 0), // Scissors loses
]),
),
(Choice::Paper, HashMap::from([
(Choice::Rock, 0), // Rock loses
(Choice::Paper, 3), // Paper draws
(Choice::Scissors, 6), // Scissors wins
]),
),
(Choice::Scissors, HashMap::from([
(Choice::Rock, 6), // Rock wins
(Choice::Paper, 0), // Paper loses
(Choice::Scissors, 3), // Scissors draws
]),
),
]);
let mut total = 0;
for round in arr {
let opponent = Choice::get_opponent_choice(round.chars().nth(0).unwrap()).unwrap();
let player = Choice::get_player_choice(round.chars().nth(2).unwrap()).unwrap();
let score = scores[&opponent][&player];
total += score + values[&player];
}
println!("{:?}", total);
}
fn strategy_2(arr: SplitTerminator<&str>, values: HashMap<Choice, i32>) {
let selections = HashMap::from([
(Choice::Rock, HashMap::from([
(Strategy::Lose, Choice::Scissors), // To lose, select Scissors
(Strategy::Draw, Choice::Rock), // To draw, select Rock
(Strategy::Win, Choice::Paper), // To win, select Paper
]),
),
(Choice::Paper, HashMap::from([
(Strategy::Lose, Choice::Rock), // To lose, select Rock
(Strategy::Draw, Choice::Paper), // To draw, select Paper
(Strategy::Win, Choice::Scissors), // To win, select Scissors
]),
),
(Choice::Scissors, HashMap::from([
(Strategy::Lose, Choice::Paper), // To lose, select Paper
(Strategy::Draw, Choice::Scissors), // To draw, select Scissors
(Strategy::Win, Choice::Rock), // To win, select Rock
]),
),
]);
let scores = HashMap::from([
(Strategy::Lose, 0),
(Strategy::Draw, 3),
(Strategy::Win, 6),
]);
let mut total = 0;
for round in arr {
let opponent = Choice::get_opponent_choice(round.chars().nth(0).unwrap()).unwrap();
let player = Strategy::get_strategy(round.chars().nth(2).unwrap()).unwrap();
let selection = &selections[&opponent][&player];
total += scores[&player] + values[&selection];
}
println!("{:?}", total);
}
As you see, there are many more lines of code, but I think this ultimately helps the code be more self-documenting.
What I learned
- .split_terminator() - The same as
.split()
, but ignores the trailing substring if it's empty. - Enums - A basic Rust data type.
- Impl - Implementation methods added to structs or enums.