Demonstrate how to find a solution to a given Sokoban level. For the purpose of this task (formally, a PSPACE-complete problem) any method may be used. However a move-optimal or push-optimal (or any other -optimal) solutions is preferred.

Sokoban levels are usually stored as a character array where

 space is an empty square
 # is a wall
 @ is the player
 $ is a box
 . is a goal
 + is the player on a goal
 * is a box on a goal
#######
#     #
#     #
#. #  #
#. $$ #
#.$$  #
#.#  @#
#######
Sokoban solutions are usually stored in the LURD format, where lowercase l, u, r and d represent a move in that (left, up, right, down) direction and capital LURD represents a push.

Please state if you use some other format for either the input or output, and why.

For more information, see the Sokoban wiki.



 
#lang racket
(require data/heap
  "../lib/vector2.rkt" "../lib/queue.rkt" (only-in "../lib/util.rkt" push! tstruct ret awhen))
 
(define level (list "#######"
                    "#     #"
                    "#     #"
                    "#. #  #"
                    "#. $$ #"
                    "#.$$  #"
                    "#.#  @#"
                    "#######"))
(define (strings->vec2 l) (lists->vec2 (map string->list l)))
;turn everything except walls into distance from goals
(define (clear-level l)
  (ret ([l (vec2-copy l)])
    (define dots (vec2-atsq l #\.))
    (define q (list->q (map (λ (p) (cons p 0)) dots)))
    (let bfs () ;this search has implicit history in the mutated vector2
      (unless (nilq? q)
        (match-define (cons p n) (deq! q))
        (define x (vec2@ l p))
        ;stop if position is either a wall or a previously filled number
        (cond [(or (eq? x #\#) (number? x)) (bfs)]
              [else (vec2! l p n)
                    (for-adj l x [p p] #f (enq! (cons p (add1 n)) q))
                    (bfs)])))))
 
;corresponds to PicoLisp's move table in "moves", while also adding a push direction mapping
(tstruct move (f d))
(define-values (mu md ml mr LURD)
  (let ()
    (define t (map (λ (x) (cons (car x) (apply pos (cdr x))))
                   '([#\u -1 0] [#\d 1 0] [#\l 0 -1] [#\r 0 1])))
    (define (mv d)
      (define x (assoc d t))
      (move (λ (p) (pos+ p (cdr x))) (car x)))
    (values (mv #\u) (mv #\d) (mv #\l) (mv #\r)
            (λ (d) (char-upcase (car (findf (λ (x) (equal? d (cdr x))) t)))))))
 
;state = player pos * box poses
(tstruct st (p b))
(define (st= s1 s2) (andmap (λ (b) (member b (st-b s2))) (st-b s1)))
(define (box? p s) (member p (st-b s)))
;calculates value of a state for insertion into priority queue
;value is sum of box distances from goals
(define (value s l) (apply + (map (λ (p) (vec2@ l p)) (st-b s))))
;init state for a level
(define (st0 l) (st (vec2-atq l #\@) (vec2-atsq l #\$)))
(define (make-solution-checker l)
  (define dots (vec2-atsq l #\.))
  (λ (s) (andmap (λ (b) (member b dots)) (st-b s))))
 
;state after push * lurd history
(tstruct push (st h))
(define (pushes s l)
  (ret ([pushes '()])
    (for ([b (in-list (st-b s))])
      (for-adj l a [p b] #f
        (define d (pos- p b)) ;direction of push
        (define op (pos- b d)) ;where player stands to push
        (define o (vec2@ l op))
        ;make sure push pos and push dest are clear
        (when (and (number? a) (number? o)
                   (not (box? p s)) (not (box? op s)))
          (awhen [@ (moves s op l)]
            (define new-st (st b (cons p (remove b (st-b s)))))
            (push! (push new-st (cons (LURD d) @)) pushes)))))))
 
;state * goal pos * level -> lurd string
(define (moves s g l)
  (define h '())
  (define q (list->q (list (list (st-p s)))))
  (let bfs ()
    (if (nilq? q)
        #f
        (match-let ([(cons p lurd) (deq! q)])
          (cond [(equal? p g) lurd]
                [(or (char=? (vec2@ l p) #\#) (box? p s) (member p h)) (bfs)]
                [else (push! p h)
                      (for-each (λ (m)
                                  (match-define (move f s) m)
                                  (enq! (cons (f p) (cons s lurd)) q))
                                (list mu md ml mr))
                      (bfs)])))))
 
(define (sokoban l)
  (define-values (clear s0 solved?)
    (let ([l (strings->vec2 l)])
      (values (clear-level l) (st0 l) (make-solution-checker l))))
  (define h '())
  (tstruct q-elem (s lurd v)) ;priority queue stores state, lurd hist, and value
  (define (elem<= s1 s2) (<= (q-elem-v s1) (q-elem-v s2))) ;compare wrapped values
  ;queue stores a single element at the beginning consisting of:
  ;1. starting state, 2. empty lurd history, 3. value of starting state
  (define q (vector->heap elem<= (vector (q-elem s0 '() (value s0 clear)))))
  (let bfs ()
    (match-define (q-elem s lurd _) (heap-min q))
    (heap-remove-min! q)
    (cond [(solved? s) (list->string (reverse lurd))]
          [(memf (λ (s1) (st= s s1)) h) (bfs)]
          [else (push! s h)
                (for-each (λ (p)
                            (define s (push-st p))
                            (heap-add! q (q-elem s (append (push-h p) lurd) (value s clear))))
                          (pushes s clear))
                (bfs)])))
 