Day 2: Solution Explanation
Approach
Day 2's problem requires implementing a Rock Paper Scissors game with two different interpretations of a strategy guide. We need to:
- Parse the input into rounds of play
- Calculate scores for each round according to both interpretations
- Sum the scores to get the total
Strategy 1 vs Strategy 2
The key difference between the two strategies is the interpretation of the second column:
- Strategy 1: The second column (X, Y, Z) represents your move (Rock, Paper, Scissors)
- Strategy 2: The second column represents the desired outcome (Lose, Draw, Win)
Game Logic
To implement the game, we need to model:
- The possible moves (Rock, Paper, Scissors)
- The possible outcomes (Win, Loss, Draw)
- The scoring rules for moves and outcomes
- The winning relationships between moves
- How to derive a move given an opponent's move and a desired outcome
Implementation Details
The Move Enum
We define a Move
enum with values for Rock, Paper, and Scissors, each with its corresponding score value:
#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone,PartialEq)] enum Move { Rock=1, Paper, Scissors } }
The numeric values (1, 2, 3) are automatically assigned based on the enum declaration order.
Parsing Input
We implement the From<u8>
trait to convert characters from the input into Move
values:
#![allow(unused)] fn main() { impl From<u8> for Move { fn from(c: u8) -> Self { match c { b'A' | b'X' => Move::Rock, b'B' | b'Y' => Move::Paper, b'C' | b'Z' => Move::Scissors, _ => unreachable!() } } } }
Determining Outcomes
We implement a method to determine if one move wins against another:
#![allow(unused)] fn main() { fn is_winning(&self, other:&Self) -> bool { matches!( (other,self), (Move::Rock, Move::Paper) | (Move::Paper, Move::Scissors) | (Move::Scissors, Move::Rock) ) } }
And a method to determine the outcome of a round:
#![allow(unused)] fn main() { fn outcome(&self, other:&Self) -> Outcome { if self.is_winning(other) { Outcome::Win } else if other.is_winning(self) { Outcome::Loss } else { Outcome::Draw } } }
Strategy 2: Deriving Moves
For Strategy 2, we need to determine what move to make given an opponent's move and a desired outcome:
#![allow(unused)] fn main() { fn derive(&self, out:Outcome) -> Move { let iter = once(Move::Rock).chain(once(Move::Paper)).chain(once(Move::Scissors)).cycle(); iter.skip_while(|e| self != e).skip(out as usize).next().unwrap() } }
This is a clever solution that creates a circular iterator of moves and skips to the move that produces the desired outcome.
Scoring
We define an Outcome
enum and implement scoring for outcomes:
#![allow(unused)] fn main() { enum Outcome { Draw, Win, Loss } impl Outcome { fn score_value(&self) -> u64 { match self { Outcome::Loss => 0, Outcome::Draw => 3, Outcome::Win => 6 } } } }
Combining Everything
We create a Round
struct to represent a round of Rock Paper Scissors:
#![allow(unused)] fn main() { struct Round(Move,Move); impl Round { fn score(&self) -> u64 { let Round(other, me) = self; me.outcome(other).score_value() + *me as u64 } } }
Each round is scored by adding the outcome value to the value of the move chosen.
Processing the Input
Finally, we process the input file, calculating scores for both strategies:
fn main() { let (score1, score2) = std::fs::read_to_string("./src/bin/day2_input.txt") .unwrap() .lines() .map(|round| ( Round::new(round).score(), // Strategy 1 Round::derived(round).score() // Strategy 2 )) .reduce(|sum, round| { (sum.0 + round.0, sum.1 + round.1) }) .unwrap_or_else(|| panic!("Empty iterator ?")); println!("Strategy 1 : {:?}",score1); println!("Strategy 2 : {:?}",score2); }
We map each line to a tuple of scores for both strategies, then reduce the results to get the total scores.
Alternative Approaches
Pattern Matching
A simpler approach could use direct pattern matching for each input combination:
#![allow(unused)] fn main() { fn strategy_1(round:&str) -> u64 { match round { "A X" => 3+1, // Rock vs Rock = Draw (3) + Rock (1) "A Y" => 6+2, // Rock vs Paper = Win (6) + Paper (2) "A Z" => 0+3, // Rock vs Scissors = Loss (0) + Scissors (3) // ... other combinations _ => panic!("unknown input") } } }
While this approach is more direct, it's less flexible and doesn't model the game's logic as cleanly.
Optimization Considerations
- The current solution uses enums to represent both moves and outcomes, which makes the code clear and easy to understand.
- The
derive
method is particularly elegant, using Rust's iterator functionality to find the right move. - For very large inputs, we could consider using a lookup table for move derivation instead of the iterator approach.