Day 11 was probably the most complex challenge so far, and saw the return on the RWS (Reader-Writer-State) monad to keep track of all the monkeys.
Data representation
This needed quite a few data structures to keep track of all the different parts.
The first decision was to separate the data into parts that would fit the RWS approach. The description of the monkeys' decision-making parameters formed the Reader. The log of how many items were processed was the Writer. The detail of which monkeys held which items was the State. The Reader part was mainly a Map
from monkey ID to its parameters and the State was a Map
from monkey ID to the items it held.
Within that, I expressed the "operation" as data, rather than a function that transformed the old worry level to the new one. Finally, the monkey description contained the function that was applied to the worry scores after each update, to keep the scores in a sensible range. For part 1, that limiting function was 'div' 3
; for part 2 it was 'mod' k
where k was the product of all the "divisible by" values.
data MonkeyCode = MonkeyCode
{ _operation :: Expression
, _test :: Int
, _trueTarget :: Int
, _falseTarget :: Int
}
deriving (Show, Eq)
data Expression = Expression Operator Operand deriving (Show, Eq)
data Operator = Plus | Times deriving (Show, Eq)
data Operand = Literal Int | Old deriving (Show, Eq)
type MonkeyCodes = M.IntMap MonkeyCode
data MonkeyDescription = MonkeyDescription { _limit :: Int -> Int
, _codes :: MonkeyCodes
}
makeLenses ''MonkeyCode
The State and Writer parts were simpler. The MonkeyLog
held how many items were handled by each monkey in each step of the simulaton.
type MonkeyHolds = M.IntMap [Int]
data MonkeyLog = MonkeyLog Int Int -- monkey ID, number of items handled this round
deriving (Show, Eq)
type MonkeyHandler = RWS MonkeyDescription [MonkeyLog] MonkeyHolds
Parsing the input took some space, as the data format was verbose.
monkeysP = makeMonkeyMaps <$> (monkeyP `sepBy` (endOfLine <* endOfLine))
where makeMonkeyMaps monkeys =
( M.fromList $ map fst monkeys
, M.fromList $ map snd monkeys
)
monkeyP = mkMonkeyPair <$> mIdP <*> startingP <*> operatorP
<*> testP <*> trueTargetP <*> falseTargetP
where mkMonkeyPair mId holding _operation _test _trueTarget _falseTarget =
((mId, MonkeyCode{..}), (mId, holding))
mIdP = ("Monkey " *> decimal) <* ":" <* endOfLine
startingP = (" Starting items: " *> (decimal `sepBy` ", ")) <* endOfLine
operatorP = (" Operation: new = old " *> expressionP) <* endOfLine
testP = (" Test: divisible by " *> decimal) <* endOfLine
trueTargetP = (" If true: throw to monkey " *> decimal) <* endOfLine
falseTargetP = (" If false: throw to monkey " *> decimal)
expressionP = Expression <$> (opP <* " ") <*> operandP
opP = (Plus <$ "+") <|> (Times <$ "*")
operandP = (Literal <$> decimal) <|> (Old <$ "old")
One monkey's decision
throwItem
handles one monkey throwing one item. It updates the worry value, tests it against that monkey's criterion, determines which monkey should receive the item, and updates that monkey's list with the new item.
throwItem :: Int -> Int -> MonkeyHandler ()
throwItem mId currentWorry =
do monkey <- asks ((! mId) . _codes)
threshold <- asks _limit
let newWorry = updateWorry currentWorry (monkey ^. operation) threshold
let testResult = worryTest (monkey ^. test) newWorry
let recipient = if testResult
then (monkey ^. trueTarget)
else (monkey ^. falseTarget)
modify (receivesItem recipient newWorry)
This all uses a bunch of helper functions. updateWorry
calculates the new worry level, using the threshold
function after the monkey's defined operation.
updateWorry :: Int -> Expression -> (Int -> Int) -> Int
updateWorry current (Expression operator operand) threshold
| operator == Plus = threshold (current + n)
| operator == Times = threshold (current * n)
where n = evalOperand operand
evalOperand (Literal k) = k
evalOperand Old = current
worryTest :: Int -> Int -> Bool
worryTest divisor worry = worry `mod` divisor == 0
receivesItem :: Int -> Int -> MonkeyHolds -> MonkeyHolds
receivesItem mId worry items = M.adjust (++ [worry]) mId items
Putting it together
Given one monkey throwing one item, having all monkeys throw all items to make a round is a couple of mapM
calls.
throwRound :: MonkeyHandler ()
throwRound =
do mIds <- gets M.keys
mapM_ throwItems mIds
throwItems :: Int -> MonkeyHandler ()
throwItems mId =
do items <- gets (! mId)
mapM_ (throwItem mId) items
modify (M.insert mId [])
tell [MonkeyLog mId (length items)]
throwItems
also writes the log of how many items are thrown by this monkey this round.
There are a couple of functions to extract the desired value from the log.
sumLogs :: [MonkeyLog] -> M.IntMap Int
sumLogs logs = foldl' addCount M.empty logs
where addCount m (MonkeyLog mId n)
| mId `M.member` m = M.adjust (+ n) mId m
| otherwise = M.insert mId n m
monkeyBusinessLevel :: [MonkeyLog] -> Int
monkeyBusinessLevel logs = prolifics!!0 * prolifics!!1
where prolifics = reverse $ sort $ M.elems $ sumLogs logs
The final part is to assemble the monkey description (together with the limiting function) and simulate the required number of rounds.
part1, part2 :: MonkeyCodes -> MonkeyHolds -> Int
part1 monkeyCode monkeyHold = monkeyBusinessLevel logs
where monkeyDesc = MonkeyDescription { _limit = (`div` 3)
, _codes = monkeyCode
}
(_, logs) = execRWS (replicateM_ 20 throwRound)
monkeyDesc monkeyHold
part2 monkeyCode monkeyHold = monkeyBusinessLevel logs
where monkeyDesc = MonkeyDescription { _limit = (`mod` threshold)
, _codes = monkeyCode
}
(_, logs) = execRWS (replicateM_ 10000 throwRound)
monkeyDesc monkeyHold
threshold = product $ monkeyCode ^.. folded . test
Code
You can get the code from my locally-hosted Git repo, or from Gitlab.