examples/pacman.rkt
#lang planet dyoo/whalesong

(require (planet dyoo/whalesong/world)
         (planet dyoo/whalesong/image)
         )


;; Constants:

(define E "empty") ;See CellValue data definition below
(define D "dot")   ;
(define W "wall")  ;

(define INIT-BOARD ;See Board data definition below
  (vector (vector W W W W W W W W W W W W W)
          (vector W D D D D D D D D D D D W)
          (vector W D W D W W W W W D W D W)
          (vector W D W D W D D D W D W D W)
          (vector W D W D D D W D D D W D W)
          (vector W D W W D W W W D W W D W)
          (vector W D D D D D E D D D D D W)
          (vector W W W W W W W W W W W W W)))

(define SMALL-BOARD
  (vector (vector E E E)
          (vector E E E)))

(define CELL-SIZE 20)

(define BOARD-WIDTH  (* CELL-SIZE (vector-length (vector-ref INIT-BOARD 0))))
(define BOARD-HEIGHT (* CELL-SIZE (vector-length INIT-BOARD)))

(define SMALL-BOARD-WIDTH  (* CELL-SIZE (vector-length (vector-ref SMALL-BOARD 0))))
(define SMALL-BOARD-HEIGHT (* CELL-SIZE (vector-length SMALL-BOARD)))

(define SCORE-HEIGHT    30)
(define SCORE-TEXT-SIZE 20)

(define PM (circle 10 "solid" "yellow"))

(define MTC  (rectangle CELL-SIZE CELL-SIZE "solid" "black")) ; empty cell
(define DTC  (overlay (circle 3  "solid" "white") MTC))       ; dot in cell
(define WALL (rectangle CELL-SIZE CELL-SIZE "solid" "blue"))  ; wall

(define MTB 
  (empty-scene BOARD-WIDTH
               (+ BOARD-HEIGHT SCORE-HEIGHT)))

(define SMALL-MTB
  (empty-scene SMALL-BOARD-WIDTH
               (+ SMALL-BOARD-HEIGHT SCORE-HEIGHT)))



;; Data definitions:


;; Score is Natural
;; interp. dots eaten by pac-man since start of game

(define INIT-SCORE  0)

;; CellValue is one of:
;; - "empty"
;; - "dot"
;; - "wall"
;; interp. the content of a board cell

;; Direction is one of:
;; - "U"
;; - "D"
;; - "L"
;; - "R"
;; interp. direction that a sprite is facing

(define-struct sprite (x y dir))
;; Sprite is (make-sprite Natural Natural Direction)
;; interp. the position in Board coordinates, and the direction of a sprite

(define INIT-PM (make-sprite 6 6 "U"))

;; Board is (vectorof (vectorof CellValue))
;; interp. the game board

(define RENDER-TEST-BOARD (vector (vector W E)
                                  (vector D E)))

(define-struct gs (pm board board-image score))
;; GameState is (make-gs Sprite Board Image Score)
;; interp. all parts of the pac-man game; pac-man, the current
;; board, the current board image, and the current score

(define MTB-GS (make-gs INIT-PM INIT-BOARD MTB INIT-SCORE))


;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;; Testing values:

;; Sprite:
(define R-SPRITE (make-sprite 1 1 "R"))
(define L-SPRITE (make-sprite 1 1 "L"))
(define U-SPRITE (make-sprite 1 1 "U"))
(define D-SPRITE (make-sprite 1 1 "D"))

;; Board:
(define EE-BOARD (vector (vector W W W W)
                         (vector W E E W)
                         (vector W W W W)))

(define ED-BOARD (vector (vector W W W W)
                         (vector W E D W)
                         (vector W W W W)))

(define DD-BOARD (vector (vector W W W W)
                         (vector W D D W)
                         (vector W W W W)))

;; GameState:
;; MTB-GS previously defined above
(define END-GS (make-gs R-SPRITE EE-BOARD SMALL-MTB 0))


;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;; Functions:


;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;; on-tick handler:


;; GameState -> GameState
;; advances the game

(define (tick gs)
  (local [(define pm          (gs-pm gs))
          (define board       (gs-board gs))
          (define board-image (gs-board-image gs))
          (define score       (gs-score gs))
          (define new-pm          (tick-pm pm board))
          (define new-board       (tick-board board new-pm))
          (define new-board-image (tick-board-image board board-image new-pm))
          (define new-score       (tick-score new-pm board score))]
    (make-gs new-pm
             new-board
             new-board-image
             new-score)))

;; Sprite Board -> Sprite
;; updates pac-man's position based on its direction
(define (tick-pm pm bd)
  (local [(define x   (sprite-x pm))
          (define y   (sprite-y pm))
          (define dir (sprite-dir pm))]
    (make-sprite (checked-move-x x y dir bd)
                 (checked-move-y x y dir bd)
                 dir)))

;; Natural Natural Direction Board -> Natural
;; moves x in direction dir, unless it runs into a wall on bd or dir is not in the x direction
;; ASSUMPTION: assumes x, y is at least one cell away from any edge of bd

(define (checked-move-x x y dir bd)
  (cond [(string=? "L" dir) (restrict-move (sub1 x) y x (sub1 x) bd)]
        [(string=? "R" dir) (restrict-move (add1 x) y x (add1 x) bd)]
        [else x]))

;; Natural Natural Direction Board -> Natural
;; moves y in direction dir, unless it runs into a wall on bd or dir is not in the y direction
;; ASSUMPTION: assumes x, y is at least one cell away from any edge of bd

(define (checked-move-y x y dir bd)
  (cond [(string=? "U" dir) (restrict-move x (sub1 y) y (sub1 y) bd)]
        [(string=? "D" dir) (restrict-move x (add1 y) y (add1 y) bd)]
        [else y]))

;; Natural Natural Natural Natural Board -> Natural
;; produces new-coord if bd does not contain a wall at check-x, check-y; otherwise produces old-coord

(define (restrict-move check-x check-y old-coord new-coord bd)
  (if (string=? (board-ref bd check-x check-y) "wall")
      old-coord
      new-coord))

;; Board Sprite -> Board
;; if cell at pacman's position is not empty, make a new board in which it is

(define (tick-board bd pm)
  (local [(define x (sprite-x pm))
          (define y (sprite-y pm))]
    (if (string=? (board-ref bd x y) "empty")
        bd
        (new-board-w-empty-at x y bd))))

;; Number Number Board -> Board
;; produces a new board with an empty cell at x, y

(define (new-board-w-empty-at x0 y0 bd)
  (map-board (lambda (x y cv)
               (if (and (= x0 x) (= y0 y))
                   "empty"
                   cv))
             bd))

;; Board Image Sprite -> Image
;; updates the board image with an empty cell at x, y if pac-man is in a cell with a dot
(define (tick-board-image bd board-image pm)
  (local [(define x (sprite-x pm))
          (define y (sprite-y pm))]
    (if (string=? (board-ref bd x y) "dot")
        (place-cell-image MTC x y board-image)
        board-image)))

;; Sprite Board Score -> Score
;; increases by 1 the score if pac-man is now in a cell containing a dot
(define (tick-score new-pm last-board score)
  (local [(define x (sprite-x new-pm))
          (define y (sprite-y new-pm))]
    (cond [(string=? (board-ref last-board x y) "dot")
           (add1 score)]
          [else score])))


;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;; on-key handler:


;; GameState KeyEvent -> GameState
;; updates pac-man's direction based on key
(define (key-handler gs key)
  (make-gs (new-dir-pm (gs-pm gs) key)
           (gs-board gs)
           (gs-board-image gs)
           (gs-score gs)))

;; Sprite KeyEvent -> Sprite
;; produces pac-man facing in a new direction based on key
(define (new-dir-pm pm key)
  (cond [(key=? "up"    key) (make-sprite (sprite-x pm) (sprite-y pm) "U")]
        [(key=? "down"  key) (make-sprite (sprite-x pm) (sprite-y pm) "D")]      
        [(key=? "left"  key) (make-sprite (sprite-x pm) (sprite-y pm) "L")]
        [(key=? "right" key) (make-sprite (sprite-x pm) (sprite-y pm) "R")]
        [else pm]))


;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;; on-tilt handler:


;; ;; GameState Number Number Number -> GameState
;; ;; change pac-man's direction based on tilt
;; (define (tilt-handler gs yaw pitch roll)
;;   (make-gs (tilt-pm (gs-pm gs) pitch roll)
;;            (gs-board gs)
;;            (gs-board-image gs)
;;            (gs-score gs)))

;; ;; Sprite Number Number -> Sprite
;; ;; changes pac-man's Direction based on pitch and roll
;; (define (tilt-pm pm pitch roll)
;;   (make-sprite (sprite-x pm)
;;                (sprite-y pm)
;;                (tilt->dir (sprite-dir pm) pitch roll)))

;; ;; Direction Number Number -> Direction
;; ;; changes Direction if there is a prominant tilt, otherwise produces old dir
;; (define (tilt->dir dir pitch roll)
;;   (cond [(> (abs pitch) (abs roll))
;;          (if (positive? pitch)
;;              "U"
;;              "D")]
;;         [(> (abs roll) (abs pitch))
;;          (if (positive? roll)
;;              "R"
;;              "L")]
;;         [else dir]))

;; (define (key-handler gs a-key)
;;   (make-gs (key-pm pm a-key)
;;            (gs-board gs)
;;            (gs-board-image gs)
;;            (gs-source gs)))

;; (define (key-pm pm a-key)
;;   (make-sprite (sprite-x pm)
;;                (sprite-y pm)
;;                (cond
;;                 [(key=? a-key "left")
;;                  "L"]
;;                 [(key=? a-key "right")
;;                  "R"]
;;                 [(key=? a-key "up")
;;                  "U"]
;;                 [(key=? a-key "down")
;;                  "D"]
;;                 [else
;;                  (sprite-dir pm)])))





;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;; stop-when handler:


;; GameState -> Boolean
;; determines if pac-man has eaten all the dots
(define (game-over? gs)
  (empty-board? (gs-board gs)))

;; Board -> Boolean
;; determines if the board is empty
(define (empty-board? bd)
  (foldr-board (lambda (x y cv b)
                 (and b (not (string=? cv "dot"))))
               true
               bd))


;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;; to-draw handler:


;; GameState -> Image
;; draws the game
(define (render gs)
  (render-pm (gs-pm gs)
             (render-score (gs-score gs)
                           (gs-board-image gs))))

;; Board -> Image
;; draws the board
(define (render-board bd)
  (foldr-board (lambda (x y cv b)
                 (place-cell-image (cell-image cv) x y b))
               MTB
               bd))

;; Sprite Image -> Image
;; adds pac-man image to img
(define (render-pm pm img)
  (place-cell-image PM (sprite-x pm) (sprite-y pm) img))

;; Score Image -> Image
;; adds score to img
(define (render-score score img) 
  (local [(define score-text
            (text (string-append "Score: " (number->string score)) SCORE-TEXT-SIZE "black"))]
    (place-image score-text
                 (/ BOARD-WIDTH 2)
                 BOARD-HEIGHT
                 img)))

;; CellValue -> Image
;; draws a board cell
(define (cell-image cv)
  (cond [(string=? cv "empty") MTC] 
        [(string=? cv "dot")   DTC]
        [(string=? cv "wall")  WALL]))


;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;;-------------------------------------------------------------------------------------
;; Operations on Board and other helpers:


;; Board Natural Natural -> CellValue
;; looks up the value of a Board cell
(define (board-ref bd x y)
  (vector-ref (vector-ref bd y) x))


(define (build-vector n f)
  (let ([vec (make-vector n)])
    (let loop ([i 0])
      (cond
       [(< i n)
        (vector-set! vec i (f i))
        (loop (add1 i))]))
    vec))
         

;; (Natural Natural CellValue -> CellValue) Board -> Board
;; the analogue of map for boards, the function is called for
;; each position in the board to produce a cell value for that
;; position in a new resulting board
(define (map-board fn bd)
  (build-vector (vector-length bd)
                (lambda (y)
                  (build-vector (vector-length (vector-ref bd y))
                                (lambda (x)
                                  (fn x y (board-ref bd x y)))))))

;; (Natural Natural CellValue X -> X) X Board -> X
;; the analogue of foldr for boards, the function is called for
;; each position in the board to produce single value
(define (foldr-board fn base bd)
  (local [(define nrows (vector-length bd))
          (define ncols (vector-length (vector-ref bd 0)))
          
          (define (rows y b)
            (cond [(= y nrows) b]
                  [else
                   (rows (add1 y)
                         (cols 0 y b))]))
          (define (cols x y b)
            (cond [(= x ncols) b]
                  [else
                   (cols (add1 x)
                         y
                         (fn x y (board-ref bd x y) b))]))]
    (rows 0 base)))

;; Image Natural Natural Image -> Image
;; adds cell-img to board-image at x, y board coordinates
(define (place-cell-image cell-img x y board-image)
  (place-image cell-img
               (+ (* x CELL-SIZE) (/ CELL-SIZE 2))
               (+ (* y CELL-SIZE) (/ CELL-SIZE 2))
               board-image))





;; -> GameState
;; runs the game
(local [(define INIT-GS (make-gs INIT-PM
                                 INIT-BOARD
                                 (render-board INIT-BOARD)
                                 INIT-SCORE))]
  (big-bang INIT-GS
            (on-tick tick 0.3)
            (to-draw render)
            (on-key key-handler)
            ;;(on-tilt tilt-handler)
            (stop-when game-over?)))