## rec-def: Minesweeper case study

Published 2022-10-10 in sections English, Haskell.

I’m on the train back from MuniHac, where I gave a talk about the `rec-def` library that I have excessively blogged about recently (here, here, here and here). I got quite flattering comments about that talk, so if you want to see if they were sincere, I suggest you watch the recording of “Getting recursive definitions off their bottoms” (but it’s not necessary for the following).

After the talk, Franz Thoma approached me and told me a story of how we was once implementing the game Minesweeper in Haskell, and in particular the part of the logic where, after the user has uncovered a field, the game would automatically uncover all fields that are next to a “neutral” field, i.e. one with zero adjacent bombs. He was using a comonadic data structure, which makes a “context-dependent parallel computation” such as uncovering one field quite natural, and was hoping that using a suitable fix-point operator, he can elegantly obtain not just the next step, but directly the result of recursively uncovering all these fields. But, much to his disappointment, that did not work out: Due to the recursion inherent in that definition, a knot-tying fixed-point operator will lead to a cyclic definition.

He was wondering if the `rec-def` library could have helped him, and we sat down to find out, and this is the tale of this blog post. I will avoid the comonadic abstractions and program it more naively, though, to not lose too many readers along the way. Maybe read Chris Penner’s blog post and Finch’s functional pearl “Getting a Quick Fix on Comonads” if you are curious about that angle.

### Minesweeper setup

Let’s start with defining a suitable data type for the grid of the minesweeper board. I’ll use the `Array` data type, it’s `Ix`-based indexing is quite useful for grids:

``````import Data.Array

type C = (Int,Int)
type Grid a = Array C a``````

The library lacks a function to generate an array from a generating function, but it is easy to add:

``````genArray :: Ix i => (i,i) -> (i -> e) -> Array i e
genArray r f = listArray r \$ map f \$ range r``````

Let’s also fix the size of the board, as a pair of lower and upper bounds (this is the format that the `Ix` type class needs):

``````size :: (C,C)
size = ((0,0), (3,3))``````

Now board is simply a grid of boolean values, with `True` indicating that a bomb is there:

``````type Board = Grid Bool

board1 :: Board
board1 = listArray size
[ False, False, False, False
, True,  False, False, False
, True,  False, False, False
, False, False, False, False
]``````

It would be nice to be able to see these board in a nicer way. So let us write A function that prints a grid, including a frame, given a function that prints something for each coordinate. Together with a function that prints a bomb (as `*`), we can print the board:

``````pGrid :: (C -> String) -> String
pGrid p = unlines
[ concat [ p' (y,x) | x <- [lx-1 .. ux+1] ]
| y  <- [ly-1 .. uy+1] ]
where
((lx,ly),(ux,uy)) = size

p' c | inRange size c = p c
p'  _ = "#"

pBombs :: Board -> String
pBombs b = pGrid \$ \c -> if b ! c then "*" else " "``````

The expression `b ! c` looks up a the coordinate in the array, and is `True` when there is a bomb at that coordinate.

So here is our board, with two bombs:

``````ghci> putStrLn \$ pBombs board1
######
#    #
#*   #
#*   #
#    #
######``````

But that’s not what we want to show to the user: Every field should have have a number that indicates the number of bombs in the surrounding fields. To that end, we first define a function that takes a coordinate, and returns all adjacent coordinates. This also takes care of the border, using `inRange`:

``````neighbors :: C -> [C]
neighbors (x,y) =
[ c
| (dx, dy) <- range ((-1,-1), (1,1))
, (dx, dy) /= (0,0)
, let c = (x + dx, y + dy)
, inRange size c
]``````

With that, we can calculate what to display in each cell – a bomb, or a number:

``````data H = Bomb | Hint Int deriving Eq

hint :: Board -> C -> H
hint b c | b ! c = Bomb
hint b c = Hint \$ sum [ 1 | c' <- neighbors c, b ! c' ]``````

With a suitable printing function, we can now see the full board:

``````hint :: Board -> C -> H
hint b c | b ! c = Bomb
hint b c = Hint \$ sum [ 1 | c' <- neighbors c, b ! c' ]

pCell :: Board -> C -> String
pCell b c = case hint b c of
Bomb -> "*"
Hint 0 -> " "
Hint n -> show n

pBoard :: Board -> String
pBoard b = pGrid (pCell b)``````

And here it is:

``````ghci> putStrLn \$ pBoard board1
######
#11  #
#*2  #
#*2  #
#11  #
######``````

Next we have to add masks: We need to keep track of which fields the user already sees. We again use a grid of booleans, and define a function to print a board with the masked fields hidden behind `?`:

``````type Mask = Grid Bool

[ True,  True,  True,  False
, False, False, False, False
, False, False, False, False
, False, False, False, False
]

pMasked b m = pGrid \$ \c -> if m ! c then pCell b c else "?"``````

So this is what the user would see

``````ghci> putStrLn \$ pMasked board1 mask1
######
#11 ?#
#????#
#????#
#????#
######``````

### Uncovering some fields

With that setup in place, we now implement the piece of logic we care about: Uncovering all fields that are next to a neutral field. Here is the first attempt:

``````solve0 :: Board -> Mask -> Mask
solve0 b m0 = m1
where
m1 = genArray size \$ \c ->
m0 ! c || or [ m0 ! c' | c' <- neighbors c, hint b c' == Hint 0 ]``````

The idea is that we calculate the new mask `m1` from the old one `m0` by the following logic: A field is visible if it was visible before (`m0 ! c`), or if any of its neighboring, neutral fields are visible.

This works so far: I uncovered the three fields next to the one neutral visible field:

``````ghci> putStrLn \$ pMasked board1 \$ solve0 board1 mask1
######
#11  #
#?2  #
#????#
#????#
######``````

But that’s not quite what we want: We want to keep doing that to uncover all fields.

### Uncovering all fields

So what happens if we change the logic to: A field is visible if it was visible before (`m0 ! c`), or if any of its neighboring, neutral fields will be visible.

In the code, this is just a single character change: Instead of looking at `m0` to see if a neighbor is visible, we look at `m1`:

``````solve1 :: Board -> Mask -> Mask
solve1 b m0 = m1
where
m1 = genArray size \$ \c ->
m0 ! c || or [ m1 ! c' | c' <- neighbors c, hint b c' == Hint 0 ]``````

(This is roughly what happened when Franz started to use the `kfix` comonadic fixed-point operator in his code, I believe.)

Does it work? It seems so:

``````ghci> putStrLn \$ pMasked board1 \$ solve1 board1 mask1
######
#11  #
#?2  #
#?2  #
#?1  #
######``````

Amazing, isn’t it!

``````mask2 :: Mask
[ True,  True,  False, False
, False, False, False, False
, False, False, False, False
, False, False, False, True
]``````

which looks as follows:

``````ghci> putStrLn \$ pMasked board1 mask2
######
#11??#
#????#
#????#
#??? #
######``````

Then our `solve1` function does not work, and just sits there:

``````ghci> putStrLn \$ pMasked board1 \$ solve1 board1 mask2
######
#11^CInterrupted.``````

Why did it work before, but now now?

It fails to work because as the code tries to figure out if a field, it needs to know if the next field will be uncovered. But to figure that out, it needs to know if the present field will be uncovered. With the normal boolean connectives (`||` and `or`), this does not make progress.

It worked with `mask1` more or less by accident: None of the fields on in the first column don’t have neutral neighbors, so nothing happens there. And for all the fields in the third and forth column, the code will know for sure that they will be uncovered based on their upper neighbors, which come first in the `neighbors` list, and due to the short-circuting properties of `||`, it doesn’t have to look at the later cells, and the vicious cycle is avoided.

### rec-def to the rescue

This is where `rec-def` comes in: By using the `RBool` type in `m1` instead of plain `Bool`, the recursive self-reference is not a problem, and it simply works:

``````import qualified Data.Recursive.Bool as RB

solve2 b m0 = fmap RB.get m1
where
m1 :: Grid RB.RBool
m1 = genArray size \$ \c ->
RB.mk (m0 ! c) RB.|| RB.or [ m1 ! c' | c' <- neighbors c, hint b c' == Hint 0 ]``````

Note that I did not change the algorithm, or the self-reference through `m1`; I just replaced `Bool` with `RBool`, `||` with `RB.||` and `or` with `RB.or`. And used `RB.get` at the end to get a normal boolean out. And 🥁, here we go:

``````ghci> putStrLn \$ pMasked board1 \$ solve2 board1 mask2
######
#11  #
#?2  #
#?2  #
#?1  #
######``````

That’s the end of this repetition of “let’s look at a tying-the-knot-problem and see how `rec-def` helps”, which always end up a bit anti-climatic because it “just works”, at least in these cases. Hope you enjoyed it nevertheless.