2

We were given a homework, where we get a sample minesweeper-like board, with blank spaces instead of numbers (board is in [String] form) and already placed mines. What we need is to create a function, that replaces all blank spaces with numbers, which are equal to number of adjecent mines.

I was unable of making any real progress, except for removing all spaces with zeroes, which is probably not even useful in any way. I also had a problem with zeroes being of Char type, which made me unable to add for example +1 to it. It would be great if this could be solved without advanced functions, so I can understand it, but any solution or at least idea of solution will do.

This is what the beginning of the code should look like.

import Data.Char

type Result = [String]

pp :: Result -> IO ()
pp x = putStr (concat (map (++"\n") x)) --prints strings in a column.


sampleInput = ["       ",
               " *     ",
               "    *  ",
               "   *   ",
               "      *",
               "***    ",
               "* *    ",
               "***    "]

minesweeper :: Result -> Result

And result should be this

Prelude>pp(minesweeper sampleInput)
1110000
1*11110
1122*10
001*221
233211*
***2011
*8*3000
***2000

I'm incredibly glad for any guidance as I'm unable of any real progress.

Update: Did a bit different yet similar solution. You can check related question here

  • 2
    I think this problem was already posted [here](https://stackoverflow.com/questions/58763683/function-that-replaces-space-with-number-which-represents-stars-around#comment103815110_58763683). I don’t think I can close this as a duplicate though because neither this question nor that one has any answers yet. But I’ll repeat my comment from that question here: – bradrn Nov 10 '19 at 22:04
  • 2
    Here’s some hints: (1) Try create a function which, given a coordinate, returns all the points around that coordinate in a given `Result`; (2) Then try create a function which, given a `Result`, returns a nested list of all coordinates in it (e.g. `["ab","cd"]` becomes `[[(0,0),(0,1)],[(1,0),(1,1)]]`); (3) Try to use these two functions together to solve your problem. – bradrn Nov 10 '19 at 22:04

1 Answers1

4

What you need here is called "stencil convolution". Look at this picture:

111
1x1
111

This is your stencil. Pick a tile on the playing board and place the stencil on top of that tile. How many 1s overlay mines, that number we should display in this tile. For example:

       .....   .....
111    .*...   .....
1x1  + ..x*. = ..3..
111    ...*.   .....
       .....   .....

Once we apply this operation to the whole board, we are basically done.

There are advanced devices for this, such as comonads and arrays, but for the purpose of this post I will keep things simple, so we are going to draft everything by hand, in the simplest types. I am also going to leave some blanks in the code, so that it is less boring for the reader. If you enable -fdefer-typed-holes, you can put things like _wut in place of ... and ghc will tell you what it thinks the type of the hole should be.


  1. I should like to deal with numbers, rather than characters: as you point out, characters are ill-suited for algorithmic work. Conversion should be two way, so that we can display our result as well.

    charsToInts :: [[Char]] -> [[Int]]
    charsToInts = (fmap . fmap) charToInt
      where
        charToInt '*' = ...
        charToInt ... = ...
    
    intsToChars :: [[Int]] -> [[Char]]
    intsToChars = ...
    
  2. A nested list is not a very comfortable type to work with. We will define some helper functions to make it easier. The operation that we will surely need is "indexing", that is, accessing an element by its index — if it is there in the first place.

    lookup2DMaybe :: [[a]] -> (Int, Int) -> Maybe a
    lookup2DMaybe u (i, j) = do
        xs' <- lookupMaybe xs  i
        x   <- ...
        return x
      where
        lookupMaybe :: [a] -> Int -> Maybe a
        lookupMaybe xs i
            | 0 <= i && i < length xs = ...
            | ...                     = ...
    
  3. Now, to interesting stuff — stencil application.

    applyStencil :: [[Int]] -> (Int, Int) -> Int
    applyStencil u = sum . Data.Maybe.catMaybes . fmap (... u) . stencil
      where
        stencil :: (Int, Int) -> [(Int, Int)]
        stencil (i, j) = [ (i + di, ...) | di <- ..., dj <- ..., (di, dj) /= ... ]
    

    Does this work?

    λ applyStencil (charsToInts sampleInput) (3, 5)
    2
    λ applyStencil (charsToInts sampleInput) (6, 1)
    8
    
  4. All that is left to do is to "map" the stencil all around the minefield. To this end, we are going to generate a board where at every point its coordinates are written. I hope somewhere along the way you will see why.

    indices :: [[a]] -> [[(Int, Int)]]
    indices u = [ [ ... | ... ] | ... ]
    

    The idea here is that we give it a board of anything, and it creates a board of coordinates of the same size.

  5. Consider the type we have so far:

    λ :type applyStencil (charsToInts sampleInput) 
    applyStencil (charsToInts sampleInput) :: (Int, Int) -> Int
    

    So, this is a function that converts coordinates to the number of mines around these coordinates. What happens if we map it over a board of coordinates?

    fill :: [[Int]] -> [[Int]]
    fill u = (fmap.fmap) (... u) (... u)
    
    λ intsToChars (fill (charsToInts sampleInput))
    ["1110000","1011110","1122110","0011221","2332110","2422011","4843000","2422000"]
    

    Looks about right, does it not?

  6. Only small things left to do: given two boards of characters, overlay them. This is outlined in an answer to another question about lists of lists. (We get a lot of of questions of this kind of late. Say hi to your professor from the Stack Overflow Haskell Teacher's Assistants Team!)

  7. Final solution!

    minesweeper x = overlay x (intsToChars . fill . charsToInts $ x)
    

    Not hard at all!


There are some ways to improve this code:

  1. Create a specialized type for the board, such that only correct boards can be constructed.
  2. Define a comonad for that type.
  3. Scratch the type altogether and generalize the code to the Store comonad.
  4. Use efficient array processing.

But the ideas we explored are there to last.

Some further reading:

  1. The basics of stencil convolution.
  2. The use of standard comonad types.
  3. Advanced optimization of stencil convolution.

 

Let me know how it goes!

Ignat Insarov
  • 4,660
  • 18
  • 37
  • Alright, this looks very promising. However, I think I need several things explained first in order to even try putting it to use. I have not encountered . operator and am unsure what function applies to what. Is it possible to rewrite it without dots? I also didnt figure out how to turn on -fdefer-typed-holes and didnt quite understand what is in the link you attached. Also, you didn't need to leave holes. Its not about whether it is boring but about me being able to understand what I should need to do. Also, what other approaches could I take if I wanted to avoid stencil? Thanks for help – Jakub Kudlej Nov 11 '19 at 17:00
  • @JakubKudlej Dot operator is simply function composition. I do not think it makes sense to avoid it in Haskell. You can supply `-fdefer-typed-holes` to `ghc` as a command line argument, or you can say `:set -fdefer-typed-holes` in `ghci`. You cannot really avoid stencil, because it is in the very definition of your problem. Overall, I do not think it gets any simpler than this. I gave you structure, separated the problem into small chunks. If you find any particular subproblem too hard, you can ask a precise question about it. I really want to help you, but you have got to learn Haskell, too. – Ignat Insarov Nov 11 '19 at 19:00
  • Should that be `stencil (i,j)` at the end of the `applyStencil` line? It seems like you eta reduced it but only on one side. – Potato44 Nov 11 '19 at 19:28
  • Stencil convolution on nested lists, that's intense ;) @Jakub Kudlej, once you are ready for the 4th optimization step, you can start over here: https://github.com/lehins/massiv#stencil There are some examples on how to create your own stencils and apply them to actual arrays. Arrays in Haskell are slightly more involved than lists, so I'd recommend to understand this answer first. – lehins Nov 11 '19 at 19:43
  • @Potato44 Thanks for catching this. – Ignat Insarov Nov 11 '19 at 20:13
  • Alright, tried to do the first step although it's probably wrong. charsToInts :: [[Char]] -> [[Int]] charsToInts = (fmap . fmap) charToInt where charToInt '*' = 1 charToInt ' ' = 0 intsToChars :: [[Int]] -> [[Char]] intsToChars = (fmap . fmap) intToChar where intToChar 0 = '0' intToChar 1 = '1' intToChar 2 = '2' intToChar 3 = '3' intToChar 4 = '4' intToChar 5 = '5' intToChar 6 = '6' intToChar 7 = '7' intToChar 8 = '8' – Jakub Kudlej Nov 11 '19 at 20:37
  • anyways,here is my attempt on indices: indices::[[a]] -> [[(Int, Int)]] indices ((x:xs):ys) = [[(c,r)|c<-[0..(length(x:xs))]]|r<-[0..(length((x:xs):ys))]] – Jakub Kudlej Nov 11 '19 at 20:48
  • @JakubKudlej See, not hard at all! – Ignat Insarov Nov 11 '19 at 21:17
  • I don't think I did what we need in chars to ints and ints to char part though – Jakub Kudlej Nov 12 '19 at 06:04
  • @JakubKudlej I put your code in the program and it worked. What makes you think it does not? – Ignat Insarov Nov 12 '19 at 10:03
  • another small error, `pp` shouldn't be included in the definition of `minesweeper` – Potato44 Nov 12 '19 at 18:56
  • @Potato44 True. – Ignat Insarov Nov 12 '19 at 19:18
  • @JakubKudlej please put your attempts above, and leave them out of the comments section. Putting that in the comments section makes it difficult for future readers to get value from your question. –  Nov 13 '19 at 20:27