6 Commits

Author SHA1 Message Date
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
4 changed files with 253 additions and 88 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)
@@ -19,23 +23,38 @@
;; make-draw :: number, number -> draw
;; Creates the draw object that handles all rendering.
(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)
;;
(define header-layer ((window 'new-layer!)))
(define maze-layer ((window 'new-layer!)))
(define coins-layer ((window 'new-layer!)))
(define key-layer ((window 'new-layer!)))
(define pacman-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! :: -> /
;; Draws the static header with the PAC-MAN title.
(define (draw-header!)
((header-tile 'draw-rectangle!) 0 0 width header-height color-header-bg)
((header-tile 'draw-text!)
"PAC-MAN" header-title-size header-title-x header-title-y color-title))
;;
;; Maze tile
;;
(define maze-tile (make-tile width height))
@@ -56,7 +75,7 @@
((key-sprite 'set-scale!) sprite-scale-key)
((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"))
((key-ui-sprite 'set-scale!) sprite-scale-key-ui)
((key-ui-sprite 'set-x!) key-ui-x)
@@ -79,12 +98,23 @@
(define time-since-last-animation 0)
;;
;; UI tiles
;; UI tile (score + time — redrawn on change)
;;
(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 key-sprite-swapped? #f)
(define cached-paused? #f)
(define cached-time-up? #f)
(define coins-dirty? #t)
;;
;; Coordinate conversion
;;
@@ -104,6 +134,7 @@
;; draw-maze! :: maze -> /
;; Draws all walls and doors.
(define (draw-maze! maze)
((maze-tile 'clear!))
((maze 'for-each-cell)
(lambda (row col cell-type)
(cond
@@ -113,39 +144,43 @@
(grid->pixel-y row)
(- cell-size-px maze-wall-shrink)
(- cell-size-px maze-wall-shrink)
"blue"))
color-wall))
((= cell-type cell-type-door)
((maze-tile 'draw-rectangle!)
(grid->pixel-x col)
(grid->pixel-y row)
(- cell-size-px maze-wall-shrink)
(- cell-size-px maze-wall-shrink)
"pink"))))))
color-door))))))
;; draw-coins! :: maze -> /
;; Draws all coins in the maze.
;; Redraws all coins as small round dots.
(define (draw-coins! maze)
((coins-tile 'clear!))
((maze 'for-each-cell)
(lambda (row col cell-type)
(when (= cell-type cell-type-coin)
((coins-tile 'draw-rectangle!)
((coins-tile 'draw-ellipse!)
(+ (grid->pixel-x col) coin-inset)
(+ (grid->pixel-y row) coin-inset)
(- cell-size-px (* 2 coin-inset) 6)
(- cell-size-px (* 2 coin-inset) 6)
"yellow")))))
coin-size
coin-size
color-coin)))))
;; init-key-position! :: key -> /
;; Sets the key sprite to its grid position. Called once at startup.
(define (init-key-position! key-obj)
(let ((pos (key-obj 'position)))
((key-sprite 'set-x!) (grid->pixel-x (pos 'col)))
((key-sprite 'set-y!) (grid->pixel-y (pos 'row)))))
;; draw-key! :: key -> /
;; 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
((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))))))
(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 -> /
;; Advances the Pac-Man sprite animation based on elapsed time.
@@ -156,60 +191,102 @@
(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.
(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)
((ui-tile 'clear!))
;; Score
((ui-tile 'draw-text!)
(number->string (score 'points))
score-text-size score-text-x score-text-y "white")
;; Separator line
((ui-tile 'draw-rectangle!)
separator-x 0 separator-width height "white")
;; Time limit
((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"))
(let ((current-score (score 'points))
(current-time ((timer 'format-time))))
(when (or (not (= current-score cached-score))
(not (string=? current-time cached-time)))
((ui-tile 'clear!))
;; Score label
((ui-tile 'draw-text!)
"SCORE" score-label-size score-label-x score-label-y color-text)
;; Score value
((ui-tile 'draw-text!)
(number->string current-score)
score-value-size score-value-x score-value-y color-text)
;; Sidebar separator
((ui-tile 'draw-rectangle!)
sidebar-x 0 sidebar-width height color-wall)
;; Time label
((ui-tile 'draw-text!)
"TIME" time-label-size time-label-x time-label-y color-text)
;; Time value
((ui-tile 'draw-text!)
current-time
time-value-size time-value-x time-value-y color-text)
;; Update cache
(set! cached-score current-score)
(set! cached-time current-time))))
;; draw-game-over! :: boolean -> /
;; Shows GAME OVER when time is up.
(define (draw-game-over! time-up?)
(when (and time-up? (not cached-time-up?))
(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-time-up? #t)))
;; draw-pause! :: boolean -> /
;; Shows or hides the pause screen.
;; Only redraws when pause state actually changes.
(define (draw-pause! 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"))))
(when (not (eq? paused? cached-paused?))
((overlay-layer 'empty!))
(when paused?
(let ((overlay-tile (make-tile width height)))
((overlay-layer 'add-drawable!) overlay-tile)
((overlay-tile 'draw-rectangle!)
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
;;
;; 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)))
(let* ((level (game 'level))
(timer (level 'timer)))
;; Always update (lightweight sprite property sets)
(draw-pacman! (level 'pacman))
;; Only redraw when dirty / changed
(draw-key! (level 'key))
(draw-coins! (level 'maze))
(draw-ui! (level 'score) (level 'timer))
(draw-pause! (level 'paused?))))
(when coins-dirty?
(draw-coins! (level 'maze))
(draw-maze! (level 'maze))
(set! coins-dirty? #f))
(draw-ui! (level 'score) timer)
(draw-pause! (level 'paused?))
(draw-game-over! ((timer 'time-up?)))))
;;
;; Callback registration
@@ -226,11 +303,16 @@
;; start-drawing! :: game -> /
;; Starts drawing by setting the draw callback.
(define (start-drawing! game)
;; Initial maze and coins draw (one-time)
(draw-maze! ((game 'level) 'maze))
(draw-coins! ((game 'level) 'maze))
((window 'set-draw-callback!)
(lambda () (draw-game! game))))
(let ((level (game 'level)))
;; Static elements (drawn once)
(draw-header!)
(draw-maze! (level 'maze))
(draw-coins! (level 'maze))
(init-key-position! (level 'key))
(set! coins-dirty? #f)
;; Register draw callback
((window 'set-draw-callback!)
(lambda () (draw-game! game)))))
;;
;; Dispatch
@@ -241,6 +323,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

@@ -33,7 +33,9 @@
(timer (make-timer))
(paused? #f)
(queued-direction #f)
(movement-timer 0))
(movement-timer 0)
(on-coins-changed! (lambda () #f))
(on-maze-changed! (lambda () #f)))
;; Initialize key after maze is created.
(set! key (make-key maze))
@@ -77,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
@@ -87,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
@@ -130,7 +134,8 @@
;; 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
@@ -201,6 +206,16 @@
;; 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)
@@ -210,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

@@ -27,6 +27,7 @@
;; Coin rendering
coin-inset
coin-size
;; Sprites
sprite-scale-pacman
@@ -56,19 +57,45 @@
rotation-up
rotation-down
;; UI positions
score-text-size
score-text-x
score-text-y
time-text-size
;; Colors
color-background
color-wall
color-door
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-y
time-value-size
time-value-x
time-value-y
separator-x
separator-width
key-ui-x
key-ui-y)
game-over-text-size
game-over-text-x
game-over-text-y
pause-text-size
pause-text-x
pause-text-y)
(begin
@@ -88,8 +115,9 @@
(define cell-type-key 3)
(define cell-type-door 4)
;; Coin rendering: inset in pixels from cell edge
(define coin-inset 7)
;; Coin rendering
(define coin-inset 9)
(define coin-size 6)
;; Sprite scale factors
(define sprite-scale-pacman 1.5)
@@ -119,22 +147,54 @@
(define rotation-up 90)
(define rotation-down -90)
;; UI positions for score display
(define score-text-size 40)
(define score-text-x 560)
(define score-text-y 20)
;; Colors — arcade-style palette (standard Racket color names only)
(define color-background "black")
(define color-wall "medium blue")
(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)
(define time-text-size 35)
(define time-label-x 710)
(define time-label-y 300)
(define time-value-x 800)
(define time-value-y 400)
;; UI layout — header bar at the top
(define header-height 90)
(define header-title-size 36)
(define header-title-x 250)
(define header-title-y 25)
;; Separator line between play field and UI
(define separator-x 670)
(define separator-width 24)
;; Score display (left side of header)
(define score-label-size 20)
(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)
(define key-ui-x 20)
(define key-ui-y 35)))
;; Key UI indicator position
(define key-ui-x 600)
(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)))