Add ghost CPUs with original Pac-Man AI targeting
Implement four ghosts (Blinky, Pinky, Inky, Clyde) with authentic Pac-Man AI: Blinky chases directly, Pinky targets 2 tiles ahead (with original up-direction bug), Inky uses vector doubling from Blinky, Clyde switches to scatter within 8-tile radius. Includes chase/scatter mode cycling, ghost house exit with staggered delays, directional sprite rendering with animation, and ghost-pacman collision detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
(define maze-layer ((window 'new-layer!)))
|
||||
(define coins-layer ((window 'new-layer!)))
|
||||
(define key-layer ((window 'new-layer!)))
|
||||
(define ghost-layer ((window 'new-layer!)))
|
||||
(define pacman-layer ((window 'new-layer!)))
|
||||
(define ui-layer ((window 'new-layer!)))
|
||||
(define overlay-layer ((window 'new-layer!)))
|
||||
@@ -47,7 +48,6 @@
|
||||
((header-layer 'add-drawable!) header-tile)
|
||||
|
||||
;; draw-header! :: -> /
|
||||
;; Draws the static header with the PAC-MAN title.
|
||||
(define (draw-header!)
|
||||
((header-tile 'draw-rectangle!) 0 0 width header-height color-header-bg)
|
||||
((header-tile 'draw-text!)
|
||||
@@ -81,6 +81,87 @@
|
||||
((key-ui-sprite 'set-x!) key-ui-x)
|
||||
((key-ui-sprite 'set-y!) key-ui-y)
|
||||
|
||||
;;
|
||||
;; Coordinate conversion
|
||||
;;
|
||||
|
||||
;; grid->pixel-x :: number -> number
|
||||
(define (grid->pixel-x col)
|
||||
(* cell-size-px col))
|
||||
|
||||
;; grid->pixel-y :: number -> number
|
||||
(define (grid->pixel-y row)
|
||||
(+ (* row cell-size-px) maze-offset-y))
|
||||
|
||||
;;
|
||||
;; Ghost sprites
|
||||
;;
|
||||
|
||||
;; load-direction-seq :: string, string -> tile-sequence
|
||||
;; Loads a 2-frame animation sequence for one direction.
|
||||
(define (load-direction-seq prefix dir-name)
|
||||
(let ((seq (make-tile-sequence
|
||||
(list (make-bitmap-tile
|
||||
(string-append prefix dir-name "-1.png"))
|
||||
(make-bitmap-tile
|
||||
(string-append prefix dir-name "-2.png"))))))
|
||||
((seq 'set-scale!) sprite-scale-ghost)
|
||||
seq))
|
||||
|
||||
;; make-ghost-draw-state :: string -> ghost-draw-state
|
||||
;; Creates sprite management state for one ghost. Returns a
|
||||
;; dispatch closure for updating position, direction, animation.
|
||||
(define (make-ghost-draw-state name)
|
||||
(let* ((prefix (string-append "pacman-sprites/" name "-"))
|
||||
(up-seq (load-direction-seq prefix "up"))
|
||||
(down-seq (load-direction-seq prefix "down"))
|
||||
(left-seq (load-direction-seq prefix "left"))
|
||||
(right-seq (load-direction-seq prefix "right"))
|
||||
(active-seq left-seq)
|
||||
(cached-dir 'left))
|
||||
;; Add initial sprite to ghost layer
|
||||
((ghost-layer 'add-drawable!) active-seq)
|
||||
|
||||
;; dir->seq :: symbol -> tile-sequence
|
||||
(define (dir->seq dir)
|
||||
(cond ((eq? dir 'up) up-seq)
|
||||
((eq? dir 'down) down-seq)
|
||||
((eq? dir 'left) left-seq)
|
||||
((eq? dir 'right) right-seq)
|
||||
(else left-seq)))
|
||||
|
||||
(define (dispatch msg)
|
||||
(cond
|
||||
;; update! :: number, number, symbol -> /
|
||||
;; Updates position and direction of this ghost's sprite.
|
||||
((eq? msg 'update!)
|
||||
(lambda (row col direction)
|
||||
;; Swap sprite sequence if direction changed
|
||||
(when (not (eq? direction cached-dir))
|
||||
(let ((old-x ((active-seq 'get-x)))
|
||||
(old-y ((active-seq 'get-y))))
|
||||
((ghost-layer 'remove-drawable!) active-seq)
|
||||
(set! active-seq (dir->seq direction))
|
||||
((active-seq 'set-x!) old-x)
|
||||
((active-seq 'set-y!) old-y)
|
||||
((ghost-layer 'add-drawable!) active-seq)
|
||||
(set! cached-dir direction)))
|
||||
;; Update position
|
||||
((active-seq 'set-x!) (grid->pixel-x col))
|
||||
((active-seq 'set-y!) (grid->pixel-y row))))
|
||||
;; animate! :: -> /
|
||||
((eq? msg 'animate!)
|
||||
(lambda () ((active-seq 'set-next!))))
|
||||
(else (error "Ghost draw state -- Unknown message:" msg))))
|
||||
dispatch))
|
||||
|
||||
;; Create draw state for each ghost (in fixed order matching level)
|
||||
(define blinky-draw (make-ghost-draw-state "blinky"))
|
||||
(define pinky-draw (make-ghost-draw-state "pinky"))
|
||||
(define inky-draw (make-ghost-draw-state "inky"))
|
||||
(define clyde-draw (make-ghost-draw-state "clyde"))
|
||||
(define ghost-draw-states (list blinky-draw pinky-draw inky-draw clyde-draw))
|
||||
|
||||
;;
|
||||
;; Pac-Man sprite
|
||||
;;
|
||||
@@ -112,27 +193,14 @@
|
||||
(define cached-time "")
|
||||
(define key-sprite-swapped? #f)
|
||||
(define cached-paused? #f)
|
||||
(define cached-time-up? #f)
|
||||
(define cached-game-over? #f)
|
||||
(define coins-dirty? #t)
|
||||
|
||||
;;
|
||||
;; Coordinate conversion
|
||||
;;
|
||||
|
||||
;; grid->pixel-x :: number -> number
|
||||
(define (grid->pixel-x col)
|
||||
(* cell-size-px col))
|
||||
|
||||
;; grid->pixel-y :: number -> number
|
||||
(define (grid->pixel-y row)
|
||||
(+ (* row cell-size-px) maze-offset-y))
|
||||
|
||||
;;
|
||||
;; Draw functions
|
||||
;;
|
||||
|
||||
;; draw-maze! :: maze -> /
|
||||
;; Draws all walls and doors.
|
||||
(define (draw-maze! maze)
|
||||
((maze-tile 'clear!))
|
||||
((maze 'for-each-cell)
|
||||
@@ -154,7 +222,6 @@
|
||||
color-door))))))
|
||||
|
||||
;; draw-coins! :: maze -> /
|
||||
;; Redraws all coins as small round dots.
|
||||
(define (draw-coins! maze)
|
||||
((coins-tile 'clear!))
|
||||
((maze 'for-each-cell)
|
||||
@@ -168,14 +235,12 @@
|
||||
color-coin)))))
|
||||
|
||||
;; init-key-position! :: key -> /
|
||||
;; Sets the key sprite to its grid position. Called once at startup.
|
||||
(define (init-key-position! key-obj)
|
||||
(let ((pos (key-obj 'position)))
|
||||
((key-sprite 'set-x!) (grid->pixel-x (pos 'col)))
|
||||
((key-sprite 'set-y!) (grid->pixel-y (pos 'row)))))
|
||||
|
||||
;; draw-key! :: key -> /
|
||||
;; Swaps the key sprite once when taken. No-op on subsequent frames.
|
||||
(define (draw-key! key-obj)
|
||||
(when (and (key-obj 'taken?) (not key-sprite-swapped?))
|
||||
((key-layer 'remove-drawable!) key-sprite)
|
||||
@@ -183,15 +248,15 @@
|
||||
(set! key-sprite-swapped? #t)))
|
||||
|
||||
;; animate-pacman! :: number -> /
|
||||
;; Advances the Pac-Man sprite animation based on elapsed time.
|
||||
(define (animate-pacman! delta-time)
|
||||
(set! time-since-last-animation (+ time-since-last-animation delta-time))
|
||||
(when (>= time-since-last-animation animation-interval-ms)
|
||||
((pacman-sprite 'set-next!))
|
||||
;; Also animate ghost sprites
|
||||
(for-each (lambda (gds) ((gds 'animate!))) ghost-draw-states)
|
||||
(set! time-since-last-animation 0)))
|
||||
|
||||
;; draw-pacman! :: pacman -> /
|
||||
;; Updates Pac-Man position and rotation.
|
||||
(define (draw-pacman! pacman)
|
||||
(let* ((pos (pacman 'position))
|
||||
(direction (pacman 'direction)))
|
||||
@@ -202,39 +267,44 @@
|
||||
((eq? direction 'up) ((pacman-sprite 'rotate!) rotation-up))
|
||||
((eq? direction 'down) ((pacman-sprite 'rotate!) rotation-down)))))
|
||||
|
||||
;; draw-ghosts! :: list -> /
|
||||
;; Updates all ghost sprite positions and directions.
|
||||
(define (draw-ghosts! ghosts)
|
||||
(for-each
|
||||
(lambda (ghost ghost-draw)
|
||||
(let* ((pos (ghost 'position))
|
||||
(row (pos 'row))
|
||||
(col (pos 'col))
|
||||
(dir (ghost 'direction)))
|
||||
((ghost-draw 'update!) row col dir)))
|
||||
ghosts ghost-draw-states))
|
||||
|
||||
;; draw-ui! :: score, timer -> /
|
||||
;; Redraws score and time only when their values have changed.
|
||||
(define (draw-ui! score timer)
|
||||
(let ((current-score (score 'points))
|
||||
(current-time ((timer 'format-time))))
|
||||
(when (or (not (= current-score cached-score))
|
||||
(not (string=? current-time cached-time)))
|
||||
((ui-tile 'clear!))
|
||||
;; Score label
|
||||
((ui-tile 'draw-text!)
|
||||
"SCORE" score-label-size score-label-x score-label-y color-text)
|
||||
;; Score value
|
||||
((ui-tile 'draw-text!)
|
||||
(number->string current-score)
|
||||
score-value-size score-value-x score-value-y color-text)
|
||||
;; Sidebar separator
|
||||
((ui-tile 'draw-rectangle!)
|
||||
sidebar-x 0 sidebar-width height color-wall)
|
||||
;; Time label
|
||||
((ui-tile 'draw-text!)
|
||||
"TIME" time-label-size time-label-x time-label-y color-text)
|
||||
;; Time value
|
||||
((ui-tile 'draw-text!)
|
||||
current-time
|
||||
time-value-size time-value-x time-value-y color-text)
|
||||
;; Update cache
|
||||
(set! cached-score current-score)
|
||||
(set! cached-time current-time))))
|
||||
|
||||
;; draw-game-over! :: boolean -> /
|
||||
;; Shows GAME OVER when time is up.
|
||||
(define (draw-game-over! time-up?)
|
||||
(when (and time-up? (not cached-time-up?))
|
||||
;; Shows GAME OVER when time is up or ghost catches Pac-Man.
|
||||
(define (draw-game-over! is-over?)
|
||||
(when (and is-over? (not cached-game-over?))
|
||||
(let ((overlay-tile (make-tile width height)))
|
||||
((overlay-layer 'add-drawable!) overlay-tile)
|
||||
((overlay-tile 'draw-rectangle!)
|
||||
@@ -242,10 +312,9 @@
|
||||
((overlay-tile 'draw-text!)
|
||||
"GAME OVER" game-over-text-size
|
||||
game-over-text-x game-over-text-y color-game-over))
|
||||
(set! cached-time-up? #t)))
|
||||
(set! cached-game-over? #t)))
|
||||
|
||||
;; draw-pause! :: boolean -> /
|
||||
;; Only redraws when pause state actually changes.
|
||||
(define (draw-pause! paused?)
|
||||
(when (not (eq? paused? cached-paused?))
|
||||
((overlay-layer 'empty!))
|
||||
@@ -272,12 +341,12 @@
|
||||
;;
|
||||
|
||||
;; draw-game! :: game -> /
|
||||
;; Draws the full game. Only redraws changed elements.
|
||||
(define (draw-game! game)
|
||||
(let* ((level (game 'level))
|
||||
(timer (level 'timer)))
|
||||
;; Always update (lightweight sprite property sets)
|
||||
(draw-pacman! (level 'pacman))
|
||||
(draw-ghosts! (level 'ghosts))
|
||||
;; Only redraw when dirty / changed
|
||||
(draw-key! (level 'key))
|
||||
(when coins-dirty?
|
||||
@@ -286,22 +355,19 @@
|
||||
(set! coins-dirty? #f))
|
||||
(draw-ui! (level 'score) timer)
|
||||
(draw-pause! (level 'paused?))
|
||||
(draw-game-over! ((timer 'time-up?)))))
|
||||
(draw-game-over! (or ((timer 'time-up?)) (level 'game-over?)))))
|
||||
|
||||
;;
|
||||
;; Callback registration
|
||||
;;
|
||||
|
||||
;; set-game-loop! :: (number -> /) -> /
|
||||
(define (set-game-loop! fun)
|
||||
((window 'set-update-callback!) fun))
|
||||
|
||||
;; set-key-callback! :: (symbol, any -> /) -> /
|
||||
(define (set-key-callback! fun)
|
||||
((window 'set-key-callback!) fun))
|
||||
|
||||
;; start-drawing! :: game -> /
|
||||
;; Starts drawing by setting the draw callback.
|
||||
(define (start-drawing! game)
|
||||
(let ((level (game 'level)))
|
||||
;; Static elements (drawn once)
|
||||
@@ -309,6 +375,8 @@
|
||||
(draw-maze! (level 'maze))
|
||||
(draw-coins! (level 'maze))
|
||||
(init-key-position! (level 'key))
|
||||
;; Initialize ghost positions
|
||||
(draw-ghosts! (level 'ghosts))
|
||||
(set! coins-dirty? #f)
|
||||
;; Register draw callback
|
||||
((window 'set-draw-callback!)
|
||||
|
||||
Reference in New Issue
Block a user