Advent of Code 2022 day 2

Day 2 and we're already into some deeper software engineering principles! To wit,

  1. Parse, don't validate
  2. Grow the language to the domain, not lower the domain to the language

Representation

The domain of this problem is the game Rock-Paper-Scissors, but represented as pairs of letters. But because we're talking about the game, I'll represent things in terms of the game and concepts that make sense in that domain.

That gives me a bunch of new data structures to represent games and rounds.

data Shape = Rock | Paper | Scissors deriving (Show, Eq, Ord, Enum)
data Result = Loss | Draw | Win deriving (Show, Eq, Ord, Enum)
data Round = Round Shape Shape deriving (Eq, Show)

This means I can't represent something that isn't a game, even though there are many text strings that aren't valid games. This is the advantage of parsing, not validating.

Then I can encode the core rule of the game as a function:

player2Result :: Round -> Result
player2Result (Round Rock Paper) = Win
player2Result (Round Paper Scissors) = Win
player2Result (Round Scissors Rock) = Win
player2Result (Round x y) | x == y = Draw
player2Result _ = Loss

The same with the instructions of how to score the round:

scoreRound :: Round -> Int
scoreRound r@(Round _ y) = scoreShape y + scoreResult (player2Result r)

scoreShape :: Shape -> Int
scoreShape s = 1 + fromEnum s

scoreResult :: Result -> Int
scoreResult r = 3 * fromEnum r

The final part is to convert the input text into this representation. Enter the parser:

match1P = roundP `sepBy` endOfLine
roundP = Round <$> p1ShapeP <*> (" " *> p2ShapeP)

p1ShapeP = aP <|> bP <|> cP
aP = Rock <$ "A"
bP = Paper <$ "B"
cP = Scissors <$ "C"

p2ShapeP = xP <|> yP <|> zP
xP = Rock <$ "X"
yP = Paper <$ "Y"
zP = Scissors <$ "Z"

If the parser succeeds, I can be confident that I only have valid rounds to work with.

Part 1

Getting the input right makes the actual solution embarrassingly simple:

main :: IO ()
main = 
  do  dataFileName <- getDataFileName
      text <- TIO.readFile dataFileName
      let match1 = successfulParse1 text
      print $ part1 match1

part1 :: [Round] -> Int
part1 = sum . fmap scoreRound

Score each round and add up the results.

Part 2

With part 2, the input now means something different, so I need to represent and parse it differently. As each input line is now a Shape and a Result, I combine them in a ShapeResult. I also need a new parser to process this new representation.

data ShapeResult = ShapeResult Shape Result deriving (Eq, Show)

match2P = shapeResultP `sepBy` endOfLine
shapeResultP = ShapeResult <$> p1ShapeP <*> (" " *> resultP)

resultP = xrP <|> yrP <|> zrP
xrP = Loss <$ "X"
yrP = Draw <$ "Y"
zrP = Win <$ "Z"

Now to find the defined move for player 2.

I could explicitly write the move required for each shape/result combination, but that would mean another place where the rules of the game are encoded. On the other hand, I should use those rules to express the idea of part 2: "figure out what shape to choose so the round ends as indicated."

I'll try all the shapes until I get the Round I need.

roundFromResult :: ShapeResult -> Round
roundFromResult (ShapeResult shape result) = Round shape p2s
  where p2s = head [ p2Shape 
                   | p2Shape <- [Rock .. Scissors]
                   , player2Result (Round shape p2Shape) == result
                   ]

This is the idea of growing the language to meet the problem, which I first met explicitly in the book On Lisp. (But don't pay too much attention to Graham's more recent utterings.)

And that gives another very simple solution for part 2:

part2 = sum . fmap (scoreRound . roundFromResult)

Code

You can get the code from my locally-hosted Git repo, or from Gitlab.