8 Commits

Author SHA1 Message Date
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
7 changed files with 950 additions and 149 deletions

View File

@@ -23,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))
@@ -60,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
;; ;;
@@ -83,7 +179,7 @@
(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))
@@ -95,30 +191,16 @@
(define cached-score -1) (define cached-score -1)
(define cached-time "") (define cached-time "")
(define cached-key-taken? #f)
(define key-sprite-swapped? #f) (define key-sprite-swapped? #f)
(define cached-paused? #f) (define cached-paused? #f)
(define cached-game-over? #f)
(define coins-dirty? #t) (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 functions
;; ;;
;; draw-maze! :: maze -> / ;; draw-maze! :: maze -> /
;; Draws all walls and doors. Called once at startup and after door
;; removal — never per-frame.
(define (draw-maze! maze) (define (draw-maze! maze)
((maze-tile 'clear!)) ((maze-tile 'clear!))
((maze 'for-each-cell) ((maze 'for-each-cell)
@@ -130,31 +212,35 @@
(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 -> /
;; Redraws all coins. Only called when coins-dirty? is true.
(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)))))
;; init-key-position! :: key -> /
(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 -> / ;; draw-key! :: key -> /
;; Swaps the key sprite once when taken. No-op on subsequent frames.
(define (draw-key! key-obj) (define (draw-key! key-obj)
(when (and (key-obj 'taken?) (not key-sprite-swapped?)) (when (and (key-obj 'taken?) (not key-sprite-swapped?))
((key-layer 'remove-drawable!) key-sprite) ((key-layer 'remove-drawable!) key-sprite)
@@ -162,72 +248,111 @@
(set! key-sprite-swapped? #t))) (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
;; Updates Pac-Man position and rotation. Lightweight — just sets ;; Linear interpolation between a and b by factor t (0..1).
;; properties on an existing sprite. (define (lerp a b t)
(define (draw-pacman! pacman) (+ 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)))
((pacman-sprite 'set-x!) (grid->pixel-x (pos 'col))) ((pacman-sprite 'set-x!) (grid->pixel-x render-col))
((pacman-sprite 'set-y!) (grid->pixel-y (pos 'row))) ((pacman-sprite 'set-y!) (grid->pixel-y render-row))
(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 -> /
;; Redraws score and time only when their values have changed.
(define (draw-ui! score timer) (define (draw-ui! score timer)
(let ((current-score (score 'points)) (let ((current-score (score 'points))
(current-time ((timer 'format-time)))) (current-time ((timer 'format-time))))
(when (or (not (= current-score cached-score)) (when (or (not (= current-score cached-score))
(not (string=? current-time cached-time))) (not (string=? current-time cached-time)))
((ui-tile 'clear!)) ((ui-tile 'clear!))
;; Score ((ui-tile 'draw-text!)
"SCORE" score-label-size score-label-x score-label-y color-text)
((ui-tile 'draw-text!) ((ui-tile 'draw-text!)
(number->string current-score) (number->string current-score)
score-text-size score-text-x score-text-y "white") score-value-size score-value-x score-value-y color-text)
;; Separator line
((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!)
current-time current-time
score-text-size time-value-x time-value-y "white") time-value-size time-value-x time-value-y color-text)
;; Update cache
(set! cached-score current-score) (set! cached-score current-score)
(set! cached-time current-time)))) (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 -> /
;; Only redraws when pause state actually changes.
(define (draw-pause! paused?) (define (draw-pause! paused?)
(when (not (eq? paused? cached-paused?)) (when (not (eq? paused? cached-paused?))
((pause-layer 'empty!)) ((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?))) (set! cached-paused? paused?)))
;; mark-coins-dirty! :: -> / ;; mark-coins-dirty! :: -> /
;; Called by the game when a coin is eaten, so coins are redrawn
;; next frame.
(define (mark-coins-dirty!) (define (mark-coins-dirty!)
(set! coins-dirty? #t)) (set! coins-dirty? #t))
;; mark-maze-dirty! :: -> / ;; mark-maze-dirty! :: -> /
;; Called by the game when a door is removed, so the maze is
;; redrawn next frame.
(define (mark-maze-dirty!) (define (mark-maze-dirty!)
(set! coins-dirty? #t)) (set! coins-dirty? #t))
@@ -236,41 +361,47 @@
;; ;;
;; draw-game! :: game -> / ;; draw-game! :: game -> /
;; Draws the full game. Only redraws changed elements.
(define (draw-game! game) (define (draw-game! game)
(let ((level (game 'level))) (let* ((level (game 'level))
(timer (level 'timer))
(pac-progress (/ (level 'pacman-movement-timer) pacman-speed-ms)))
;; Always update (lightweight sprite property sets) ;; Always update (lightweight sprite property sets)
(draw-pacman! (level 'pacman)) (draw-pacman! (level 'pacman) pac-progress)
(draw-ghosts! (level 'ghosts))
;; Only redraw when dirty / changed ;; Only redraw when dirty / changed
(draw-key! (level 'key)) (draw-key! (level 'key))
(when coins-dirty? (when coins-dirty?
(draw-coins! (level 'maze)) (draw-coins! (level 'maze))
(draw-maze! (level 'maze)) (draw-maze! (level 'maze))
(set! coins-dirty? #f)) (set! coins-dirty? #f))
(draw-ui! (level 'score) (level 'timer)) (draw-ui! (level 'score) timer)
(draw-pause! (level 'paused?)))) (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 draw (one-time) (let ((level (game 'level)))
(draw-maze! ((game 'level) 'maze)) ;; Static elements (drawn once)
(draw-coins! ((game 'level) 'maze)) (draw-header!)
(set! coins-dirty? #f) (draw-maze! (level 'maze))
((window 'set-draw-callback!) (draw-coins! (level 'maze))
(lambda () (draw-game! game)))) (init-key-position! (level 'key))
;; Initialize ghost positions
(draw-ghosts! (level 'ghosts))
(set! coins-dirty? #f)
;; Register draw callback
((window 'set-draw-callback!)
(lambda () (draw-game! game)))))
;; ;;
;; Dispatch ;; Dispatch

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,20 +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-coins-changed! (lambda () #f))
(on-maze-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))
@@ -53,29 +104,266 @@
((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!))
@@ -87,7 +375,6 @@
;; ;;
;; 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!))
@@ -98,7 +385,6 @@
;; ;;
;; 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)
@@ -106,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))
@@ -123,25 +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!))) (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))
@@ -149,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?)))
@@ -176,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!))
@@ -188,45 +469,56 @@
((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
;; ;;
;; set-on-coins-changed! :: (-> /) -> /
;; Registers a callback for when coins change (eaten or key picked up).
(define (set-on-coins-changed! callback)
(set! on-coins-changed! callback))
;; set-on-maze-changed! :: (-> /) -> /
;; Registers a callback for when the maze changes (door removed).
(define (set-on-maze-changed! callback)
(set! on-maze-changed! callback))
(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-coins-changed!) set-on-coins-changed!)
((eq? msg 'set-on-maze-changed!) set-on-maze-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"))))