Day 2 - Rock, Paper, Scissors

Solution code on GitHub

All 2022 Problems

Subscribe to Monday Morning Haskell!

Problem Overview

Full Description

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 Z

The 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:

  1. 6 points are given for a win, 3 for a draw, and 0 for a loss.
  2. 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 Z

I 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!

Video

Link to YouTube

Previous
Previous

Day 3 - Rucksacks and Badges

Next
Next

Day 1 - Intro Problem