sigildocs

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 descriptions

Then 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

  1. Add a score system
  2. Add time limits or turn counts
  3. Create multiple endings
  4. Add an NPC the player can talk to

What's Next

Time to build a standalone executable!

Next: Distribution →