Day 2 - Rock, Paper, Scissors
Subscribe to Monday Morning Haskell!
Problem Overview
In today's problem, we're playing Rock-Paper-Scissors (RPS). Given a series of rounds of RPS play, we're supposed to evaluate the total "score" we get depending on what we've played. Inputs are line-by-line with two characters on each line:
A Y
B X
C ZThe first character always tells us what the opponent plays in that match - A = Rock, B = Paper, C = Scissors.
In the first part of the problem, the second character simply tells us what figure we should play (X = Rock, Y = Paper, Z = Scissors). In the second part of the problem, this character actually tells us the result we are trying to achieve - X = Loss, Y = Draw, Z = Win.
In both cases, our final solution is to calculate our total score over all the rounds, tabulated as follows:
- 6 points are given for a win, 3 for a draw, and 0 for a loss.
- Then we get a certain number of points for the figure we played - 1 for Rock, 2 for Paper, and 3 for Scissors.
So for part 1, the simple 3-line inputs gives the following results:
Round 1: Play paper (2 points) against rock. Win (6 points)
Round 2: Play rock (1 point) against paper. Lose (0 points)
Round 3: Play scissors (3 points) against scissors. Draw (3 points)Adding up all the points gives a total of 15.
For part 2, we get the following sequence by trying to match the results:
Round 1: Draw (3 points) against rock by playing rock (1 point)
Round 2: Lose (0 points) against paper by playing rock (1 point)
Round 3: Win (6 points) against scissors by playing rock (1 point)This gives a total of 12 points.
Solution Approach and Insights
This problem follows the simple and common "fold line-by-line" solution approach. I have some pre-planned boilerplate in my solution template for this! The folding action is not hard here - we just have to evaluate the result of the match and score it appropriately.
Parsing the Input
So remember, our sample input looks like this:
A Y
B X
C ZI started with an RPS type for the three possible figures we can playing:
data RPS = Rock | Paper | Scissors
deriving (Show, Eq)
So we parse one of the figures using alternatives:
parseRPS :: ParsecT Void Text m RPS
parseRPS = parseRock <|> parsePaper <|> parseScissors
where
parseRock = (char 'A' <|> char 'X') >> return Rock
parsePaper = (char 'B' <|> char 'Y') >> return Paper
parseScissors = (char 'C' <|> char 'Z') >> return Scissors
So we can parse a single line by taking two of these figures with a space between. In my template, I have a generic LineType alias to use both while parsing and folding over lines. In our case, each line is two of these RPS values.
type LineType = (RPS, RPS)
parseLine :: (MonadLogger m) => ParsecT Void Text m LineType
parseLine = do
first <- parseRPS
char ' '
second <- parseRPS
return (first, second)
Then our final input uses the very common sepEndBy1 ... eol pattern. We use another alias for InputType here.
type InputType = [LineType]
parseInput :: (MonadLogger m) => ParsecT Void Text m InputType
parseInput = sepEndBy1 parseLine eol
As a final wrinkle, we'll make a separate type for part 2, because the letters represent something semantically different. We won't change the parser. We'll just write a translation function to use later.
data ExpectedResult = Loss | Draw | Win
deriving (Show, Eq)
rpsToResult :: RPS -> ExpectedResult
rpsToResult Rock = Loss
rpsToResult Paper = Draw
rpsToResult Scissors = Win
translate :: (RPS, RPS) -> (RPS, ExpectedResult)
translate (first, second) = (first, rpsToResult second)
Getting the Easy Solution
As I mentioned above, this problem fits a common pattern: fold our inputs line-by-line and accumulate a solution. I'll use some more generic types and values to outline this approach.
solveFold :: (MonadLogger m) => [LineType] -> m EasySolutionType
solveFold = foldM foldLine initialFoldV
type FoldType = Int
initialFoldV :: FoldType
initialFoldV = 0
foldLine :: (MonadLogger m) => FoldType -> LineType -> m FoldType
foldLine = ...
We're tracking a score, so the FoldType that we're modifying with each step is just an Int, and we give the initial value of 0. Solving the problem is as simple as applying foldM with a proper folding function and the initial value. The only challenge is filling in foldLine. For this, we need two scoring functions, one for the figure we choose (scoreRps) and another for the outcome of the match, which just requires looking at each case:
scoreRps :: RPS -> Int
scoreRps Rock = 1
scoreRps Paper = 2
scoreRps Scissors = 3
evalMatch :: (RPS, RPS) -> Int
evalMatch (Rock, Rock) = 3
evalMatch (Rock, Paper) = 6
evalMatch (Rock, Scissors) = 0
evalMatch (Paper, Rock) = 0
evalMatch (Paper, Paper) = 3
evalMatch (Paper, Scissors) = 6
evalMatch (Scissors, Rock) = 6
evalMatch (Scissors, Paper) = 0
evalMatch (Scissors, Scissors) = 3
And our fold simply applies both these to the input and adds to the previous result!
foldLine :: (MonadLogger m) => FoldType -> LineType -> m FoldType
foldLine prevScore (first, second) = return $ prevScore + (evalMatch (first, second) + scoreRps second)
Getting the "Hard" Solution
For part 2, all we really need is to translate each input pair so it has an ExpectedResult, and then use a different evaluation function. Here's how we evaluate each pair:
evalMatchHard :: (RPS, ExpectedResult) -> Int
evalMatchHard (Rock, Win) = 8 -- Play Paper (2 + 6)
evalMatchHard (Rock, Draw) = 4 -- Play Rock (1 + 3)
evalMatchHard (Rock, Loss) = 3 -- Play Scissors (3 + 0)
evalMatchHard (Paper, Win) = 9 -- Play Scissors (3 + 6)
evalMatchHard (Paper, Draw) = 5 -- Play Paper (2 + 3)
evalMatchHard (Paper, Loss) = 1 -- Play Rock (1 + 0)
evalMatchHard (Scissors, Win) = 7 -- Play Rock (1 + 6)
evalMatchHard (Scissors, Draw) = 6 -- Play Scissors (3 + 3)
evalMatchHard (Scissors, Loss) = 2 -- Play Paper (2 + 0)
And we fold over the inputs like so:
type HardSolutionType = EasySolutionType -- < Int
processInputHard :: (MonadLogger m) => InputType -> m HardSolutionType
processInputHard inputs = foldM foldExpResult initialFoldV (map translate inputs)
foldExpResult :: (MonadLogger m) => Int -> (RPS, ExpectedResult) -> m Int
foldExpResult prevScore (oppPlay, result) = return $ prevScore + evalMatchHard (oppPlay, result)
Answering the Question
No further work is needed beyond passing our inputs to our functions and taking the result:
solveEasy :: FilePath -> IO (Maybe Int)
solveEasy fp = runStdoutLoggingT $ do
input <- parseFile parseInput fp
Just <$> processInputEasy input
solveHard :: FilePath -> IO (Maybe Int)
solveHard fp = runStdoutLoggingT $ do
input <- parseFile parseInput fp
Just <$> processInputHard input
-- Note: These functions are the same, since nothing extra was required in processing!
processInputEasy = solveFold
Now we're done!