Today’s Advent of Code problem, “Dirac Dice”, is the one I least like talking about. I did enjoy solving it. The twist from part 1 to part 2 is good. Simply put, my issue with it is that despite all my efforts, there’s so little code to share between both parts it all feels a bit artificial. So bear with me, it’s still a literate Haskell post with imports on top.
import Control.Arrow ((&&&))
import Control.Lens
import Control.Monad.Cont
import Control.Monad.Free
import Control.Monad.Free.TH
import Control.Monad.Trans.Accum
import Data.Function (fix)
import Data.Monoid (Sum(Sum))
import Data.MultiSet (MultiSet)
import qualified Data.MultiSet as MSet
The problem state is very simple: two players, each with a position and a score. One of those has the distinction of being active.
data State = State
_currentPlayer :: !Player
{ _nextPlayer :: !Player
,
}deriving (Eq,Ord)
data Player = Player
_plId :: !PlayerId
{ _pos :: !Int
, _score :: !Int
,
}deriving (Eq,Ord)
data PlayerId = P1 | P2 deriving (Eq,Ord)
'State
makeLenses ''Player makeLenses '
The game operation is rather simple. I’ll write it up in a free monad.
data GameF a = Roll (Int -> a) | End State deriving Functor
'GameF
makeFree '
step :: MonadFree GameF m => Int -> State -> m State
= do
step lim st <- roll
r1 <- roll
r2 <- roll
r3 let pl' = advance (r1 + r2 + r3) (st ^. currentPlayer)
= State { _currentPlayer = st ^. nextPlayer
st' = pl'
, _nextPlayer
}if pl' ^. score >= lim then end st' else pure st'
The step function takes a lim
argument to limit the game
to reaching a specific score. It then rolls the die three times,
advances the player, adds their resulting position to their score and
returns the state. Either as a simple monadic value when the game is
still live, or as a terminal free member.
The entire tricky part of the implementation is in this helper, that concentrates all the off-by-one errors you could dream of in a single place.
advance :: Int -> Player -> Player
=
advance n pl let pos' = 1 + (pl ^. pos - 1 + n) `mod` 10
in pl & pos .~ pos'
& score +~ pos'
Part 1 is the training game: it stops at a score of 1 000, the die is predictable and we are asked for a specific checksum involving the loser’s score.
I’ll implement the die throw at little cost using my very good friend
the Accum
monad.
type Training = Accum (Sum Int)
rollTraining :: Training Int
= do
rollTraining Sum n <- look
Sum 1)
add (pure (n `mod` 100 + 1)
This has the advantage of easily letting me read the roll count in the end.
Then I can write a wrapper to interpret the game and return the requested checksum.
runTraining :: State -> Int
= loserScore * rolls where
runTraining st0 Sum rolls) =
(loserScore,-> iterM (interpret end) (game st0))) pure)
runAccum (runContT (callCC (\end mempty
= fix $ \loop st -> step 1000 st >>= loop
game = \case
interpret end Roll next -> lift rollTraining >>= next
End st -> end (st ^. currentPlayer . score)
I’m not too satisfied with resorting to callCC
to
extract the result, but the types of both foldFree
and
iterM
didn’t appear to let me do otherwise. Free monad
experts come to me!
Anyway, this runs fine and dandy and earns a star.
Now to part 2. Here rolling the die splits the universe. “I know”, I think, “I’ll use a list monad!”
type Dirac = []
rollDirac :: Dirac Int
= [1..3] rollDirac
Well, Haskell makes it easy to split the world, but that doesn’t make it the right thing to do. The example in the problem statement is a good enough hint: if we’re going to count victories one by one in addition to all the consing, it won’t be done in time for next Christmas.
The seasoned competitive programmers will have noticed that the core space is quite small, and we’re limiting Dirac games to only a few scores, so it’s a rather direct application of dynamic programming.
But DP isn’t strictly necessary either. We can use the same kind of trick as on day 14: the state cardinality is smaller than 2 × 102 × 212 = 88 200. By flattening it after each step, we only have that times the 33 Dirac splits to explore per turn. And the maximum turn count is very trivially bounded (and small).
That path does introduce the problem of counting victories. It would
be a bit unwieldy to thread a Writer monad through: the tallies do have
to be multiplied by the occurrence count, which isn’t trivially
accessible at free monad interpreter level. I chose to expand the state
with a Nothing
value, that will represent whatever end
state we want to count. So the game step function just needs to ignore
(but keep) them:
type State' = Maybe State
diracStep :: MultiSet State' -> MultiSet State'
= MSet.unionsMap $
diracStep . maybe [Nothing] (iterM interpretDirac . fmap Just . step 21) MSet.fromList
The interpreter isn’t much more complicated than the previous one.
The End
case is where we convert a victory to a tally.
interpretDirac :: GameF (Dirac State') -> Dirac State'
Roll next) = rollDirac >>= next
interpretDirac (End st) = Nothing <$ guard (st ^. nextPlayer . plId == P1) interpretDirac (
The Dirac runner is actually simpler. Thank the sane checksum.
runDirac :: State -> Int
= MSet.size . stabilize diracStep . MSet.singleton . Just runDirac
Here’s the rest of the code for completeness.1
stabilize :: Eq a => (a -> a) -> a -> a
= go where
stabilize f | x' == x = x
go x | otherwise = go x'
where x' = f x
parse :: String -> State
map (read . last . words) . lines -> [a,b]) =
parse (State { _currentPlayer = Player { _plId = P1, _pos = a, _score = 0 }
= Player { _plId = P2, _pos = b, _score = 0 }
, _nextPlayer
}
main :: IO ()
= interact $ show . (runTraining &&& runDirac) . parse main
This concludes today’s solution. I hope you enjoyed it, because in all honesty I only wrote it for completeness. See you tomorrow!
You don’t really think I wrote a parser during Advent for just two digits?↩︎