5 Commits

Author SHA1 Message Date
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
joren
4c98ca61c5 Implement automatic Pac-Man movement with queued direction turning
Pac-Man now moves automatically in its current direction every
pacman-speed-ms (200ms). Arrow keys queue a desired turn direction
instead of moving directly. Each movement tick:

1. Try the queued direction — if passable, turn and move that way
2. Otherwise keep moving in the current direction
3. Stop only when hitting a wall (no direction change)

New internal state:
- queued-direction: the direction the player wants to turn next
- movement-timer: accumulates delta-time, triggers move at interval

New helper:
- can-move?: checks if a direction is passable (no wall/locked door)

Changed behavior:
- key-press! now sets queued-direction instead of calling move-pacman!
- update! now drives movement via advance-pacman! on a timer
- move-pacman! no longer checks time-up? (advance-pacman! handles it)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:15:46 +01:00
joren
39a91a5aa0 Add pacman-speed-ms constant for automatic movement interval
Adds a 200ms movement tick rate, controlling how fast Pac-Man moves
automatically through the maze.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:15:11 +01:00
4 changed files with 192 additions and 82 deletions

View File

@@ -7,6 +7,10 @@
;; All graphics logic is isolated in this ADT. Game logic knows nothing about
;; pixels, windows, or sprites. Grid-to-pixel conversion happens exclusively
;; 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)
(import (scheme base)
@@ -85,6 +89,17 @@
(define ui-tile (make-tile width height))
((ui-layer 'add-drawable!) ui-tile)
;;
;; Change tracking — skip redraws when state hasn't changed
;;
(define cached-score -1)
(define cached-time "")
(define cached-key-taken? #f)
(define key-sprite-swapped? #f)
(define cached-paused? #f)
(define coins-dirty? #t)
;;
;; Coordinate conversion
;;
@@ -102,8 +117,10 @@
;;
;; draw-maze! :: maze -> /
;; Draws all walls and doors.
;; Draws all walls and doors. Called once at startup and after door
;; removal — never per-frame.
(define (draw-maze! maze)
((maze-tile 'clear!))
((maze 'for-each-cell)
(lambda (row col cell-type)
(cond
@@ -123,7 +140,7 @@
"pink"))))))
;; draw-coins! :: maze -> /
;; Draws all coins in the maze.
;; Redraws all coins. Only called when coins-dirty? is true.
(define (draw-coins! maze)
((coins-tile 'clear!))
((maze 'for-each-cell)
@@ -137,15 +154,12 @@
"yellow")))))
;; draw-key! :: key -> /
;; Draws the key at its position, or shows it in UI if taken.
;; Swaps the key sprite once when taken. No-op on subsequent frames.
(define (draw-key! key-obj)
(if (key-obj 'taken?)
(begin
(when (and (key-obj 'taken?) (not key-sprite-swapped?))
((key-layer 'remove-drawable!) key-sprite)
((key-layer 'add-drawable!) key-ui-sprite))
(let ((pos (key-obj 'position)))
((key-sprite 'set-x!) (grid->pixel-x (pos 'col)))
((key-sprite 'set-y!) (grid->pixel-y (pos 'row))))))
((key-layer 'add-drawable!) key-ui-sprite)
(set! key-sprite-swapped? #t)))
;; animate-pacman! :: number -> /
;; Advances the Pac-Man sprite animation based on elapsed time.
@@ -156,26 +170,29 @@
(set! time-since-last-animation 0)))
;; draw-pacman! :: pacman -> /
;; Draws Pac-Man at its current position with correct rotation.
;; Updates Pac-Man position and rotation. Lightweight — just sets
;; properties on an existing sprite.
(define (draw-pacman! pacman)
(let* ((pos (pacman 'position))
(direction (pacman 'direction)))
;; Set position
((pacman-sprite 'set-x!) (grid->pixel-x (pos 'col)))
((pacman-sprite 'set-y!) (grid->pixel-y (pos 'row)))
;; Set rotation based on direction
(cond ((eq? direction 'right) ((pacman-sprite 'rotate!) rotation-right))
((eq? direction 'left) ((pacman-sprite 'rotate!) rotation-left))
((eq? direction 'up) ((pacman-sprite 'rotate!) rotation-up))
((eq? direction 'down) ((pacman-sprite 'rotate!) rotation-down)))))
;; draw-ui! :: score, timer -> /
;; Draws the score and time limit on screen.
;; Redraws score and time only when their values have changed.
(define (draw-ui! score timer)
(let ((current-score (score 'points))
(current-time ((timer 'format-time))))
(when (or (not (= current-score cached-score))
(not (string=? current-time cached-time)))
((ui-tile 'clear!))
;; Score
((ui-tile 'draw-text!)
(number->string (score 'points))
(number->string current-score)
score-text-size score-text-x score-text-y "white")
;; Separator line
((ui-tile 'draw-rectangle!)
@@ -184,30 +201,52 @@
((ui-tile 'draw-text!)
"Time remaining:" time-text-size time-label-x time-label-y "white")
((ui-tile 'draw-text!)
((timer 'format-time))
score-text-size time-value-x time-value-y "white"))
current-time
score-text-size time-value-x time-value-y "white")
;; Update cache
(set! cached-score current-score)
(set! cached-time current-time))))
;; draw-pause! :: boolean -> /
;; Shows or hides the pause screen.
;; Only redraws when pause state actually changes.
(define (draw-pause! paused?)
(when (not (eq? paused? cached-paused?))
((pause-layer 'empty!))
(when paused?
(let ((pause-tile (make-tile width height)))
((pause-layer 'add-drawable!) pause-tile)
((pause-tile 'draw-rectangle!) 0 90 670 height "black")
((pause-tile 'draw-text!) "Game Paused" 40 200 400 "red"))))
((pause-tile 'draw-text!) "Game Paused" 40 200 400 "red")))
(set! cached-paused? paused?)))
;; mark-coins-dirty! :: -> /
;; Called by the game when a coin is eaten, so coins are redrawn
;; next frame.
(define (mark-coins-dirty!)
(set! coins-dirty? #t))
;; mark-maze-dirty! :: -> /
;; Called by the game when a door is removed, so the maze is
;; redrawn next frame.
(define (mark-maze-dirty!)
(set! coins-dirty? #t))
;;
;; Main draw function
;;
;; draw-game! :: game -> /
;; Draws the full game (registered as draw callback).
;; Draws the full game. Only redraws changed elements.
(define (draw-game! game)
(let ((level (game 'level)))
;; Always update (lightweight sprite property sets)
(draw-pacman! (level 'pacman))
;; Only redraw when dirty / changed
(draw-key! (level 'key))
(when coins-dirty?
(draw-coins! (level 'maze))
(draw-maze! (level 'maze))
(set! coins-dirty? #f))
(draw-ui! (level 'score) (level 'timer))
(draw-pause! (level 'paused?))))
@@ -226,9 +265,10 @@
;; start-drawing! :: game -> /
;; Starts drawing by setting the draw callback.
(define (start-drawing! game)
;; Initial maze and coins draw (one-time)
;; Initial draw (one-time)
(draw-maze! ((game 'level) 'maze))
(draw-coins! ((game 'level) 'maze))
(set! coins-dirty? #f)
((window 'set-draw-callback!)
(lambda () (draw-game! game))))
@@ -241,6 +281,8 @@
((eq? msg 'set-key-callback!) set-key-callback!)
((eq? msg 'start-drawing!) start-drawing!)
((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))))
dispatch-draw))))

View File

@@ -34,8 +34,12 @@
((draw 'animate-pacman!) delta-time))
;; start! :: -> /
;; Starts the game by registering all callbacks.
;; Starts the game by registering all callbacks and change listeners.
(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-key-callback!) key-handler)
((draw 'start-drawing!) dispatch-game))

View File

@@ -4,9 +4,11 @@
;; Level ADT ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Contains all game logic: Pac-Man movement, collision detection, coin/key
;; pickup, door opening, teleportation, pause, and time management.
;; Contains NO graphics code.
;; 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.
(define-library (pacman-project adt level)
(import (scheme base)
@@ -29,7 +31,11 @@
(key #f)
(score (make-score))
(timer (make-timer))
(paused? #f))
(paused? #f)
(queued-direction #f)
(movement-timer 0)
(on-coins-changed! (lambda () #f))
(on-maze-changed! (lambda () #f)))
;; Initialize key after maze is created.
(set! key (make-key maze))
@@ -47,6 +53,23 @@
((eq? direction 'down) (cons 1 0))
(else (cons 0 0))))
;; can-move? :: symbol -> boolean
;; Checks if Pac-Man can move in the given direction (no wall or
;; locked door blocking the way).
(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))))
;;
;; Coin logic
;;
@@ -56,7 +79,8 @@
(define (eat-coin! row col)
((maze 'cell-set!) row col cell-type-empty)
((score 'increase!))
((timer 'increase!)))
((timer 'increase!))
(on-coins-changed!))
;;
;; Key logic
@@ -66,7 +90,8 @@
;; Picks up the key and clears the cell.
(define (pick-up-key! row col)
((maze 'cell-set!) row col cell-type-empty)
((key 'take!)))
((key 'take!))
(on-coins-changed!))
;;
;; Teleportation logic
@@ -88,9 +113,9 @@
;;
;; move-pacman! :: symbol -> /
;; Moves Pac-Man in the given direction with all game rules.
;; Moves Pac-Man one step in the given direction, handling collisions,
;; teleportation, and item pickup.
(define (move-pacman! direction)
(when (not ((timer 'time-up?)))
(let* ((delta (direction->delta direction))
(delta-row (car delta))
(delta-col (cdr delta))
@@ -98,7 +123,7 @@
(next-row (+ (current-pos 'row) delta-row))
(next-col (+ (current-pos 'col) delta-col)))
;; Update direction for the draw layer.
;; Update facing direction for the draw layer.
((pacman 'direction!) direction)
(cond
@@ -106,10 +131,11 @@
((or (< next-col 0) (>= next-col (maze 'cols)))
(teleport-horizontal! next-row next-col))
;; Door: only open if key has been taken.
;; Door: open it if key has been taken.
(((maze 'door?) next-row next-col)
(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
@@ -120,7 +146,22 @@
(((maze 'key?) next-row next-col)
(pick-up-key! next-row next-col))
(((maze 'coin?) next-row next-col)
(eat-coin! next-row next-col)))))))))
(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?)))
(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))))))
;;
;; Pause logic
@@ -135,31 +176,46 @@
;;
;; key-press! :: symbol -> /
;; Processes a key press.
;; Processes a key press. Arrow keys queue a desired direction.
(define (key-press! pressed-key)
(cond
((eq? pressed-key 'escape) (toggle-pause!))
((not paused?)
(cond
((eq? pressed-key 'right) (move-pacman! 'right))
((eq? pressed-key 'left) (move-pacman! 'left))
((eq? pressed-key 'up) (move-pacman! 'up))
((eq? pressed-key 'down) (move-pacman! 'down))))))
((eq? pressed-key 'right) (set! queued-direction 'right))
((eq? pressed-key 'left) (set! queued-direction 'left))
((eq? pressed-key 'up) (set! queued-direction 'up))
((eq? pressed-key 'down) (set! queued-direction 'down))))))
;;
;; Update (game loop function)
;;
;; update! :: number -> /
;; Called each frame with elapsed milliseconds.
;; 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?)
((timer 'decrease!) delta-time)))
((timer 'decrease!) delta-time)
(set! movement-timer (+ movement-timer delta-time))
(when (>= movement-timer pacman-speed-ms)
(advance-pacman!)
(set! movement-timer 0))))
;;
;; 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)
@@ -169,6 +225,8 @@
((eq? msg 'paused?) paused?)
((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!)
(else (error "Level ADT -- Unknown message:" msg))))
dispatch-level))))

View File

@@ -33,6 +33,9 @@
sprite-scale-key
sprite-scale-key-ui
;; Movement
pacman-speed-ms
;; Animation
animation-interval-ms
@@ -93,6 +96,9 @@
(define sprite-scale-key 1.5)
(define sprite-scale-key-ui 3)
;; Movement speed: time between automatic movement ticks
(define pacman-speed-ms 200)
;; Animation timing
(define animation-interval-ms 100)