Joachim Breitner's Homepage
rec-def: Minesweeper case study
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
= listArray r $ map f $ range r genArray r f
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)
= ((0,0), (3,3)) size
Now board is simply a grid of boolean values, with True
indicating that a bomb is there:
type Board = Grid Bool
board1 :: Board
= listArray size
board1 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
= unlines
pGrid p concat [ p' (y,x) | x <- [lx-1 .. ux+1] ]
[ | y <- [ly-1 .. uy+1] ]
where
= size
((lx,ly),(ux,uy))
| inRange size c = p c
p' c = "#"
p' _
pBombs :: Board -> String
= pGrid $ \c -> if b ! c then "*" else " " pBombs b
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))
/= (0,0)
, (dx, dy) 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
| b ! c = Bomb
hint b c = Hint $ sum [ 1 | c' <- neighbors c, b ! c' ] hint b c
With a suitable printing function, we can now see the full board:
hint :: Board -> C -> H
| b ! c = Bomb
hint b c = Hint $ sum [ 1 | c' <- neighbors c, b ! c' ]
hint b c
pCell :: Board -> C -> String
= case hint b c of
pCell b c Bomb -> "*"
Hint 0 -> " "
Hint n -> show n
pBoard :: Board -> String
= pGrid (pCell b) pBoard 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
mask1 :: Mask
= listArray size
mask1 True, True, True, False
[ False, False, False, False
, False, False, False, False
, False, False, False, False
,
]
pMasked :: Board -> Mask -> String
= pGrid $ \c -> if m ! c then pCell b c else "?" pMasked b m
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
= m1
solve0 b m0 where
m1 :: Mask
= genArray size $ \c ->
m1 ! c || or [ m0 ! c' | c' <- neighbors c, hint b c' == Hint 0 ] m0
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
= m1
solve1 b m0 where
m1 :: Mask
= genArray size $ \c ->
m1 ! c || or [ m1 ! c' | c' <- neighbors c, hint b c' == Hint 0 ] m0
(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!
Unfortunately, it seems to work by accident. If I start with a different mask:
mask2 :: Mask
= listArray size
mask2 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 :: Board -> Mask -> Mask
= fmap RB.get m1
solve2 b m0 where
m1 :: Grid RB.RBool
= genArray size $ \c ->
m1 ! c) RB.|| RB.or [ m1 ! c' | c' <- neighbors c, hint b c' == Hint 0 ] RB.mk (m0
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.
Have something to say? You can post a comment by sending an e-Mail to me at <mail@joachim-breitner.de>, and I will include it here.