Chapter 13: Game Polish
Let's add save/load functionality and other refinements.
Save and Load
We'll serialize game state to a file using Scheme's read/write.
Add to adventure/commands.sgl:
;; Add to exports
(export command-save command-load)
;; Add imports
(import (sigil io))
;; Save game
(define (command-save state)
(call-with-output-file "savegame.dat"
(lambda (port)
;; Save as an association list
(write
(list
(cons 'current-room (game-state-current-room state))
(cons 'inventory (game-state-inventory state))
(cons 'room-items
(map (lambda (r)
(cons (room-id (cdr r))
(room-items (cdr r))))
(game-state-rooms state))))
port)))
(display "Game saved.")
(newline)
state)
;; Load game
(define (command-load state)
(if (not (file-exists? "savegame.dat"))
(begin
(display "No save file found.")
(newline)
state)
(call-with-input-file "savegame.dat"
(lambda (port)
(let ((save-data (read port)))
(if (eof-object? save-data)
(begin
(display "Save file is corrupt.")
(newline)
state)
(let* ((current-room (cdr (assq 'current-room save-data)))
(inventory (cdr (assq 'inventory save-data)))
(room-items (cdr (assq 'room-items save-data)))
;; Rebuild rooms with saved item positions
(new-rooms
(map (lambda (room-pair)
(let* ((rm (cdr room-pair))
(saved-items (cdr (assq (room-id rm) room-items))))
(cons (room-id rm)
(room rm
items: (or saved-items (room-items rm))))))
(game-state-rooms state))))
(display "Game loaded.")
(newline)
(let ((new-state (game-state state
current-room: current-room
inventory: inventory
rooms: new-rooms)))
(command-look new-state)))))))))Add these to the command dispatcher in execute-command:
((string=? verb "save")
(command-save state))
((string=? verb "load")
(command-load state))Improved Room Descriptions
Show exits more clearly. Update command-look:
(define (format-exits exits)
(if (null? exits)
"There are no obvious exits."
(str "Exits: "
(string-join
(map (lambda (e) (symbol->string (car e)))
exits)
", "))))
(define (command-look state)
(let* ((room-id (game-state-current-room state))
(rm (lookup-room state room-id)))
(newline)
(display "=== ")
(display (room-name rm))
(display " ===")
(newline)
(newline)
(display (room-description rm))
(newline)
(newline)
;; Show exits
(display (format-exits (room-exits rm)))
(newline)
;; Show items in room
(let ((items (room-items rm)))
(unless (null? items)
(newline)
(display "You can see:")
(newline)
(for-each
(lambda (item-id)
(let ((it (lookup-item state item-id)))
(display " - ")
(display (item-name it))
(newline)))
items)))
(newline)
state))Command Synonyms
Support more natural language. Update execute-command:
;; Add at the top of the cond
((or (string=? verb "get")
(string=? verb "grab")
(string=? verb "pick"))
(if (pair? args)
(command-take (string->symbol (car args)) state)
(begin
(display "Take what?")
(newline)
state)))
((or (string=? verb "l")
(string=? verb "look"))
(if (pair? args)
(command-examine (string->symbol (car args)) state)
(command-look state)))Brief Mode
Add an option to show short room descriptions on revisits. First, add new fields to game-state in world.sgl:
(define-struct game-state
(current-room)
(inventory default: '())
(rooms)
(items)
(won default: #f)
(visited default: '()) ; List of visited room ids
(brief-mode default: #f)) ; Show brief descriptionsThen update command-go:
(define (command-go direction state)
(let* ((room-id (game-state-current-room state))
(next-room (get-exit-room state room-id direction)))
(if next-room
(let* ((visited (game-state-visited state))
(been-here (memq next-room visited))
(new-state (game-state state
current-room: next-room
visited: (if been-here
visited
(cons next-room visited)))))
(if (and been-here (game-state-brief-mode state))
(command-brief-look new-state)
(command-look new-state)))
(begin
(display "You can't go that way.")
(newline)
state))))
(define (command-brief-look state)
(let* ((room-id (game-state-current-room state))
(rm (lookup-room state room-id)))
(display (room-name rm))
(newline)
state))Colorful Output (Advanced)
If the terminal supports ANSI colors:
(define (color-text text color)
(let ((code (case color
((red) "31")
((green) "32")
((yellow) "33")
((blue) "34")
((magenta) "35")
((cyan) "36")
((white) "37")
(else "0"))))
(str "\x1b[" code "m" text "\x1b[0m")))
;; Usage:
(display (color-text "=== The Kitchen ===" 'cyan))Adding Sound Cues
Print atmospheric text for immersion:
(define (print-atmospheric room-id)
(case room-id
((kitchen)
(display "[A clock ticks softly somewhere.]")
(newline))
((garden)
(display "[Birds chirp in the distance.]")
(newline))
((study)
(display "[Dust motes float in a shaft of light.]")
(newline))))Testing Your Game
Create a test script to verify the game works:
;; test-game.sgl
(import (adventure world)
(adventure commands))
(define (test-walkthrough)
(let* ((s0 (create-world))
(s1 (execute-command (parse-command "e") s0))
(s2 (execute-command (parse-command "s") s1))
(s3 (execute-command (parse-command "take key") s2))
(s4 (execute-command (parse-command "n") s3))
(s5 (execute-command (parse-command "use key") s4)))
(if (eq? (game-state-won s5) #t)
(display "TEST PASSED: Walkthrough completed successfully!")
(display "TEST FAILED: Did not win"))
(newline)))
(test-walkthrough)Final Polish Checklist
Before distributing your game:
- [ ] All rooms are reachable
- [ ] All items can be examined
- [ ] Win condition works
- [ ] Save/load works
- [ ] Help command lists all commands
- [ ] No typos in descriptions
- [ ] Game doesn't crash on bad input
Practice Exercises
- Add a score system
- Add time limits or turn counts
- Create multiple endings
- Add an NPC the player can talk to
What's Next
Time to build a standalone executable!