12 Commits

Author SHA1 Message Date
8a1d078069 Merge pull request 'Fix ghost and Pac-Man movement interpolation teleport' (#1) from fix/ghost-movement into feature/ghosts
Reviewed-on: #1
2026-03-23 12:40:05 +01:00
f2cb49ad2b Fix ghost and Pac-Man movement interpolation teleport 2026-03-23 12:38:23 +01:00
joren
0e14140db9 Fix interpolation: use offset backward lerp instead of forward extrapolation
Forward extrapolation caused visible teleporting at every direction
change (ghosts jumping ~1.3 tiles at intersections). Replace with
lerp(prev, current, 0.5 + t/2) which starts the visual at the
midpoint (max 0.5 tile / 12px lag) and smoothly reaches the
destination. No jumps, no teleporting, minimal hitbox offset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 12:00:04 +01:00
joren
5e7456eba9 Fix visual hitbox offset with forward extrapolation
Switch from backward interpolation (lerp prev→current) to forward
extrapolation (current + (current-prev) * t). The visual now leads
toward the next tile, aligning with the logical hitbox so coins
disappear when Pac-Man visually reaches them.

When blocked (sync-prev! sets prev=current), the offset is zero
and the sprite stays at the current tile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:56:22 +01:00
joren
f251478dd6 Add smooth sub-tile interpolation for Pac-Man and ghosts
Entities now move smoothly between tiles instead of snapping. Previous
positions are tracked in pacman and ghost ADTs; the draw layer linearly
interpolates between prev and current based on movement timer progress.

Residual time is carried across movement ticks for consistent speed at
varying frame rates. Teleportation and ghost house exits call sync-prev!
to prevent cross-map interpolation artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:52:00 +01:00
joren
9028dd031c 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>
2026-03-23 11:42:34 +01:00
joren
91b548e0bf Fix(Colors): Use standard Racket color names and round coin dots
The (pp1 graphics) library resolves colors via Racket's color database,
which doesn't support hex strings — they return #f causing a contract
violation on set-brush.

Replaced all hex colors with standard names:
  #2121DE -> "medium blue", #FFB851 -> "gold", #FFB8FF -> "hot pink",
  #FFFF00 -> "yellow", #FF0000 -> "red", #111111 -> "dark slate gray"

Also switched coins from draw-rectangle! to draw-ellipse! for round
dot rendering (arcade-accurate).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:29:14 +01:00
joren
b18aa49e8b Fix(Key) + UI: Fix key sprite position and arcade-style visual overhaul
Key bug fix:
- Key sprite position was never set on startup, appearing at (0,0)
- Added init-key-position! called in start-drawing! to place key sprite
  at its grid position on load

Arcade UI polish:
- Header bar with "PAC-MAN" title in yellow
- "SCORE" label above score value in header
- Sidebar separator uses wall color instead of white block
- "TIME" label with large countdown in sidebar area
- Coins now rendered as small centered dots (coin-size constant)
- Arcade color palette: #2121DE walls, #FFB851 coins, #FFB8FF doors
- "GAME OVER" overlay in red when time expires
- "PAUSED" overlay covers maze area only (not header)
- Window title set to "PAC-MAN"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:27:32 +01:00
joren
3f12a740da UI: Rework constants for arcade-style layout and color palette
- Named color constants (arcade blue walls, golden coins, pink doors)
- Header bar layout with PAC-MAN title, score label, key indicator
- Sidebar time display with label and large value
- Game over and pause overlay positions
- Smaller round-looking coins (coin-size + coin-inset)
- Removed old magic position values

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:26:26 +01:00
joren
eb309b74b1 Optimize(Game): Wire level change events to draw dirty flags
On start!, connects level's coin/maze change callbacks to the draw
ADT's mark-coins-dirty! and mark-maze-dirty! methods. This completes
the event chain: level state change -> dirty flag -> redraw next frame.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:21:31 +01:00
joren
55f1c2a382 Optimize(Level): Add change notification callbacks for draw invalidation
Level now fires callbacks when game state changes that require redrawing:
- on-coins-changed!: fired when a coin is eaten or key is picked up
- on-maze-changed!: fired when a door is removed

Exposes set-on-coins-changed! and set-on-maze-changed! messages so the
game ADT can wire these to the draw ADT's dirty flags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:21:26 +01:00
joren
5b43b3c8d5 Optimize(Draw): Skip unchanged elements in draw callback
Previously every frame: cleared+redrawed 868 cells for coins, cleared+
redrawed all UI text, emptied+recreated pause layer, and swapped key
sprites repeatedly.

Now uses dirty flags and cached values:
- Coins: only redrawn when coins-dirty? is set (on coin eat/key pickup)
- Maze: only redrawn when a door is removed
- UI: only redrawn when score or time string actually changes
- Key: sprite swap happens exactly once, not every frame
- Pause: layer only modified when paused? state transitions

Exposes mark-coins-dirty! and mark-maze-dirty! messages so the game
ADT can signal changes from the level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:21:20 +01:00
8 changed files with 1011 additions and 147 deletions

View File

@@ -7,6 +7,10 @@
;; All graphics logic is isolated in this ADT. Game logic knows nothing about ;; All graphics logic is isolated in this ADT. Game logic knows nothing about
;; pixels, windows, or sprites. Grid-to-pixel conversion happens exclusively ;; pixels, windows, or sprites. Grid-to-pixel conversion happens exclusively
;; here. ;; here.
;;
;; Performance: coins, UI, maze, key, and pause are only redrawn when their
;; underlying state changes. The draw callback tracks previous values and
;; skips unchanged elements.
(define-library (pacman-project adt draw) (define-library (pacman-project adt draw)
(import (scheme base) (import (scheme base)
@@ -19,23 +23,38 @@
;; make-draw :: number, number -> draw ;; make-draw :: number, number -> draw
;; Creates the draw object that handles all rendering. ;; Creates the draw object that handles all rendering.
(define (make-draw width height) (define (make-draw width height)
(let ((window (make-window width height "Pacman"))) (let ((window (make-window width height "PAC-MAN")))
((window 'set-background!) "black") ((window 'set-background!) color-background)
;; ;;
;; Layers (order determines draw order) ;; Layers (order determines draw order)
;; ;;
(define header-layer ((window 'new-layer!)))
(define maze-layer ((window 'new-layer!))) (define maze-layer ((window 'new-layer!)))
(define coins-layer ((window 'new-layer!))) (define coins-layer ((window 'new-layer!)))
(define key-layer ((window 'new-layer!))) (define key-layer ((window 'new-layer!)))
(define ghost-layer ((window 'new-layer!)))
(define pacman-layer ((window 'new-layer!))) (define pacman-layer ((window 'new-layer!)))
(define ui-layer ((window 'new-layer!))) (define ui-layer ((window 'new-layer!)))
(define pause-layer ((window 'new-layer!))) (define overlay-layer ((window 'new-layer!)))
;; ;;
;; Maze tiles ;; Header bar — static, drawn once
;;
(define header-tile (make-tile width header-height))
((header-layer 'add-drawable!) header-tile)
;; draw-header! :: -> /
(define (draw-header!)
((header-tile 'draw-rectangle!) 0 0 width header-height color-header-bg)
((header-tile 'draw-text!)
"PAC-MAN" header-title-size header-title-x header-title-y color-title))
;;
;; Maze tile
;; ;;
(define maze-tile (make-tile width height)) (define maze-tile (make-tile width height))
@@ -56,12 +75,93 @@
((key-sprite 'set-scale!) sprite-scale-key) ((key-sprite 'set-scale!) sprite-scale-key)
((key-layer 'add-drawable!) key-sprite) ((key-layer 'add-drawable!) key-sprite)
;; Key UI indicator (next to the score) ;; Key UI indicator (shown in header when taken)
(define key-ui-sprite (make-bitmap-tile "pacman-sprites/key.png")) (define key-ui-sprite (make-bitmap-tile "pacman-sprites/key.png"))
((key-ui-sprite 'set-scale!) sprite-scale-key-ui) ((key-ui-sprite 'set-scale!) sprite-scale-key-ui)
((key-ui-sprite 'set-x!) key-ui-x) ((key-ui-sprite 'set-x!) key-ui-x)
((key-ui-sprite 'set-y!) key-ui-y) ((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 ;; Pac-Man sprite
;; ;;
@@ -79,31 +179,30 @@
(define time-since-last-animation 0) (define time-since-last-animation 0)
;; ;;
;; UI tiles ;; UI tile (score + time — redrawn on change)
;; ;;
(define ui-tile (make-tile width height)) (define ui-tile (make-tile width height))
((ui-layer 'add-drawable!) ui-tile) ((ui-layer 'add-drawable!) ui-tile)
;; ;;
;; Coordinate conversion ;; Change tracking — skip redraws when state hasn't changed
;; ;;
;; grid->pixel-x :: number -> number (define cached-score -1)
(define (grid->pixel-x col) (define cached-time "")
(* cell-size-px col)) (define key-sprite-swapped? #f)
(define cached-paused? #f)
;; grid->pixel-y :: number -> number (define cached-game-over? #f)
(define (grid->pixel-y row) (define coins-dirty? #t)
(+ (* row cell-size-px) maze-offset-y))
;; ;;
;; Draw functions ;; Draw functions
;; ;;
;; draw-maze! :: maze -> / ;; draw-maze! :: maze -> /
;; Draws all walls and doors.
(define (draw-maze! maze) (define (draw-maze! maze)
((maze-tile 'clear!))
((maze 'for-each-cell) ((maze 'for-each-cell)
(lambda (row col cell-type) (lambda (row col cell-type)
(cond (cond
@@ -113,124 +212,196 @@
(grid->pixel-y row) (grid->pixel-y row)
(- cell-size-px maze-wall-shrink) (- cell-size-px maze-wall-shrink)
(- cell-size-px maze-wall-shrink) (- cell-size-px maze-wall-shrink)
"blue")) color-wall))
((= cell-type cell-type-door) ((= cell-type cell-type-door)
((maze-tile 'draw-rectangle!) ((maze-tile 'draw-rectangle!)
(grid->pixel-x col) (grid->pixel-x col)
(grid->pixel-y row) (grid->pixel-y row)
(- cell-size-px maze-wall-shrink) (- cell-size-px maze-wall-shrink)
(- cell-size-px maze-wall-shrink) (- cell-size-px maze-wall-shrink)
"pink")))))) color-door))))))
;; draw-coins! :: maze -> / ;; draw-coins! :: maze -> /
;; Draws all coins in the maze.
(define (draw-coins! maze) (define (draw-coins! maze)
((coins-tile 'clear!)) ((coins-tile 'clear!))
((maze 'for-each-cell) ((maze 'for-each-cell)
(lambda (row col cell-type) (lambda (row col cell-type)
(when (= cell-type cell-type-coin) (when (= cell-type cell-type-coin)
((coins-tile 'draw-rectangle!) ((coins-tile 'draw-ellipse!)
(+ (grid->pixel-x col) coin-inset) (+ (grid->pixel-x col) coin-inset)
(+ (grid->pixel-y row) coin-inset) (+ (grid->pixel-y row) coin-inset)
(- cell-size-px (* 2 coin-inset) 6) coin-size
(- cell-size-px (* 2 coin-inset) 6) coin-size
"yellow"))))) color-coin)))))
;; draw-key! :: key -> / ;; init-key-position! :: key -> /
;; Draws the key at its position, or shows it in UI if taken. (define (init-key-position! key-obj)
(define (draw-key! key-obj)
(if (key-obj 'taken?)
(begin
((key-layer 'remove-drawable!) key-sprite)
((key-layer 'add-drawable!) key-ui-sprite))
(let ((pos (key-obj 'position))) (let ((pos (key-obj 'position)))
((key-sprite 'set-x!) (grid->pixel-x (pos 'col))) ((key-sprite 'set-x!) (grid->pixel-x (pos 'col)))
((key-sprite 'set-y!) (grid->pixel-y (pos 'row)))))) ((key-sprite 'set-y!) (grid->pixel-y (pos 'row)))))
;; draw-key! :: key -> /
(define (draw-key! key-obj)
(when (and (key-obj 'taken?) (not key-sprite-swapped?))
((key-layer 'remove-drawable!) key-sprite)
((key-layer 'add-drawable!) key-ui-sprite)
(set! key-sprite-swapped? #t)))
;; animate-pacman! :: number -> / ;; animate-pacman! :: number -> /
;; Advances the Pac-Man sprite animation based on elapsed time.
(define (animate-pacman! delta-time) (define (animate-pacman! delta-time)
(set! time-since-last-animation (+ time-since-last-animation delta-time)) (set! time-since-last-animation (+ time-since-last-animation delta-time))
(when (>= time-since-last-animation animation-interval-ms) (when (>= time-since-last-animation animation-interval-ms)
((pacman-sprite 'set-next!)) ((pacman-sprite 'set-next!))
;; Also animate ghost sprites
(for-each (lambda (gds) ((gds 'animate!))) ghost-draw-states)
(set! time-since-last-animation 0))) (set! time-since-last-animation 0)))
;; draw-pacman! :: pacman -> / ;; lerp :: number, number, number -> number
;; Draws Pac-Man at its current position with correct rotation. ;; Linear interpolation between a and b by factor t (0..1).
(define (draw-pacman! pacman) (define (lerp a b t)
(+ a (* t (- b a))))
;; draw-pacman! :: pacman, number -> /
;; Draws Pac-Man with smooth backward interpolation. Uses
;; lerp(prev, current, t) so the visual smoothly transitions
;; from the previous tile strictly to the current tile by t=1.
(define (draw-pacman! pacman progress)
(let* ((pos (pacman 'position)) (let* ((pos (pacman 'position))
(row (pos 'row))
(col (pos 'col))
(prev-row (pacman 'prev-row))
(prev-col (pacman 'prev-col))
(t (max 0 (min progress 1)))
(render-row (lerp prev-row row t))
(render-col (lerp prev-col col t))
(direction (pacman 'direction))) (direction (pacman 'direction)))
;; Set position ((pacman-sprite 'set-x!) (grid->pixel-x render-col))
((pacman-sprite 'set-x!) (grid->pixel-x (pos 'col))) ((pacman-sprite 'set-y!) (grid->pixel-y render-row))
((pacman-sprite 'set-y!) (grid->pixel-y (pos 'row)))
;; Set rotation based on direction
(cond ((eq? direction 'right) ((pacman-sprite 'rotate!) rotation-right)) (cond ((eq? direction 'right) ((pacman-sprite 'rotate!) rotation-right))
((eq? direction 'left) ((pacman-sprite 'rotate!) rotation-left)) ((eq? direction 'left) ((pacman-sprite 'rotate!) rotation-left))
((eq? direction 'up) ((pacman-sprite 'rotate!) rotation-up)) ((eq? direction 'up) ((pacman-sprite 'rotate!) rotation-up))
((eq? direction 'down) ((pacman-sprite 'rotate!) rotation-down))))) ((eq? direction 'down) ((pacman-sprite 'rotate!) rotation-down)))))
;; draw-ghosts! :: list -> /
;; Updates all ghost sprite positions with smooth backward interpolation.
(define (draw-ghosts! ghosts)
(for-each
(lambda (ghost ghost-draw)
(let* ((pos (ghost 'position))
(row (pos 'row))
(col (pos 'col))
(prev-row (ghost 'prev-row))
(prev-col (ghost 'prev-col))
(t (max 0 (min (/ (ghost 'movement-timer) ghost-speed-ms) 1)))
(render-row (lerp prev-row row t))
(render-col (lerp prev-col col t))
(dir (ghost 'direction)))
((ghost-draw 'update!) render-row render-col dir)))
ghosts ghost-draw-states))
;; draw-ui! :: score, timer -> / ;; draw-ui! :: score, timer -> /
;; Draws the score and time limit on screen.
(define (draw-ui! score timer) (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!)) ((ui-tile 'clear!))
;; Score
((ui-tile 'draw-text!) ((ui-tile 'draw-text!)
(number->string (score 'points)) "SCORE" score-label-size score-label-x score-label-y color-text)
score-text-size score-text-x score-text-y "white") ((ui-tile 'draw-text!)
;; Separator line (number->string current-score)
score-value-size score-value-x score-value-y color-text)
((ui-tile 'draw-rectangle!) ((ui-tile 'draw-rectangle!)
separator-x 0 separator-width height "white") sidebar-x 0 sidebar-width height color-wall)
;; Time limit
((ui-tile 'draw-text!) ((ui-tile 'draw-text!)
"Time remaining:" time-text-size time-label-x time-label-y "white") "TIME" time-label-size time-label-x time-label-y color-text)
((ui-tile 'draw-text!) ((ui-tile 'draw-text!)
((timer 'format-time)) current-time
score-text-size time-value-x time-value-y "white")) time-value-size time-value-x time-value-y color-text)
(set! cached-score current-score)
(set! cached-time current-time))))
;; draw-game-over! :: boolean -> /
;; 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!)
0 (- game-over-text-y 20) 672 100 color-background)
((overlay-tile 'draw-text!)
"GAME OVER" game-over-text-size
game-over-text-x game-over-text-y color-game-over))
(set! cached-game-over? #t)))
;; draw-pause! :: boolean -> / ;; draw-pause! :: boolean -> /
;; Shows or hides the pause screen.
(define (draw-pause! paused?) (define (draw-pause! paused?)
((pause-layer 'empty!)) (when (not (eq? paused? cached-paused?))
((overlay-layer 'empty!))
(when paused? (when paused?
(let ((pause-tile (make-tile width height))) (let ((overlay-tile (make-tile width height)))
((pause-layer 'add-drawable!) pause-tile) ((overlay-layer 'add-drawable!) overlay-tile)
((pause-tile 'draw-rectangle!) 0 90 670 height "black") ((overlay-tile 'draw-rectangle!)
((pause-tile 'draw-text!) "Game Paused" 40 200 400 "red")))) 0 maze-offset-y 672 (- height maze-offset-y) color-pause-bg)
((overlay-tile 'draw-text!)
"PAUSED" pause-text-size
pause-text-x pause-text-y color-pause-text)))
(set! cached-paused? paused?)))
;; mark-coins-dirty! :: -> /
(define (mark-coins-dirty!)
(set! coins-dirty? #t))
;; mark-maze-dirty! :: -> /
(define (mark-maze-dirty!)
(set! coins-dirty? #t))
;; ;;
;; Main draw function ;; Main draw function
;; ;;
;; draw-game! :: game -> / ;; draw-game! :: game -> /
;; Draws the full game (registered as draw callback).
(define (draw-game! game) (define (draw-game! game)
(let ((level (game 'level))) (let* ((level (game 'level))
(draw-pacman! (level 'pacman)) (timer (level 'timer))
(pac-progress (/ (level 'pacman-movement-timer) pacman-speed-ms)))
;; Always update (lightweight sprite property sets)
(draw-pacman! (level 'pacman) pac-progress)
(draw-ghosts! (level 'ghosts))
;; Only redraw when dirty / changed
(draw-key! (level 'key)) (draw-key! (level 'key))
(when coins-dirty?
(draw-coins! (level 'maze)) (draw-coins! (level 'maze))
(draw-ui! (level 'score) (level 'timer)) (draw-maze! (level 'maze))
(draw-pause! (level 'paused?)))) (set! coins-dirty? #f))
(draw-ui! (level 'score) timer)
(draw-pause! (level 'paused?))
(draw-game-over! (or ((timer 'time-up?)) (level 'game-over?)))))
;; ;;
;; Callback registration ;; Callback registration
;; ;;
;; set-game-loop! :: (number -> /) -> /
(define (set-game-loop! fun) (define (set-game-loop! fun)
((window 'set-update-callback!) fun)) ((window 'set-update-callback!) fun))
;; set-key-callback! :: (symbol, any -> /) -> /
(define (set-key-callback! fun) (define (set-key-callback! fun)
((window 'set-key-callback!) fun)) ((window 'set-key-callback!) fun))
;; start-drawing! :: game -> / ;; start-drawing! :: game -> /
;; Starts drawing by setting the draw callback.
(define (start-drawing! game) (define (start-drawing! game)
;; Initial maze and coins draw (one-time) (let ((level (game 'level)))
(draw-maze! ((game 'level) 'maze)) ;; Static elements (drawn once)
(draw-coins! ((game 'level) 'maze)) (draw-header!)
(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!) ((window 'set-draw-callback!)
(lambda () (draw-game! game)))) (lambda () (draw-game! game)))))
;; ;;
;; Dispatch ;; Dispatch
@@ -241,6 +412,8 @@
((eq? msg 'set-key-callback!) set-key-callback!) ((eq? msg 'set-key-callback!) set-key-callback!)
((eq? msg 'start-drawing!) start-drawing!) ((eq? msg 'start-drawing!) start-drawing!)
((eq? msg 'animate-pacman!) animate-pacman!) ((eq? msg 'animate-pacman!) animate-pacman!)
((eq? msg 'mark-coins-dirty!) mark-coins-dirty!)
((eq? msg 'mark-maze-dirty!) mark-maze-dirty!)
(else (error "Draw ADT -- Unknown message:" msg)))) (else (error "Draw ADT -- Unknown message:" msg))))
dispatch-draw)))) dispatch-draw))))

View File

@@ -34,8 +34,12 @@
((draw 'animate-pacman!) delta-time)) ((draw 'animate-pacman!) delta-time))
;; start! :: -> / ;; start! :: -> /
;; Starts the game by registering all callbacks. ;; Starts the game by registering all callbacks and change listeners.
(define (start!) (define (start!)
;; Wire level change events to draw dirty flags.
((level 'set-on-coins-changed!) (draw 'mark-coins-dirty!))
((level 'set-on-maze-changed!) (draw 'mark-maze-dirty!))
;; Register graphics callbacks.
((draw 'set-game-loop!) game-loop) ((draw 'set-game-loop!) game-loop)
((draw 'set-key-callback!) key-handler) ((draw 'set-key-callback!) key-handler)
((draw 'start-drawing!) dispatch-game)) ((draw 'start-drawing!) dispatch-game))

View File

@@ -0,0 +1,114 @@
#lang r7rs
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Ghost ADT ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; A ghost has a position, direction, movement mode, and type identity.
;; The targeting AI is handled by the level — this ADT only stores state.
;; Each ghost instance represents one of the four ghosts (Blinky, Inky,
;; Pinky, Clyde). Contains NO graphics code.
(define-library (pacman-project adt ghost)
(import (scheme base)
(pacman-project adt position))
(export make-ghost)
(begin
;; make-ghost :: symbol, number, number, number, number, number -> ghost
;; Creates a ghost of the given type at start-row/col with a scatter
;; corner target and a house exit delay (0 = starts outside).
(define (make-ghost ghost-type start-row start-col
scatter-row scatter-col exit-delay)
(let ((position (make-position start-row start-col))
(direction 'left)
(mode (if (= exit-delay 0) 'scatter 'in-house))
(scatter-target (make-position scatter-row scatter-col))
(house-timer exit-delay)
(movement-timer 0)
(reverse-queued? #f)
(prev-row start-row)
(prev-col start-col))
;; direction! :: symbol -> /
(define (direction! new-dir)
(set! direction new-dir))
;; mode! :: symbol -> /
;; Sets the ghost mode. When switching between chase/scatter,
;; the ghost must reverse direction (queued for next move).
(define (mode! new-mode)
(when (and (not (eq? mode new-mode))
(not (eq? mode 'in-house))
(not (eq? new-mode 'in-house)))
(set! reverse-queued? #t))
(set! mode new-mode))
;; move! :: number, number -> /
;; Saves previous position, then moves by delta.
(define (move! delta-row delta-col)
(set! prev-row (position 'row))
(set! prev-col (position 'col))
((position 'row!) (+ (position 'row) delta-row))
((position 'col!) (+ (position 'col) delta-col)))
;; sync-prev! :: -> /
;; Sets previous position to current. Call after teleportation
;; or ghost house exit to prevent long-range interpolation.
(define (sync-prev!)
(set! prev-row (position 'row))
(set! prev-col (position 'col)))
;; consume-reverse! :: -> boolean
;; Returns #t and clears the flag if a reverse was queued.
(define (consume-reverse!)
(if reverse-queued?
(begin (set! reverse-queued? #f) #t)
#f))
;; update-house-timer! :: number -> /
;; Decreases house timer. When it reaches 0, ghost exits.
(define (update-house-timer! delta-time)
(when (eq? mode 'in-house)
(set! house-timer (- house-timer delta-time))
(when (<= house-timer 0)
(set! mode 'scatter))))
;; reset-movement-timer! :: -> /
(define (reset-movement-timer!)
(set! movement-timer 0))
;; set-movement-timer! :: number -> /
(define (set-movement-timer! val)
(set! movement-timer val))
;; advance-movement-timer! :: number -> number
;; Returns updated movement timer value.
(define (advance-movement-timer! delta-time)
(set! movement-timer (+ movement-timer delta-time))
movement-timer)
;; dispatch-ghost :: symbol -> any
(define (dispatch-ghost msg)
(cond ((eq? msg 'position) position)
((eq? msg 'direction) direction)
((eq? msg 'direction!) direction!)
((eq? msg 'type) ghost-type)
((eq? msg 'mode) mode)
((eq? msg 'mode!) mode!)
((eq? msg 'move!) move!)
((eq? msg 'prev-row) prev-row)
((eq? msg 'prev-col) prev-col)
((eq? msg 'sync-prev!) sync-prev!)
((eq? msg 'scatter-target) scatter-target)
((eq? msg 'in-house?) (eq? mode 'in-house))
((eq? msg 'consume-reverse!) consume-reverse!)
((eq? msg 'update-house-timer!) update-house-timer!)
((eq? msg 'movement-timer) movement-timer)
((eq? msg 'advance-movement-timer!) advance-movement-timer!)
((eq? msg 'reset-movement-timer!) reset-movement-timer!)
((eq? msg 'set-movement-timer!) set-movement-timer!)
(else (error "Ghost ADT -- Unknown message:" msg))))
dispatch-ghost))))

View File

@@ -4,11 +4,13 @@
;; Level ADT ;; ;; Level ADT ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Contains all game logic: automatic Pac-Man movement, collision detection, ;; Contains all game logic: automatic Pac-Man movement, ghost AI, collision
;; coin/key pickup, door opening, teleportation, pause, and time management. ;; detection, coin/key pickup, door opening, teleportation, pause, and time
;; Pac-Man moves automatically in its current direction. Arrow keys queue a ;; management. Pac-Man moves automatically in its current direction. Arrow
;; desired turn direction, which is applied at the next movement tick if the ;; keys queue a desired turn direction, which is applied at the next movement
;; path is clear. Contains NO graphics code. ;; tick if the path is clear. Ghost targeting follows the original Pac-Man
;; AI: Blinky chases directly, Pinky targets ahead, Inky uses vector
;; doubling from Blinky, Clyde is shy within 8 tiles. Contains NO graphics.
(define-library (pacman-project adt level) (define-library (pacman-project adt level)
(import (scheme base) (import (scheme base)
@@ -16,6 +18,7 @@
(pacman-project adt position) (pacman-project adt position)
(pacman-project adt maze) (pacman-project adt maze)
(pacman-project adt pacman) (pacman-project adt pacman)
(pacman-project adt ghost)
(pacman-project adt key) (pacman-project adt key)
(pacman-project adt score) (pacman-project adt score)
(pacman-project adt timer)) (pacman-project adt timer))
@@ -24,7 +27,7 @@
(begin (begin
;; make-level :: -> level ;; make-level :: -> level
;; Creates a new level with all game objects. ;; Creates a new level with all game objects including four ghosts.
(define (make-level) (define (make-level)
(let ((maze (make-maze)) (let ((maze (make-maze))
(pacman (make-pacman 5 2)) (pacman (make-pacman 5 2))
@@ -32,18 +35,68 @@
(score (make-score)) (score (make-score))
(timer (make-timer)) (timer (make-timer))
(paused? #f) (paused? #f)
(game-over? #f)
(queued-direction #f) (queued-direction #f)
(movement-timer 0)) (movement-timer 0)
(on-coins-changed! (lambda () #f))
(on-maze-changed! (lambda () #f))
(on-ghosts-changed! (lambda () #f)))
;; Initialize key after maze is created. ;; Initialize key after maze is created.
(set! key (make-key maze)) (set! key (make-key maze))
;;
;; Create four ghosts
;;
(define blinky
(make-ghost 'blinky
blinky-start-row blinky-start-col
blinky-scatter-row blinky-scatter-col
0))
(define pinky
(make-ghost 'pinky
pinky-start-row pinky-start-col
pinky-scatter-row pinky-scatter-col
pinky-exit-delay))
(define inky
(make-ghost 'inky
inky-start-row inky-start-col
inky-scatter-row inky-scatter-col
inky-exit-delay))
(define clyde
(make-ghost 'clyde
clyde-start-row clyde-start-col
clyde-scatter-row clyde-scatter-col
clyde-exit-delay))
(define ghosts (list blinky pinky inky clyde))
;;
;; Ghost mode cycling
;;
;; Mode schedule: alternating scatter/chase durations.
;; After the last entry, ghosts stay in chase forever.
(define mode-schedule
(list (cons 'scatter scatter-duration-1)
(cons 'chase chase-duration-1)
(cons 'scatter scatter-duration-2)
(cons 'chase chase-duration-2)
(cons 'scatter scatter-duration-3)))
(define current-schedule mode-schedule)
(define mode-timer (if (null? mode-schedule) 0 (cdar mode-schedule)))
(define global-mode 'scatter)
;; ;;
;; Direction helpers ;; Direction helpers
;; ;;
;; direction->delta :: symbol -> (number . number) ;; direction->delta :: symbol -> (number . number)
;; Converts a direction to a (delta-row . delta-col) pair.
(define (direction->delta direction) (define (direction->delta direction)
(cond ((eq? direction 'right) (cons 0 1)) (cond ((eq? direction 'right) (cons 0 1))
((eq? direction 'left) (cons 0 -1)) ((eq? direction 'left) (cons 0 -1))
@@ -51,50 +104,287 @@
((eq? direction 'down) (cons 1 0)) ((eq? direction 'down) (cons 1 0))
(else (cons 0 0)))) (else (cons 0 0))))
;; reverse-direction :: symbol -> symbol
(define (reverse-direction dir)
(cond ((eq? dir 'right) 'left)
((eq? dir 'left) 'right)
((eq? dir 'up) 'down)
((eq? dir 'down) 'up)
(else dir)))
;; can-move? :: symbol -> boolean ;; can-move? :: symbol -> boolean
;; Checks if Pac-Man can move in the given direction (no wall or ;; Checks if Pac-Man can move in the given direction.
;; locked door blocking the way).
(define (can-move? direction) (define (can-move? direction)
(let* ((delta (direction->delta direction)) (let* ((delta (direction->delta direction))
(current-pos (pacman 'position)) (current-pos (pacman 'position))
(next-row (+ (current-pos 'row) (car delta))) (next-row (+ (current-pos 'row) (car delta)))
(next-col (+ (current-pos 'col) (cdr delta)))) (next-col (+ (current-pos 'col) (cdr delta))))
(cond (cond
;; Teleportation tunnels are always passable.
((or (< next-col 0) (>= next-col (maze 'cols))) #t) ((or (< next-col 0) (>= next-col (maze 'cols))) #t)
;; Walls block.
(((maze 'wall?) next-row next-col) #f) (((maze 'wall?) next-row next-col) #f)
;; Doors block unless the key has been taken.
(((maze 'door?) next-row next-col) (key 'taken?)) (((maze 'door?) next-row next-col) (key 'taken?))
(else #t)))) (else #t))))
;; ghost-can-move? :: ghost, symbol -> boolean
;; Checks if a ghost can move in the given direction.
;; Active ghosts cannot re-enter the ghost house.
(define (ghost-can-move? ghost direction)
(let* ((delta (direction->delta direction))
(pos (ghost 'position))
(next-row (+ (pos 'row) (car delta)))
(next-col (+ (pos 'col) (cdr delta))))
(cond
;; Off-grid horizontally: tunnel
((or (< next-col 0) (>= next-col (maze 'cols))) #t)
;; Walls block
(((maze 'wall?) next-row next-col) #f)
;; All doors block active ghosts (including ghost house door)
(((maze 'door?) next-row next-col) #f)
(else #t))))
;;
;; Distance calculation
;;
;; squared-distance :: number, number, number, number -> number
(define (squared-distance r1 c1 r2 c2)
(+ (* (- r1 r2) (- r1 r2))
(* (- c1 c2) (- c1 c2))))
;;
;; Ghost targeting AI
;;
;; ghost-target :: ghost -> (number . number)
;; Returns the target tile (row . col) for the ghost based on its
;; current mode and type-specific AI.
(define (ghost-target ghost)
(let ((mode (ghost 'mode)))
(cond
;; Scatter: head to assigned corner
((eq? mode 'scatter)
(let ((st (ghost 'scatter-target)))
(cons (st 'row) (st 'col))))
;; Chase: type-specific targeting
((eq? mode 'chase)
(let* ((pac-pos (pacman 'position))
(pac-row (pac-pos 'row))
(pac-col (pac-pos 'col))
(pac-dir (pacman 'direction))
(ghost-type (ghost 'type)))
(cond
;; Blinky: target Pac-Man directly
((eq? ghost-type 'blinky)
(cons pac-row pac-col))
;; Pinky: target 2 tiles ahead of Pac-Man
;; Original bug: when facing up, also offset 2 left
((eq? ghost-type 'pinky)
(let ((ahead (direction->delta pac-dir)))
(if (eq? pac-dir 'up)
(cons (+ pac-row (* (car ahead) pinky-look-ahead))
(+ pac-col (* (cdr ahead) pinky-look-ahead) (- pinky-look-ahead)))
(cons (+ pac-row (* (car ahead) pinky-look-ahead))
(+ pac-col (* (cdr ahead) pinky-look-ahead))))))
;; Inky: double the vector from Blinky to 2-ahead-of-Pac-Man
((eq? ghost-type 'inky)
(let* ((ahead (direction->delta pac-dir))
(pivot-row (+ pac-row (* (car ahead) pinky-look-ahead)))
(pivot-col (+ pac-col (* (cdr ahead) pinky-look-ahead)))
(blinky-pos (blinky 'position)))
(cons (+ pivot-row (- pivot-row (blinky-pos 'row)))
(+ pivot-col (- pivot-col (blinky-pos 'col))))))
;; Clyde: chase Pac-Man, but scatter when within 8 tiles
((eq? ghost-type 'clyde)
(let* ((ghost-pos (ghost 'position))
(dist-sq (squared-distance
(ghost-pos 'row) (ghost-pos 'col)
pac-row pac-col)))
(if (< dist-sq (* clyde-shy-distance clyde-shy-distance))
(let ((st (ghost 'scatter-target)))
(cons (st 'row) (st 'col)))
(cons pac-row pac-col))))
;; Fallback
(else (cons pac-row pac-col)))))
;; Default (in-house etc): stay put
(else
(let ((pos (ghost 'position)))
(cons (pos 'row) (pos 'col)))))))
;; choose-ghost-direction :: ghost -> symbol
;; Picks the best direction for the ghost at its current tile.
;; Cannot reverse; chooses direction minimizing distance to target.
;; Tie-break order: up, left, down, right.
(define (choose-ghost-direction ghost)
(let* ((target (ghost-target ghost))
(target-row (car target))
(target-col (cdr target))
(current-dir (ghost 'direction))
(reverse-dir (reverse-direction current-dir))
(pos (ghost 'position))
(row (pos 'row))
(col (pos 'col))
(candidates '(up left down right)))
(let loop ((dirs candidates)
(best-dir current-dir)
(best-dist 999999999))
(if (null? dirs)
;; If no valid direction found, reverse as last resort
(if (= best-dist 999999999) reverse-dir best-dir)
(let* ((dir (car dirs))
(delta (direction->delta dir))
(nr (+ row (car delta)))
(nc (+ col (cdr delta))))
(if (and (not (eq? dir reverse-dir))
(ghost-can-move? ghost dir))
(let ((dist (squared-distance nr nc target-row target-col)))
(if (< dist best-dist)
(loop (cdr dirs) dir dist)
(loop (cdr dirs) best-dir best-dist)))
(loop (cdr dirs) best-dir best-dist)))))))
;;
;; Ghost movement
;;
;; move-ghost! :: ghost -> /
;; Moves a ghost one tile in its chosen direction.
(define (move-ghost! ghost)
(let* ((dir (ghost 'direction))
(delta (direction->delta dir))
(pos (ghost 'position))
(next-row (+ (pos 'row) (car delta)))
(next-col (+ (pos 'col) (cdr delta))))
;; Handle tunnel teleportation
(cond
((< next-col 0)
((pos 'col!) (- (maze 'cols) 1))
((ghost 'sync-prev!)))
((>= next-col (maze 'cols))
((pos 'col!) 0)
((ghost 'sync-prev!)))
(else
((ghost 'move!) (car delta) (cdr delta))))))
;; advance-ghost! :: ghost -> /
;; Chooses direction and moves one ghost.
(define (advance-ghost! ghost)
(when (not (ghost 'in-house?))
;; Handle queued reverse from mode change
(when ((ghost 'consume-reverse!))
((ghost 'direction!) (reverse-direction (ghost 'direction))))
;; Choose best direction at current tile
(let ((new-dir (choose-ghost-direction ghost)))
((ghost 'direction!) new-dir)
(move-ghost! ghost))
(on-ghosts-changed!)))
;; exit-ghost-house! :: ghost -> /
;; Moves a ghost from inside the house to the exit position.
(define (exit-ghost-house! ghost)
(let ((pos (ghost 'position)))
((pos 'row!) ghost-house-exit-row)
((pos 'col!) ghost-house-exit-col)
((ghost 'direction!) 'left)
((ghost 'sync-prev!))
(on-ghosts-changed!)))
;;
;; Ghost-Pacman collision
;;
;; check-ghost-collision! :: -> /
;; Checks if any ghost occupies the same tile as Pac-Man.
(define (check-ghost-collision!)
(let ((pac-pos (pacman 'position)))
(for-each
(lambda (ghost)
(when (and (not (ghost 'in-house?))
(= ((ghost 'position) 'row) (pac-pos 'row))
(= ((ghost 'position) 'col) (pac-pos 'col)))
(set! game-over? #t)))
ghosts)))
;;
;; Mode cycling
;;
;; update-ghost-mode! :: number -> /
;; Advances the global mode timer and switches scatter/chase.
(define (update-ghost-mode! delta-time)
(when (not (null? current-schedule))
(set! mode-timer (- mode-timer delta-time))
(when (<= mode-timer 0)
(set! current-schedule (cdr current-schedule))
(if (null? current-schedule)
;; Permanent chase after schedule ends
(begin
(set! global-mode 'chase)
(for-each (lambda (g) ((g 'mode!) 'chase)) ghosts))
(let ((next-mode (caar current-schedule))
(next-duration (cdar current-schedule)))
(set! global-mode next-mode)
(set! mode-timer next-duration)
(for-each (lambda (g)
(when (not (g 'in-house?))
((g 'mode!) next-mode)))
ghosts))))))
;;
;; Update ghosts
;;
;; update-ghosts! :: number -> /
;; Updates all ghost timers, house exits, and movement.
(define (update-ghosts! delta-time)
(for-each
(lambda (ghost)
;; Update house timer for ghosts still inside
(let ((was-in-house? (ghost 'in-house?)))
((ghost 'update-house-timer!) delta-time)
;; Ghost just exited house
(when (and was-in-house? (not (ghost 'in-house?)))
(exit-ghost-house! ghost)
((ghost 'mode!) global-mode)))
;; Movement tick for active ghosts (carry residual time)
(when (not (ghost 'in-house?))
(let ((mt ((ghost 'advance-movement-timer!) delta-time)))
(when (>= mt ghost-speed-ms)
(advance-ghost! ghost)
((ghost 'set-movement-timer!) (- mt ghost-speed-ms))))))
ghosts))
;; ;;
;; Coin logic ;; Coin logic
;; ;;
;; eat-coin! :: number, number -> / ;; eat-coin! :: number, number -> /
;; Removes the coin at the cell and updates score/time.
(define (eat-coin! row col) (define (eat-coin! row col)
((maze 'cell-set!) row col cell-type-empty) ((maze 'cell-set!) row col cell-type-empty)
((score 'increase!)) ((score 'increase!))
((timer 'increase!))) ((timer 'increase!))
(on-coins-changed!))
;; ;;
;; Key logic ;; Key logic
;; ;;
;; pick-up-key! :: number, number -> / ;; pick-up-key! :: number, number -> /
;; Picks up the key and clears the cell.
(define (pick-up-key! row col) (define (pick-up-key! row col)
((maze 'cell-set!) row col cell-type-empty) ((maze 'cell-set!) row col cell-type-empty)
((key 'take!))) ((key 'take!))
(on-coins-changed!))
;; ;;
;; Teleportation logic ;; Teleportation logic
;; ;;
;; teleport-horizontal! :: number, number -> / ;; teleport-horizontal! :: number, number -> /
;; Teleports Pac-Man to the other side of the maze.
(define (teleport-horizontal! row col) (define (teleport-horizontal! row col)
(let ((pac-pos (pacman 'position))) (let ((pac-pos (pacman 'position)))
(cond ((< col 0) (cond ((< col 0)
@@ -102,15 +392,15 @@
((pac-pos 'row!) row)) ((pac-pos 'row!) row))
((>= col (maze 'cols)) ((>= col (maze 'cols))
((pac-pos 'col!) 0) ((pac-pos 'col!) 0)
((pac-pos 'row!) row))))) ((pac-pos 'row!) row)))
;; Prevent interpolation across the entire map
((pacman 'sync-prev!))))
;; ;;
;; Movement logic ;; Movement logic
;; ;;
;; move-pacman! :: symbol -> / ;; move-pacman! :: symbol -> /
;; Moves Pac-Man one step in the given direction, handling collisions,
;; teleportation, and item pickup.
(define (move-pacman! direction) (define (move-pacman! direction)
(let* ((delta (direction->delta direction)) (let* ((delta (direction->delta direction))
(delta-row (car delta)) (delta-row (car delta))
@@ -119,24 +409,20 @@
(next-row (+ (current-pos 'row) delta-row)) (next-row (+ (current-pos 'row) delta-row))
(next-col (+ (current-pos 'col) delta-col))) (next-col (+ (current-pos 'col) delta-col)))
;; Update facing direction for the draw layer.
((pacman 'direction!) direction) ((pacman 'direction!) direction)
(cond (cond
;; Teleportation: outside grid horizontally.
((or (< next-col 0) (>= next-col (maze 'cols))) ((or (< next-col 0) (>= next-col (maze 'cols)))
(teleport-horizontal! next-row next-col)) (teleport-horizontal! next-row next-col))
;; Door: open it if key has been taken.
(((maze 'door?) next-row next-col) (((maze 'door?) next-row next-col)
(when (key 'taken?) (when (key 'taken?)
((maze 'remove-door!) next-row next-col))) ((maze 'remove-door!) next-row next-col)
(on-maze-changed!)))
;; Normal movement: only if not a wall.
(else (else
(when (not ((maze 'wall?) next-row next-col)) (when (not ((maze 'wall?) next-row next-col))
((pacman 'move!) delta-row delta-col) ((pacman 'move!) delta-row delta-col)
;; Check what's at the new position.
(cond (cond
(((maze 'key?) next-row next-col) (((maze 'key?) next-row next-col)
(pick-up-key! next-row next-col)) (pick-up-key! next-row next-col))
@@ -144,25 +430,26 @@
(eat-coin! next-row next-col)))))))) (eat-coin! next-row next-col))))))))
;; advance-pacman! :: -> / ;; advance-pacman! :: -> /
;; Called every movement tick. Tries the queued direction first; if
;; that path is blocked, continues in the current direction.
(define (advance-pacman!) (define (advance-pacman!)
(when (not ((timer 'time-up?))) (when (not (or ((timer 'time-up?)) game-over?))
(let ((current-dir (pacman 'direction))) (let* ((current-dir (pacman 'direction))
;; Try the queued direction first. (pos-before-row ((pacman 'position) 'row))
(pos-before-col ((pacman 'position) 'col)))
(cond (cond
((and queued-direction (can-move? queued-direction)) ((and queued-direction (can-move? queued-direction))
(move-pacman! queued-direction) (move-pacman! queued-direction)
(set! queued-direction #f)) (set! queued-direction #f))
;; Otherwise keep moving in the current direction.
((can-move? current-dir) ((can-move? current-dir)
(move-pacman! current-dir)))))) (move-pacman! current-dir)))
;; If pacman didn't actually move, sync prev to avoid drift
(when (and (= ((pacman 'position) 'row) pos-before-row)
(= ((pacman 'position) 'col) pos-before-col))
((pacman 'sync-prev!))))))
;; ;;
;; Pause logic ;; Pause logic
;; ;;
;; toggle-pause! :: -> /
(define (toggle-pause!) (define (toggle-pause!)
(set! paused? (not paused?))) (set! paused? (not paused?)))
@@ -171,7 +458,6 @@
;; ;;
;; key-press! :: symbol -> / ;; key-press! :: symbol -> /
;; Processes a key press. Arrow keys queue a desired direction.
(define (key-press! pressed-key) (define (key-press! pressed-key)
(cond (cond
((eq? pressed-key 'escape) (toggle-pause!)) ((eq? pressed-key 'escape) (toggle-pause!))
@@ -183,19 +469,36 @@
((eq? pressed-key 'down) (set! queued-direction 'down)))))) ((eq? pressed-key 'down) (set! queued-direction 'down))))))
;; ;;
;; Update (game loop function) ;; Update (game loop)
;; ;;
;; update! :: number -> / ;; update! :: number -> /
;; Called each frame with elapsed milliseconds. Advances the movement
;; timer and moves Pac-Man automatically when the interval elapses.
(define (update! delta-time) (define (update! delta-time)
(when (not paused?) (when (not (or paused? game-over?))
((timer 'decrease!) delta-time) ((timer 'decrease!) delta-time)
;; Pac-Man movement (carry residual for smoother interpolation)
(set! movement-timer (+ movement-timer delta-time)) (set! movement-timer (+ movement-timer delta-time))
(when (>= movement-timer pacman-speed-ms) (when (>= movement-timer pacman-speed-ms)
(advance-pacman!) (advance-pacman!)
(set! movement-timer 0)))) (set! movement-timer (- movement-timer pacman-speed-ms))
(check-ghost-collision!))
;; Ghost mode cycling and movement
(update-ghost-mode! delta-time)
(update-ghosts! delta-time)
(check-ghost-collision!)))
;;
;; Callbacks
;;
(define (set-on-coins-changed! callback)
(set! on-coins-changed! callback))
(define (set-on-maze-changed! callback)
(set! on-maze-changed! callback))
(define (set-on-ghosts-changed! callback)
(set! on-ghosts-changed! callback))
;; ;;
;; Dispatch ;; Dispatch
@@ -204,12 +507,18 @@
(define (dispatch-level msg) (define (dispatch-level msg)
(cond ((eq? msg 'maze) maze) (cond ((eq? msg 'maze) maze)
((eq? msg 'pacman) pacman) ((eq? msg 'pacman) pacman)
((eq? msg 'ghosts) ghosts)
((eq? msg 'key) key) ((eq? msg 'key) key)
((eq? msg 'score) score) ((eq? msg 'score) score)
((eq? msg 'timer) timer) ((eq? msg 'timer) timer)
((eq? msg 'paused?) paused?) ((eq? msg 'paused?) paused?)
((eq? msg 'game-over?) game-over?)
((eq? msg 'pacman-movement-timer) movement-timer)
((eq? msg 'key-press!) key-press!) ((eq? msg 'key-press!) key-press!)
((eq? msg 'update!) update!) ((eq? msg 'update!) update!)
((eq? msg 'set-on-coins-changed!) set-on-coins-changed!)
((eq? msg 'set-on-maze-changed!) set-on-maze-changed!)
((eq? msg 'set-on-ghosts-changed!) set-on-ghosts-changed!)
(else (error "Level ADT -- Unknown message:" msg)))) (else (error "Level ADT -- Unknown message:" msg))))
dispatch-level)))) dispatch-level))))

View File

@@ -4,8 +4,9 @@
;; Pac-Man ADT ;; ;; Pac-Man ADT ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Manages the logical state of the player: grid position and current ;; Manages the logical state of the player: grid position, direction, and
;; direction. Contains NO graphics code. ;; previous position for smooth rendering interpolation. Contains NO
;; graphics code.
(define-library (pacman-project adt pacman) (define-library (pacman-project adt pacman)
(import (scheme base) (import (scheme base)
@@ -18,7 +19,9 @@
;; Creates a Pac-Man object at the given start position (row, col). ;; Creates a Pac-Man object at the given start position (row, col).
(define (make-pacman start-row start-col) (define (make-pacman start-row start-col)
(let ((position (make-position start-row start-col)) (let ((position (make-position start-row start-col))
(direction 'right)) (direction 'right)
(prev-row start-row)
(prev-col start-col))
;; position! :: position -> / ;; position! :: position -> /
(define (position! new-position) (define (position! new-position)
@@ -29,11 +32,20 @@
(set! direction new-direction)) (set! direction new-direction))
;; move! :: number, number -> / ;; move! :: number, number -> /
;; Moves Pac-Man by a delta on the grid. ;; Saves previous position, then moves by delta on the grid.
(define (move! delta-row delta-col) (define (move! delta-row delta-col)
(set! prev-row (position 'row))
(set! prev-col (position 'col))
((position 'row!) (+ (position 'row) delta-row)) ((position 'row!) (+ (position 'row) delta-row))
((position 'col!) (+ (position 'col) delta-col))) ((position 'col!) (+ (position 'col) delta-col)))
;; sync-prev! :: -> /
;; Sets previous position to current. Call after teleportation
;; to prevent interpolation across the map.
(define (sync-prev!)
(set! prev-row (position 'row))
(set! prev-col (position 'col)))
;; dispatch-pacman :: symbol -> any ;; dispatch-pacman :: symbol -> any
(define (dispatch-pacman msg) (define (dispatch-pacman msg)
(cond ((eq? msg 'position) position) (cond ((eq? msg 'position) position)
@@ -41,6 +53,9 @@
((eq? msg 'direction) direction) ((eq? msg 'direction) direction)
((eq? msg 'direction!) direction!) ((eq? msg 'direction!) direction!)
((eq? msg 'move!) move!) ((eq? msg 'move!) move!)
((eq? msg 'prev-row) prev-row)
((eq? msg 'prev-col) prev-col)
((eq? msg 'sync-prev!) sync-prev!)
(else (error "Pac-Man ADT -- Unknown message:" msg)))) (else (error "Pac-Man ADT -- Unknown message:" msg))))
dispatch-pacman)))) dispatch-pacman))))

View File

@@ -27,14 +27,60 @@
;; Coin rendering ;; Coin rendering
coin-inset coin-inset
coin-size
;; Sprites ;; Sprites
sprite-scale-pacman sprite-scale-pacman
sprite-scale-key sprite-scale-key
sprite-scale-key-ui sprite-scale-key-ui
sprite-scale-ghost
;; Movement ;; Movement
pacman-speed-ms pacman-speed-ms
ghost-speed-ms
;; Ghost house
ghost-house-door-row
ghost-house-door-col-left
ghost-house-door-col-right
ghost-house-exit-row
ghost-house-exit-col
;; Ghost start positions
blinky-start-row
blinky-start-col
pinky-start-row
pinky-start-col
inky-start-row
inky-start-col
clyde-start-row
clyde-start-col
;; Ghost scatter corners
blinky-scatter-row
blinky-scatter-col
pinky-scatter-row
pinky-scatter-col
inky-scatter-row
inky-scatter-col
clyde-scatter-row
clyde-scatter-col
;; Ghost house exit delays (ms)
pinky-exit-delay
inky-exit-delay
clyde-exit-delay
;; Ghost mode durations (ms)
scatter-duration-1
chase-duration-1
scatter-duration-2
chase-duration-2
scatter-duration-3
;; Ghost AI
clyde-shy-distance
pinky-look-ahead
;; Animation ;; Animation
animation-interval-ms animation-interval-ms
@@ -56,19 +102,45 @@
rotation-up rotation-up
rotation-down rotation-down
;; UI positions ;; Colors
score-text-size color-background
score-text-x color-wall
score-text-y color-door
time-text-size color-coin
color-text
color-title
color-header-bg
color-game-over
color-pause-bg
color-pause-text
;; UI layout
header-height
header-title-size
header-title-x
header-title-y
score-label-size
score-label-x
score-label-y
score-value-size
score-value-x
score-value-y
key-ui-x
key-ui-y
sidebar-x
sidebar-width
time-label-size
time-label-x time-label-x
time-label-y time-label-y
time-value-size
time-value-x time-value-x
time-value-y time-value-y
separator-x game-over-text-size
separator-width game-over-text-x
key-ui-x game-over-text-y
key-ui-y) pause-text-size
pause-text-x
pause-text-y)
(begin (begin
@@ -88,16 +160,62 @@
(define cell-type-key 3) (define cell-type-key 3)
(define cell-type-door 4) (define cell-type-door 4)
;; Coin rendering: inset in pixels from cell edge ;; Coin rendering
(define coin-inset 7) (define coin-inset 9)
(define coin-size 6)
;; Sprite scale factors ;; Sprite scale factors
(define sprite-scale-pacman 1.5) (define sprite-scale-pacman 1.5)
(define sprite-scale-key 1.5) (define sprite-scale-key 1.5)
(define sprite-scale-key-ui 3) (define sprite-scale-key-ui 3)
(define sprite-scale-ghost 1.5)
;; Movement speed: time between automatic movement ticks ;; Movement speed: time between automatic movement ticks
(define pacman-speed-ms 200) (define pacman-speed-ms 200)
(define ghost-speed-ms 220)
;; Ghost house position (door and exit point above door)
(define ghost-house-door-row 12)
(define ghost-house-door-col-left 13)
(define ghost-house-door-col-right 14)
(define ghost-house-exit-row 11)
(define ghost-house-exit-col 14)
;; Ghost start positions
(define blinky-start-row 11)
(define blinky-start-col 14)
(define pinky-start-row 14)
(define pinky-start-col 13)
(define inky-start-row 14)
(define inky-start-col 11)
(define clyde-start-row 14)
(define clyde-start-col 16)
;; Ghost scatter target corners
(define blinky-scatter-row 0)
(define blinky-scatter-col 27)
(define pinky-scatter-row 0)
(define pinky-scatter-col 0)
(define inky-scatter-row 30)
(define inky-scatter-col 27)
(define clyde-scatter-row 30)
(define clyde-scatter-col 0)
;; Ghost house exit delays (ms) — staggered release
(define pinky-exit-delay 2000)
(define inky-exit-delay 5000)
(define clyde-exit-delay 8000)
;; Ghost mode durations (ms) — scatter/chase alternation
(define scatter-duration-1 7000)
(define chase-duration-1 20000)
(define scatter-duration-2 7000)
(define chase-duration-2 20000)
(define scatter-duration-3 5000)
;; Ghost AI parameters
(define clyde-shy-distance 8)
(define pinky-look-ahead 2)
;; Animation timing ;; Animation timing
(define animation-interval-ms 100) (define animation-interval-ms 100)
@@ -119,22 +237,54 @@
(define rotation-up 90) (define rotation-up 90)
(define rotation-down -90) (define rotation-down -90)
;; UI positions for score display ;; Colors — arcade-style palette (standard Racket color names only)
(define score-text-size 40) (define color-background "black")
(define score-text-x 560) (define color-wall "medium blue")
(define score-text-y 20) (define color-door "hot pink")
(define color-coin "gold")
(define color-text "white")
(define color-title "yellow")
(define color-header-bg "dark slate gray")
(define color-game-over "red")
(define color-pause-bg "black")
(define color-pause-text "red")
;; UI positions for time display (right side of separator) ;; UI layout — header bar at the top
(define time-text-size 35) (define header-height 90)
(define time-label-x 710) (define header-title-size 36)
(define time-label-y 300) (define header-title-x 250)
(define time-value-x 800) (define header-title-y 25)
(define time-value-y 400)
;; Separator line between play field and UI ;; Score display (left side of header)
(define separator-x 670) (define score-label-size 20)
(define separator-width 24) (define score-label-x 20)
(define score-label-y 25)
(define score-value-size 32)
(define score-value-x 20)
(define score-value-y 50)
;; Key UI position (next to score) ;; Key UI indicator position
(define key-ui-x 20) (define key-ui-x 600)
(define key-ui-y 35))) (define key-ui-y 30)
;; Sidebar (right of maze)
(define sidebar-x 672)
(define sidebar-width 4)
;; Time display (right sidebar area)
(define time-label-size 20)
(define time-label-x 700)
(define time-label-y 200)
(define time-value-size 40)
(define time-value-x 710)
(define time-value-y 240)
;; Game over overlay
(define game-over-text-size 48)
(define game-over-text-x 180)
(define game-over-text-y 380)
;; Pause overlay
(define pause-text-size 48)
(define pause-text-x 200)
(define pause-text-y 400)))

View File

@@ -11,6 +11,7 @@
(prefix (pacman-project tests test-position) position:) (prefix (pacman-project tests test-position) position:)
(prefix (pacman-project tests test-maze) maze:) (prefix (pacman-project tests test-maze) maze:)
(prefix (pacman-project tests test-pacman) pacman:) (prefix (pacman-project tests test-pacman) pacman:)
(prefix (pacman-project tests test-ghost) ghost:)
(prefix (pacman-project tests test-score) score:) (prefix (pacman-project tests test-score) score:)
(prefix (pacman-project tests test-timer) timer:)) (prefix (pacman-project tests test-timer) timer:))
@@ -18,6 +19,7 @@
(position:test) (position:test)
(maze:test) (maze:test)
(pacman:test) (pacman:test)
(ghost:test)
(score:test) (score:test)
(timer:test)) (timer:test))

View File

@@ -0,0 +1,97 @@
#lang r7rs
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Tests: Ghost ADT ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-library (pacman-project tests test-ghost)
(import (scheme base)
(pp1 tests)
(pacman-project adt ghost))
(export test)
(begin
;; Test creation and initial state
(define (test-creation)
(define g (make-ghost 'blinky 11 14 0 27 0))
(define pos (g 'position))
(check-eq? (pos 'row) 11 "Start row should be 11")
(check-eq? (pos 'col) 14 "Start col should be 14")
(check-eq? (g 'type) 'blinky "Type should be blinky")
(check-eq? (g 'direction) 'left "Default direction should be left"))
;; Test that exit-delay 0 means ghost starts outside (scatter mode)
(define (test-blinky-starts-active)
(define g (make-ghost 'blinky 11 14 0 27 0))
(check-eq? (g 'in-house?) #f "Blinky should start outside")
(check-eq? (g 'mode) 'scatter "Blinky should start in scatter"))
;; Test that non-zero exit-delay means ghost starts in house
(define (test-pinky-starts-in-house)
(define g (make-ghost 'pinky 14 13 0 0 2000))
(check-eq? (g 'in-house?) #t "Pinky should start in house")
(check-eq? (g 'mode) 'in-house "Pinky mode should be in-house"))
;; Test direction change
(define (test-direction)
(define g (make-ghost 'blinky 11 14 0 27 0))
((g 'direction!) 'right)
(check-eq? (g 'direction) 'right "Direction should be right"))
;; Test mode change and reverse queuing
(define (test-mode-change-queues-reverse)
(define g (make-ghost 'blinky 11 14 0 27 0))
((g 'mode!) 'chase)
(check-eq? (g 'mode) 'chase "Mode should be chase")
(check-eq? ((g 'consume-reverse!)) #t "Reverse should be queued after mode change"))
;; Test reverse consumed only once
(define (test-consume-reverse-once)
(define g (make-ghost 'blinky 11 14 0 27 0))
((g 'mode!) 'chase)
((g 'consume-reverse!))
(check-eq? ((g 'consume-reverse!)) #f "Reverse should be consumed after first call"))
;; Test move
(define (test-move)
(define g (make-ghost 'blinky 11 14 0 27 0))
((g 'move!) 0 -1)
(define pos (g 'position))
(check-eq? (pos 'col) 13 "Col should be 13 after move left"))
;; Test scatter target
(define (test-scatter-target)
(define g (make-ghost 'blinky 11 14 0 27 0))
(define st (g 'scatter-target))
(check-eq? (st 'row) 0 "Scatter target row should be 0")
(check-eq? (st 'col) 27 "Scatter target col should be 27"))
;; Test house timer expiry
(define (test-house-exit)
(define g (make-ghost 'pinky 14 13 0 0 2000))
((g 'update-house-timer!) 1000)
(check-eq? (g 'in-house?) #t "Still in house after 1000ms")
((g 'update-house-timer!) 1000)
(check-eq? (g 'in-house?) #f "Should exit house after 2000ms")
(check-eq? (g 'mode) 'scatter "Should be in scatter after exiting"))
;; Test movement timer
(define (test-movement-timer)
(define g (make-ghost 'blinky 11 14 0 27 0))
(check-eq? ((g 'advance-movement-timer!) 100) 100 "Timer should be 100")
(check-eq? ((g 'advance-movement-timer!) 120) 220 "Timer should be 220")
((g 'reset-movement-timer!))
(check-eq? (g 'movement-timer) 0 "Timer should reset to 0"))
(define (test)
(run-test test-creation "Ghost: creation and initial state")
(run-test test-blinky-starts-active "Ghost: Blinky starts active")
(run-test test-pinky-starts-in-house "Ghost: Pinky starts in house")
(run-test test-direction "Ghost: direction change")
(run-test test-mode-change-queues-reverse "Ghost: mode change queues reverse")
(run-test test-consume-reverse-once "Ghost: consume reverse only once")
(run-test test-move "Ghost: move")
(run-test test-scatter-target "Ghost: scatter target")
(run-test test-house-exit "Ghost: house timer exit")
(run-test test-movement-timer "Ghost: movement timer"))))