Day 2 and we're already into some deeper software engineering principles! To wit,
- Parse, don't validate
- 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.