src/story.lisp @ da570ce8525a

Swap vectors and lists, it feels better
author Steve Losh <steve@stevelosh.com>
date Mon, 09 Jan 2017 19:26:35 +0000
parents cd5ecc8e47cd
children 452b599e2613
(in-package :sand.story)

;;; Basically a Lispy version of Tracery https://github.com/galaxykate/tracery
;;; without the nutty string-parsing stuff.
;;;
;;; (define-symbol name ...expressions...)
;;;
;;; strings evaluate to themselves: "foo bar" -> "foo bar"
;;;
;;; symbols funcall their symbol-function: animal -> "mouse"
;;;
;;; lists evaluate their contents and concatenate them with spaces in between:
;;;     ("foo" animal "bar") -> "foo mouse bar"
;;;
;;; the magic keyword :. inside a list suppresses the space there:
;;;
;;;     ("foo" "bar" :. "baz") -> "foo barbaz"
;;;
;;; vectors evaluate the head and pipe it through all the functions in the tail:
;;;
;;;     #(animal capitalize pos) -> "Mouse's"


;;;; Utils ---------------------------------------------------------------------
(defun emptyp (string)
  (zerop (length string)))

(defun cat (&rest strings)
  "Concatenate `strings` into a string."
  (apply #'concatenate 'string strings))

(defun ch (string index)
  "Return the character of `string` at `index`.  Allows negative indices."
  (if (emptyp string)
    nil
    (aref string (if (minusp index)
                   (+ (length string) index)
                   index))))

(defun chop (string n)
  "Chop `n` characters off the end of `string`"
  (subseq string 0 (max 0 (- (length string) n))))

(defun vowelp (character)
  (ensure-boolean (member character '(#\a #\e #\i #\o #\u))))

(defmacro assert-nonempty (place message)
  `(assert (not (emptyp ,place)) (,place) ,message))


;;;; Guts ---------------------------------------------------------------------
(defun separate (list)
  (-<> list
    (split-sequence:split-sequence :. <>)
    (mapcar (rcurry #'riffle " ") <>)
    (apply #'append <>)))

(defun string-pre (contents)
  (separate contents))

(defun string-post (contents)
  (apply #'cat contents))


(defparameter *combination-pre* #'identity)
(defparameter *combination-post* #'identity)


(defun evaluate-combination (list)
  (-<> list
    (funcall *combination-pre* <>)
    (mapcar #'evaluate-expression <>)
    (funcall *combination-post* <>)))

(defun evaluate-modifiers (vector)
  (reduce (flip #'funcall) vector
          :start 1
          :initial-value (evaluate-expression (aref vector 0))))

(defun evaluate-expression (expr)
  (typecase expr
    ((or string keyword null) expr)
    (symbol (funcall expr))
    (vector (evaluate-modifiers expr))
    (list (if (eq (first expr) 'quote)
            (second expr)
            (evaluate-combination expr)))
    (t expr)))


(defun generate (expression)
  (evaluate-expression expression))

(defun generate-string (expression)
  (let ((*combination-pre* #'string-pre)
        (*combination-post* #'string-post))
    (evaluate-expression expression)))


(defmacro define-symbol (name &rest expressions)
  `(defun ,name ()
     (evaluate-expression
       (random-elt ,(coerce expressions 'vector)))))


;;;; Modifiers ----------------------------------------------------------------
(defun cap (string)
  "Capitalize the first character of `string`."
  (assert-nonempty string "Cannot capitalize an empty string.")
  (string-capitalize string :end 1))

(defun cap-all (string)
  "Capitalize each word of `string`."
  (assert-nonempty string "Cannot capitalize-all an empty string.")
  (string-capitalize string))

(defun q (string)
  "Wrap `string` in quotation marks."
  (cat "\"" string "\""))

(defun a (string)
  "Add an indefinite article (a or an) to the front of `string`."
  (assert-nonempty string "Cannot add an article to an empty string.")
  (cat (if (vowelp (ch string 0))
         "an "
         "a ")
       string))

(defun s (string)
  "Pluralize `string`."
  (assert-nonempty string "Cannot pluralize an empty string.")
  (case (ch string -1)
    (#\y (if (vowelp (ch string -2))
           (cat string "s")
           (cat (chop string 1) "ies")))
    (#\x (cat (chop string 1) "en"))
    ((#\z #\h) (cat (chop string 1) "es"))
    (t (cat string "s"))))

(defun pos (string)
  "Make `string` posessive by adding an apostrophe (and possibly an s)."
  (assert-nonempty string "Cannot make an empty string posessive.")
  (cat string
       (if (eql #\s (ch string -1))
         "'"
         "'s")))


;;;; Example ------------------------------------------------------------------
(define-symbol name
  "arjun"
  "yuuma"
  "jess"
  "bob smith")

(define-symbol nature-noun
  "ocean"
  "mountain"
  "forest"
  "cloud"
  "river"
  "tree"
  "sky"
  "sea"
  "desert")

(define-symbol animal
  "unicorn"
  "raven"
  "turkey"
  "wallaby"
  "sparrow"
  "scorpion"
  "coyote"
  "eagle"
  "owl"
  "lizard"
  "zebra"
  "duck"
  "kitten")

(define-symbol color
  "orange"
  "blue"
  "white"
  "black"
  "grey"
  "purple"
  "indigo"
  "turquoise")

(define-symbol activity
  "running"
  "jumping"
  "flying"
  "carousing")

(define-symbol sentence
  ("The" color animal "of the" nature-noun "is called" #(name cap-all q) :. ".")
  ("The" animal "was" activity "in the" #(nature-noun s) :. ".")
  (#(name cap-all pos) "favorite color is" color :. ".")
  (#(nature-noun cap) "air is fresh.")
  ("The" #(animal s) "were" activity "in the" nature-noun :. "."))


; (generate-string 'sentence)

(define-symbol monster
  :bat
  :kobold
  :goblin)

(define-symbol size
  5 6 7 8 9 10)

(define-symbol money
  (#(100 random) :gold)
  (#(500 random) :silver))

(define-symbol potion-type
  "healing"
  "levitation"
  "detect magic"
  "confusion")

(define-symbol potion-quality
  "strong"
  "weak"
  "small")

(define-symbol potion
  (potion-quality "potion of" potion-type)
  ("potion of" potion-type))

(define-symbol enchant
  "+1"
  "+2"
  "+3")

(define-symbol armor-piece
  "shield"
  "breastplate"
  "suit of chain mail"
  "belt"
  "helmet")

(define-symbol armor
  armor-piece
  (enchant armor-piece))

(define-symbol item%
  armor
  potion)

(defun item ()
  (generate-string #(item% a)))

(define-symbol loot
  money
  item)

(define-symbol room%
  (:size size
   :loot loot
   :monster monster))


; (iterate (repeat 30) (pr (funcall #'generate 'room%)))