Advent of Code 2022: Day 2
Published on
Implementation of play
Here it is for reference:
data Shape = Rock | Paper | Scissors
deriving (Show)
data Outcome = Win | Lose | Tie
deriving (Show)
play :: Shape -> Shape -> Outcome
=
play
\casesRock Rock -> Tie
Rock Paper -> Lose
Rock Scissors -> Win
Paper Rock -> Win
Paper Paper -> Tie
Paper Scissors -> Lose
Scissors Rock -> Lose
Scissors Paper -> Win
Scissors Scissors -> Tie
This definition is wonderfully obvious, but contains some redundancy. Could it be expressed more concisely? A couple of approaches spring to mind, so let’s try both!
Mechanical approach
A couple of redundancies are obvious:
- Three of the cases are flipped versions of other cases.
- A shape vs itself is always
Tie
.
What does the code look like if we try to encode those rules directly?
play :: Shape -> Shape -> Outcome
=
play case
\Rock Scissors -> Win
Scissors Paper -> Win
Paper Rock -> Win
a b| a == b -> Tie
| otherwise -> case play b a of
Win -> Lose
Lose -> Win
Tie -> Tie
This is not great. It is the same number of lines of code, but now contains a potentially dangerous recursive call, and is far less obvious. Notably, the rock-scissors-paper cycle is still not directly modelled. Let’s strive for satisfaction instead, and see where we get to.
Satisfaction-oriented design
I want the definition of play
to read like my internal model of the game:
Scissors-beats-paper-beats-rock-beats-scissors, and both players choosing the same shape is a tie.
The cyclical description evokes memories of modular arithmetic… can we use that, somehow? Let us project Shape
into the integers modulo 3:
Shape |
integer (mod 3) |
---|---|
Rock |
0 |
Paper |
1 |
Scissors |
2 |
Now, for the projection of any two shapes s and t:
- if s - t = 1 (mod 3) then s wins.
- if s - t = 0 (mod 3) then it is a tie.
- if s - t = -1 (mod 3) then s loses.
Aha! With this written out, it becomes obvious that we can also project Outcome
into the integers modulo 3:
Outcome |
integer (mod 3) |
---|---|
Win |
1 |
Tie |
0 |
Lose |
2 |
Now for shape inputs can be directly computed by outcome = challenge - response (mod 3).
What does this look like in Haskell?
data Shape = Rock | Paper | Scissors
deriving (Enum, Show)
data Outcome = Tie | Win | Lose
deriving (Enum, Show)
play :: Shape -> Shape -> Outcome
=
play challenge response toEnum $
fromEnum challenge - fromEnum response) `mod` 3 (
Much more pleasant!