[racket] Rosetta Code Minesweeper Implementation

From: Matthias Felleisen (matthias at ccs.neu.edu)
Date: Mon Jun 3 11:37:53 EDT 2013

Please pre-process inputs so that invalid inputs -- '! 4 4' or worse '1' -- don't crash the program. It should be easy to do. Also print instructions as run boots up. -- Matthias




On Jun 3, 2013, at 11:03 AM, Sean Kanaley wrote:

> Hello all,
> 
> Rosetta Code does not have a minesweeper implementation in Racket.  I have created the one shown below.  Before I post it to Rosetta, I was looking for possible feedback to shorten, make clearer, make "safer" in some way, make more use of library functions, etc.  I would hate to give Racket a bad name in the official show-what-your-language-can-do repository.
> 
> I believe it's not an unreasonable implementation, and I understand this request may not sound exhilarating, so I do not expect any replies!  After however long I feel I will simply post this solution!  With comments and a bit of utility function groundwork it's 131 lines, 82 with that removed.
> 
> To test, run "(run)" and enter "! <row> <col>" to clear @ (row,col) or "? <row> <col>" to assume a mine @ (row,col).
> 
> #lang racket
> ;turns list into list of lists each of size n
> (define (group-n n l)
>   (let group-n ([l l] [acc '()])
>     (if (null? l)
>         (reverse acc)
>         (let-values ([(takes drops) (split-at l n)])
>           (group-n drops (cons takes acc))))))
> 
> ;small 2d vector library
> ;uses built-in vector with minor size stored at index 0
> (define (build-vector2 maj min [f (const 0)])
>   (let ([v (build-vector (add1 (* maj min))
>                          (λ (n) (let-values ([(i j) (quotient/remainder (sub1 n) min)])
>                                   (f i j))))])
>     (vector-set! v 0 min)
>     v))
> (define (vector2-maj v) (quotient (sub1 (vector-length v)) (vector2-min v)))
> (define (vector2-min v) (vector-ref v 0))
> (define (vector2-in-range? v i j) 
>   (and (<= 0 i (sub1 (vector2-maj v)))
>        (<= 0 j (sub1 (vector2-min v)))))
> (define (vector2-index v i j) (+ 1 j (* i (vector2-min v))))
> (define (vector2-ref v i j) (vector-ref v (vector2-index v i j)))
> (define (vector2-set! v i j x) (vector-set! v (vector2-index v i j) x))
> (define (vector2->immutable-vector2 v) (vector->immutable-vector v))
> (define (vector2->lists v) (group-n (vector2-min v) (cdr (vector->list v))))
> 
> ;board uses vector2's directly, but maintaining an abstraction is nice
> (define (board-ref b row col) (vector2-ref b row col))
> (define (board-rows b) (vector2-maj b))
> (define (board-cols b) (vector2-min b))
> (define (on-board? b row col) (vector2-in-range? b row col))
> (define (board->lists b) (vector2->lists b))
> ;run on adjacent board positions
> (define-syntax-rule (for-adj b (r row) (c col) body ...)
>   (for ([i '(0 0 1 1 1 -1 -1 -1)] [j '(1 -1 0 -1 1 0 -1 1)])
>     (let ([r (+ row i)]
>           [c (+ col j)])
>       (when (on-board? b r c)
>         body ...))))
> ;mark is either hidden, assume-mine, or clear
> ;n is int equal to # adj mines or -1 for mine
> (struct pos ([mark #:mutable] n))
> (define (mine? p) (= (pos-n p) -1))
> ;hidden0? is needed because only spaces with no mines in them and no mines adjacent to them are cleared
> (define (hidden0? p)
>   (and (symbol=? (pos-mark p) 'hidden)
>        (zero? (pos-n p))))
> (define (show-pos p)
>   (match-let ([(pos m n) p])
>     (case m
>       [(hidden) "."]
>       [(assume-mine) "?"]
>       [(clear) (if (zero? n) " " (number->string n))]
>       [else (error "illegal mark" m)])))
> ;put "|" around positions
> (define (show-board b)
>   (for ([row (board->lists b)])
>     (displayln (format "|~a|" (string-join (map show-pos row) "|")))))
> 
> ;winning = every position is either cleared or a hidden mine
> (define (win? b)
>   (for*/and ([r (range 0 (board-rows b))]
>              [c (range 0 (board-cols b))])
>     (let ([p (board-ref b r c)])
>       (or (symbol=? (pos-mark p) 'clear)
>           (mine? p)))))
> 
> ;the board is immutable even though its individual positions can mutate their mark field
> (define (init-board rows cols)
>   (let ([chance (+ (/ (random) 10) 0.1)]
>         ;empty board
>         [b (build-vector2 rows cols (λ (r c) (pos 'hidden 0)))])
>     ;loop whole board
>     (for* ([row (range 0 rows)]
>            [col (range 0 cols)])
>       (when (< (random) chance)
>         ;put a mine
>         (vector2-set! b row col (pos 'hidden -1))
>         ;increment adjacent mine counts unless that adjacent position is a mine
>         (for-adj b (r row) (c col)
>                  (let ([p (board-ref b r c)])
>                    (unless (mine? p)
>                      (vector2-set! b r c (pos 'hidden (add1 (pos-n p)))))))))
>     (vector2->immutable-vector2 b)))
> 
> ;only clear position if its hidden and isn't adjacent to a mine
> (define (try-clear! p)
>   (when (hidden0? p)
>     (set-pos-mark! p 'clear)))
> 
> ;the following player move functions return boolean where #f = lose, #t = still going
> ;assuming can never directly lose ((void) == #t)
> (define (assume! b row col) (set-pos-mark! (board-ref b row col) 'assume-mine))
> 
> ;clearing loses when the chosen position is a mine
> ;void = #t as far as if works, so no need to return #t
> (define (clear! b row col)
>   (let ([p (board-ref b row col)])
>     (and (not (mine? p))
>          ;not a mine, so recursively check adjacents, and maintain list of visited positions
>          ;to avoid infinite loops
>          (let ([seen '()])
>            ;clear the chosen position first
>            (set-pos-mark! p 'clear)
>            (let clear-adj ([row row] [col col])
>              (for-adj b (r row) (c col)
>                       ;make sure its not seen
>                       (when (and (not (member (list r c) seen))
>                                  (try-clear! (board-ref b r c)))
>                         ;it was cleared, so loop after saving this position as being seen
>                         (set! seen (cons (list r c) seen))
>                         (clear-adj r c))))))))
> 
> (define (parse-and-do-move! b s)
>   (match-let* ([(list type row col) (string-split s)]
>                [row (string->number row)]
>                [col (string->number col)])
>     (case type
>       [("?") (assume! b row col)]
>       [("!") (clear! b row col)]
>       [else (error "invalid move command" type)])))
> (define (run)
>   (let ([b (init-board 4 6)])
>     (let run ()
>       (show-board b)
>       (display "enter move: ")
>       (if (parse-and-do-move! b (read-line))
>           (if (win? b) (displayln "CLEAR!") (run))
>           (displayln "BOOM!")))))
> ____________________
>  Racket Users list:
>  http://lists.racket-lang.org/users



Posted on the users mailing list.