Add ghost CPUs with original Pac-Man AI targeting
Implement four ghosts (Blinky, Pinky, Inky, Clyde) with authentic Pac-Man AI: Blinky chases directly, Pinky targets 2 tiles ahead (with original up-direction bug), Inky uses vector doubling from Blinky, Clyde switches to scatter within 8-tile radius. Includes chase/scatter mode cycling, ghost house exit with staggered delays, directional sprite rendering with animation, and ghost-pacman collision detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,13 @@
|
||||
;; Level ADT ;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Contains all game logic: automatic Pac-Man movement, collision detection,
|
||||
;; coin/key pickup, door opening, teleportation, pause, and time management.
|
||||
;; Pac-Man moves automatically in its current direction. Arrow keys queue a
|
||||
;; desired turn direction, which is applied at the next movement tick if the
|
||||
;; path is clear. Contains NO graphics code.
|
||||
;; Contains all game logic: automatic Pac-Man movement, ghost AI, collision
|
||||
;; detection, coin/key pickup, door opening, teleportation, pause, and time
|
||||
;; management. Pac-Man moves automatically in its current direction. Arrow
|
||||
;; keys queue a desired turn direction, which is applied at the next movement
|
||||
;; 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)
|
||||
(import (scheme base)
|
||||
@@ -16,6 +18,7 @@
|
||||
(pacman-project adt position)
|
||||
(pacman-project adt maze)
|
||||
(pacman-project adt pacman)
|
||||
(pacman-project adt ghost)
|
||||
(pacman-project adt key)
|
||||
(pacman-project adt score)
|
||||
(pacman-project adt timer))
|
||||
@@ -24,7 +27,7 @@
|
||||
(begin
|
||||
|
||||
;; 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)
|
||||
(let ((maze (make-maze))
|
||||
(pacman (make-pacman 5 2))
|
||||
@@ -32,20 +35,68 @@
|
||||
(score (make-score))
|
||||
(timer (make-timer))
|
||||
(paused? #f)
|
||||
(game-over? #f)
|
||||
(queued-direction #f)
|
||||
(movement-timer 0)
|
||||
(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.
|
||||
(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->delta :: symbol -> (number . number)
|
||||
;; Converts a direction to a (delta-row . delta-col) pair.
|
||||
(define (direction->delta direction)
|
||||
(cond ((eq? direction 'right) (cons 0 1))
|
||||
((eq? direction 'left) (cons 0 -1))
|
||||
@@ -53,29 +104,263 @@
|
||||
((eq? direction 'down) (cons 1 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
|
||||
;; Checks if Pac-Man can move in the given direction (no wall or
|
||||
;; locked door blocking the way).
|
||||
;; Checks if Pac-Man can move in the given direction.
|
||||
(define (can-move? direction)
|
||||
(let* ((delta (direction->delta direction))
|
||||
(current-pos (pacman 'position))
|
||||
(next-row (+ (current-pos 'row) (car delta)))
|
||||
(next-col (+ (current-pos 'col) (cdr delta))))
|
||||
(cond
|
||||
;; Teleportation tunnels are always passable.
|
||||
((or (< next-col 0) (>= next-col (maze 'cols))) #t)
|
||||
;; Walls block.
|
||||
(((maze 'wall?) next-row next-col) #f)
|
||||
;; Doors block unless the key has been taken.
|
||||
(((maze 'door?) next-row next-col) (key 'taken?))
|
||||
(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)))
|
||||
((>= next-col (maze 'cols))
|
||||
((pos 'col!) 0))
|
||||
(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)
|
||||
(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
|
||||
(when (not (ghost 'in-house?))
|
||||
(let ((mt ((ghost 'advance-movement-timer!) delta-time)))
|
||||
(when (>= mt ghost-speed-ms)
|
||||
(advance-ghost! ghost)
|
||||
((ghost 'reset-movement-timer!))))))
|
||||
ghosts))
|
||||
|
||||
;;
|
||||
;; Coin logic
|
||||
;;
|
||||
|
||||
;; eat-coin! :: number, number -> /
|
||||
;; Removes the coin at the cell and updates score/time.
|
||||
(define (eat-coin! row col)
|
||||
((maze 'cell-set!) row col cell-type-empty)
|
||||
((score 'increase!))
|
||||
@@ -87,7 +372,6 @@
|
||||
;;
|
||||
|
||||
;; pick-up-key! :: number, number -> /
|
||||
;; Picks up the key and clears the cell.
|
||||
(define (pick-up-key! row col)
|
||||
((maze 'cell-set!) row col cell-type-empty)
|
||||
((key 'take!))
|
||||
@@ -98,7 +382,6 @@
|
||||
;;
|
||||
|
||||
;; teleport-horizontal! :: number, number -> /
|
||||
;; Teleports Pac-Man to the other side of the maze.
|
||||
(define (teleport-horizontal! row col)
|
||||
(let ((pac-pos (pacman 'position)))
|
||||
(cond ((< col 0)
|
||||
@@ -113,8 +396,6 @@
|
||||
;;
|
||||
|
||||
;; move-pacman! :: symbol -> /
|
||||
;; Moves Pac-Man one step in the given direction, handling collisions,
|
||||
;; teleportation, and item pickup.
|
||||
(define (move-pacman! direction)
|
||||
(let* ((delta (direction->delta direction))
|
||||
(delta-row (car delta))
|
||||
@@ -123,25 +404,20 @@
|
||||
(next-row (+ (current-pos 'row) delta-row))
|
||||
(next-col (+ (current-pos 'col) delta-col)))
|
||||
|
||||
;; Update facing direction for the draw layer.
|
||||
((pacman 'direction!) direction)
|
||||
|
||||
(cond
|
||||
;; Teleportation: outside grid horizontally.
|
||||
((or (< next-col 0) (>= next-col (maze 'cols)))
|
||||
(teleport-horizontal! next-row next-col))
|
||||
|
||||
;; Door: open it if key has been taken.
|
||||
(((maze 'door?) next-row next-col)
|
||||
(when (key 'taken?)
|
||||
((maze 'remove-door!) next-row next-col)
|
||||
(on-maze-changed!)))
|
||||
(on-maze-changed!)))
|
||||
|
||||
;; Normal movement: only if not a wall.
|
||||
(else
|
||||
(when (not ((maze 'wall?) next-row next-col))
|
||||
((pacman 'move!) delta-row delta-col)
|
||||
;; Check what's at the new position.
|
||||
(cond
|
||||
(((maze 'key?) next-row next-col)
|
||||
(pick-up-key! next-row next-col))
|
||||
@@ -149,17 +425,13 @@
|
||||
(eat-coin! next-row next-col))))))))
|
||||
|
||||
;; advance-pacman! :: -> /
|
||||
;; Called every movement tick. Tries the queued direction first; if
|
||||
;; that path is blocked, continues in the current direction.
|
||||
(define (advance-pacman!)
|
||||
(when (not ((timer 'time-up?)))
|
||||
(when (not (or ((timer 'time-up?)) game-over?))
|
||||
(let ((current-dir (pacman 'direction)))
|
||||
;; Try the queued direction first.
|
||||
(cond
|
||||
((and queued-direction (can-move? queued-direction))
|
||||
(move-pacman! queued-direction)
|
||||
(set! queued-direction #f))
|
||||
;; Otherwise keep moving in the current direction.
|
||||
((can-move? current-dir)
|
||||
(move-pacman! current-dir))))))
|
||||
|
||||
@@ -167,7 +439,6 @@
|
||||
;; Pause logic
|
||||
;;
|
||||
|
||||
;; toggle-pause! :: -> /
|
||||
(define (toggle-pause!)
|
||||
(set! paused? (not paused?)))
|
||||
|
||||
@@ -176,7 +447,6 @@
|
||||
;;
|
||||
|
||||
;; key-press! :: symbol -> /
|
||||
;; Processes a key press. Arrow keys queue a desired direction.
|
||||
(define (key-press! pressed-key)
|
||||
(cond
|
||||
((eq? pressed-key 'escape) (toggle-pause!))
|
||||
@@ -188,45 +458,55 @@
|
||||
((eq? pressed-key 'down) (set! queued-direction 'down))))))
|
||||
|
||||
;;
|
||||
;; Update (game loop function)
|
||||
;; Update (game loop)
|
||||
;;
|
||||
|
||||
;; 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)
|
||||
(when (not paused?)
|
||||
(when (not (or paused? game-over?))
|
||||
((timer 'decrease!) delta-time)
|
||||
;; Pac-Man movement
|
||||
(set! movement-timer (+ movement-timer delta-time))
|
||||
(when (>= movement-timer pacman-speed-ms)
|
||||
(advance-pacman!)
|
||||
(set! movement-timer 0))))
|
||||
(set! movement-timer 0)
|
||||
(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
|
||||
;;
|
||||
|
||||
;; 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)
|
||||
(cond ((eq? msg 'maze) maze)
|
||||
((eq? msg 'pacman) pacman)
|
||||
((eq? msg 'ghosts) ghosts)
|
||||
((eq? msg 'key) key)
|
||||
((eq? msg 'score) score)
|
||||
((eq? msg 'timer) timer)
|
||||
((eq? msg 'paused?) paused?)
|
||||
((eq? msg 'game-over?) game-over?)
|
||||
((eq? msg 'key-press!) key-press!)
|
||||
((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))))
|
||||
|
||||
dispatch-level))))
|
||||
|
||||
Reference in New Issue
Block a user