silt.lisp @ 39eb27bab17b

Update README
author Steve Losh <steve@stevelosh.com>
date Thu, 02 Mar 2023 20:20:00 -0500
parents 43050007ebb5
children (none)
(in-package :silt)
#+sbcl (require :sb-sprof)

; (declaim (optimize (speed 3) (debug 0) (safety 0)))
; (declaim (optimize (speed 3) (debug 0) (safety 1)))
; (declaim (optimize (speed 1) (debug 1) (safety 1)))


;;;; Data ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(alexandria:define-constant +world-exponent+ 10)
(alexandria:define-constant +world-size+ (expt 2 +world-exponent+))
(defparameter *screen-width* 1)
(defparameter *screen-height* 1)
(defparameter *screen-center-x* 1)
(defparameter *screen-center-y* 1)
(defparameter *view-x* 0)
(defparameter *view-y* 0)
(defparameter *cursor-x* 0)
(defparameter *cursor-y* 0)
(defparameter *paused* nil)
(defparameter *game-log* nil)
(defparameter *population* 0)
(defparameter *tick* 0)
(defparameter *timing* (cons 0 0))
(defparameter *temperature* 0)
(defparameter *frame-skip* 1)
(defparameter *sleep* t)


(deftype world-coordinate ()
  `(integer 0 ,(1- +world-size+)))

(deftype world-array ()
  `(simple-array single-float (,+world-size+ ,+world-size+)))


(defun allocate-heightmap ()
  (make-array (list +world-size+ +world-size+)
    :element-type 'single-float
    :initial-element 0.0
    :adjustable nil))


(declaim (type world-array *heightmap*))
(defvar *heightmap* (allocate-heightmap))


;;;; Colors ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmacro defcolors (&rest colors)
  `(progn
    ,@(iterate (for n :from 0)
               (for (constant nil nil) :in colors)
               (collect `(alexandria:define-constant ,constant ,n)))
    (defun init-colors ()
      ,@(iterate
          (for (constant fg bg) :in colors)
          (collect `(charms/ll:init-pair ,constant ,fg ,bg))))))

(defcolors
  (+color-white-black+  charms/ll:COLOR_WHITE   charms/ll:COLOR_BLACK)
  (+color-blue-black+   charms/ll:COLOR_BLUE    charms/ll:COLOR_BLACK)
  (+color-cyan-black+   charms/ll:COLOR_CYAN    charms/ll:COLOR_BLACK)
  (+color-yellow-black+ charms/ll:COLOR_YELLOW  charms/ll:COLOR_BLACK)
  (+color-green-black+  charms/ll:COLOR_GREEN   charms/ll:COLOR_BLACK)
  (+color-pink-black+   charms/ll:COLOR_MAGENTA charms/ll:COLOR_BLACK)

  (+color-black-white+  charms/ll:COLOR_BLACK charms/ll:COLOR_WHITE)
  (+color-black-yellow+ charms/ll:COLOR_BLACK charms/ll:COLOR_YELLOW)

  (+color-white-blue+   charms/ll:COLOR_WHITE charms/ll:COLOR_BLUE)

  (+color-white-red+    charms/ll:COLOR_WHITE charms/ll:COLOR_RED)

  (+color-white-green+  charms/ll:COLOR_WHITE charms/ll:COLOR_GREEN))

(defmacro with-color (color &body body)
  (once-only (color)
    `(unwind-protect
      (progn
        (charms/ll:attron (charms/ll:color-pair ,color))
        ,@body)
      (charms/ll:attroff (charms/ll:color-pair ,color)))))


;;;; Ticklists ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-ticklist ()
  nil)

(defmacro ticklist-push (ticklist value lifespan)
  `(push (cons ,lifespan ,value) ,ticklist))

(defun ticklist-tick (ticklist)
  (flet ((decrement (entry)
           (decf (car entry)))
         (dead (entry)
           (minusp (car entry))))
    (_ ticklist
      (mapc #'decrement _)
      (remove-if #'dead _))))

(defun ticklist-contents (ticklist)
  (mapcar #'cdr ticklist))


;;;; Weightlists ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defstruct (weightlist (:constructor %make-weightlist))
  weights sums items total)

(defun make-weightlist (items weights)
  "Make a weightlist of the given items and weights."
  (%make-weightlist
    :items items
    :weights weights
    :sums (prefix-sums weights)
    :total (apply #'+ weights)))

(defun weightlist-random (weightlist)
  "Return a random item from the weightlist, taking the weights into account."
  (iterate
    (with n = (random (weightlist-total weightlist)))
    (for item :in (weightlist-items weightlist))
    (for weight :in (weightlist-sums weightlist))
    (finding item :such-that (< n weight))))


;;;; Utils ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun manage-screen ()
  (multiple-value-bind (w h)
      (charms:window-dimensions charms:*standard-window*)
    (setf *screen-width* (1- w) *screen-height* (1- h)
          *screen-center-x* (floor w 2)
          *screen-center-y* (floor h 2))))

(defmacro render (&body body)
  `(prog2
    (progn
      (manage-screen)
      (charms:clear-window charms:*standard-window*))
    (progn ,@body)
    (charms:refresh-window charms:*standard-window*)))

(defun clamp-w (x)
  (clamp 0 (1- *screen-width*) x))

(defun clamp-h (y)
  (clamp 0 (1- *screen-height*) y))


(defun write-string-at (string x y)
  (charms:write-string-at-point
    charms:*standard-window*
    string
    (clamp-w x)
    (clamp-h y)))

(defun write-char-at (char x y)
  (charms:write-char-at-point
    charms:*standard-window*
    char
    (clamp-w x)
    (clamp-h y)))


(defun left-pad (s n)
  (format nil "~v@A" n s))

(defun right-pad (s n)
  (format nil "~vA" n s))


(defun write-centered (text x y)
  (etypecase text
    (string (write-centered (list text) x y))
    (list (iterate
            (for string :in text)
            (for tx = (- x (floor (length string) 2)))
            (for ty :from y)
            (write-string-at string tx ty)))))

(defun write-left (text x y &key pad)
  (etypecase text
    (string (write-left (list text) x y))
    (list (iterate
            (with padding = (if (and pad text)
                              (apply #'max (mapcar #'length text))
                              0))
            (for string :in text)
            (for tx = x)
            (for ty :from y)
            (unless (string= "" string)
              (write-string-at (right-pad string padding) tx ty))))))

(defun write-right (text x y &key pad)
  (etypecase text
    (string (write-right (list text) x y))
    (list (iterate
            (with padding = (if (and pad text)
                              (apply #'max (mapcar #'length text))
                              0))
            (for string :in text)
            (for tx = (- x (max padding (length string))))
            (for ty :from y)
            (unless (string= "" string)
              (write-string-at (left-pad string padding) tx ty))))))


(defun log-height ()
  (max 5 (floor *screen-height* 4)))


(defun log-message (s &rest args)
  (ticklist-push *game-log* (apply #'format nil s args) 200)
  (setf *game-log* (take (log-height) *game-log*)))


(defclause-sequence ACROSS-FLAT-ARRAY INDEX-OF-FLAT-ARRAY
  :access-fn 'row-major-aref
  :size-fn 'array-total-size
  :sequence-type 'array
  :element-type t)


(declaim (inline wrap)
         (ftype (function (fixnum) world-coordinate) wrap)
         (ftype (function (fixnum fixnum)) terrain-type terrain-char))

(defun wrap (coord)
  (mod coord +world-size+))

(defun move-view (dx dy)
  (setf *view-x* (wrap (+ *view-x* dx))
        *view-y* (wrap (+ *view-y* dy))))

(defun move-cursor (dx dy)
  (setf *cursor-x* (clamp-w (+ *cursor-x* dx))
        *cursor-y* (clamp-h (+ *cursor-y* dy))))


(defun terrain-type (x y)
  (let ((h (aref *heightmap* (wrap x) (wrap y))))
    (cond ((< h 0.23) :deep-water)
          ((< h 0.3)  :shallow-water)
          ((< h 0.34) :sand)
          ((< h 0.65) :grass)
          ((< h 0.7)  :dirt)
          ((< h 0.75) :hills)
          ((< h 0.9)  :mountain)
          (t          :snow))))

(defun terrain-char (x y)
  (case (terrain-type x y)
    (:deep-water    (values #\~ +color-blue-black+))
    (:shallow-water (values #\~ +color-cyan-black+))
    (:sand          (values #\: +color-yellow-black+))
    (:grass         (values #\. +color-green-black+))
    (:dirt          (values #\. +color-white-black+))
    (:hills         (values #\^ +color-white-black+))
    (:mountain      (values #\# +color-white-black+))
    (:snow          (values #\* +color-black-white+))))


(defun random-coordinate (&optional terrain-type)
  (iterate
    (repeat 10000)
    (for x = (random +world-size+))
    (for y = (random +world-size+))
    (finding (cons x y) :such-that (or (null terrain-type)
                                       (eql terrain-type (terrain-type x y))))))


(defun world-to-screen (wx wy)
  "Convert world-space coordinates to screen-space."
  (values (wrap (- wx *view-x*))
          (wrap (- wy *view-y*))))

(defun screen-to-world (sx sy)
  "Convert screen-space coordinates to world-space."
  (values (wrap (+ sx *view-x*))
          (wrap (+ sy *view-y*))))

(defun onscreenp (sx sy)
  "Return whether the given screen-space coords are visible in the viewport."
  (and (< -1 sx *screen-width*)
       (< -1 sy *screen-height*)))


(defmacro timing (&body body)
  `(let ((start (get-internal-run-time)))
     (prog1
       (progn ,@body)
       (setf (cdr *timing*)
             (/ (+ (* (car *timing*) (cdr *timing*))
                   (- (get-internal-run-time) start))
               (incf (car *timing*)))))))

(defun reset-timing ()
  (setf *timing* (cons 0 0)))


(defmacro modf (place n)
  `(zapf ,place (mod % ,n)))


;;;; Terrain Generation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun average4 (a b c d)
  (/ (+ a b c d) 4))


(defun hm-ref (heightmap x y)
  (flet ((ref (n)
           (cond
             ((< -1 n +world-size+) n)
             ((= n +world-size+) 0)
             (t (mod n +world-size+)))))
    (aref heightmap (ref x) (ref y))))


(defun heightmap-extrema (heightmap)
  (iterate
    (for v :across-flat-array heightmap :with-index i)
    (maximize v :into max)
    (minimize v :into min)
    (finally (return (values min max)))))

(defun normalize-heightmap (heightmap)
  (multiple-value-bind (min max) (heightmap-extrema heightmap)
    (iterate
      (with span = (- max min))
      (for v :across-flat-array heightmap :with-index i)
      (setf (row-major-aref heightmap i)
            (/ (- v min) span)))))


(defun ds-init (heightmap)
  (setf (aref heightmap 0 0) 0.5))


(defun ds-square (heightmap x y radius spread)
  (setf (aref heightmap x y)
        (random-around (average4 (hm-ref heightmap (- x radius) (- y radius))
                                 (hm-ref heightmap (- x radius) (+ y radius))
                                 (hm-ref heightmap (+ x radius) (- y radius))
                                 (hm-ref heightmap (+ x radius) (+ y radius)))
                       spread)))

(defun ds-diamond (heightmap x y radius spread)
  (setf (aref heightmap x y)
        (random-around (average4 (hm-ref heightmap (- x radius) y)
                                 (hm-ref heightmap (+ x radius) y)
                                 (hm-ref heightmap x (- y radius))
                                 (hm-ref heightmap x (+ y radius)))
                       spread)))


(defun ds-squares (heightmap radius spread)
  (iterate
    (for x :from radius :below +world-size+ :by (* 2 radius))
    (iterate
      (for y :from radius :below +world-size+ :by (* 2 radius))
      (ds-square heightmap x y radius spread))))

(defun ds-diamonds (heightmap radius spread)
  (iterate
    (for i :from 0)
    (for y :from 0 :below +world-size+ :by radius)
    (iterate
      (with shift = (if (evenp i) radius 0))
      (for x :from shift :below +world-size+ :by (* 2 radius))
      (ds-diamond heightmap x y radius spread))))


(defun diamond-square (heightmap)
  (ds-init heightmap)
  (let ((spread 0.8)
        (spread-reduction 0.7))
    (recursively ((radius (floor +world-size+ 2))
                  (spread spread))
      (when (>= radius 1)
        (ds-squares heightmap radius spread)
        (ds-diamonds heightmap radius spread)
        (recur (/ radius 2)
               (* spread spread-reduction)))))
  (normalize-heightmap heightmap)
  heightmap)


;;;; Name Generation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defparameter *name-syllables*
  (_ "syllables.txt"
    alexandria:read-file-into-string
    read-from-string
    (coerce _ 'vector)))

(defun random-name ()
  (format nil "~:(~{~A~}~)"
          (iterate (repeat (random-range 1 5))
                   (collect (random-elt *name-syllables*)))))


;;;; Aspects ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Coordinates
(define-aspect coords
  (x :type world-coordinate)
  (y :type world-coordinate))

(deftype coordinate-array ()
  `(simple-array list (,+world-size+ ,+world-size+)))

(declaim (type coordinate-array *coords-contents*))
(defparameter *coords-contents*
  (make-array (list +world-size+ +world-size+) :initial-element nil))


(declaim (ftype (function (fixnum fixnum) list)
                coords-lookup)
         (ftype (function (coords) list)
                coords-insert-entity coords-remove-entity)
         (ftype (function (coords fixnum fixnum) list)
                coords-move-entity))

(defun coords-insert-entity (e)
  (push e (aref *coords-contents* (coords/x e) (coords/y e))))

(defun coords-remove-entity (e)
  (zapf (aref *coords-contents* (coords/x e) (coords/y e))
        (delete e %)))

(defun coords-move-entity (e new-x new-y)
  (coords-remove-entity e)
  (setf (coords/x e) (wrap new-x)
        (coords/y e) (wrap new-y))
  (coords-insert-entity e))

(defun coords-lookup (x y)
  (aref *coords-contents* (wrap x) (wrap y)))


(defun nearby (entity)
  (remove entity
          (iterate
            outer
            (with r = 1)
            (with x = (coords/x entity))
            (with y = (coords/y entity))
            (for dx :from (- r) :to r)
            (iterate
              (for dy :from (- r) :to r)
              (in outer
                  (appending (coords-lookup (+ x dx)
                                            (+ y dy))))))))


(defmethod initialize-instance :after ((entity coords) &key)
  (callf (coords/x entity) #'wrap
        (coords/y entity) #'wrap))

(defmethod entity-created :after ((entity coords))
  (coords-insert-entity entity))

(defmethod entity-destroyed :after ((entity coords))
  (coords-remove-entity entity))


;;; Flavor Text
(define-aspect flavor text)


;;; Inspection
(define-aspect inspectable
  (slots :initform nil))

(defun inspectable-get (entity slot)
  (etypecase slot
    (symbol (cons slot (slot-value entity slot)))
    (function (funcall slot entity))))


;;; Visibility
(define-aspect visible glyph color)


;;; Food
(define-aspect edible
  energy
  original-energy)

(define-aspect decomposing
  rate
  (remaining :initform 1.0))

(define-aspect fruiting
  chance)


(defmethod initialize-instance :after ((e edible) &key)
  (setf (edible/original-energy e)
        (edible/energy e)))


(define-system (rot :inline t) ((entity decomposing))
  (when (minusp (decf (decomposing/remaining entity)
                      (decomposing/rate entity)))
    (destroy-entity entity)))

(define-system (rot-food :inline t) ((entity decomposing edible))
  (setf (edible/energy entity)
        (lerp 0.0 (edible/original-energy entity)
              (decomposing/remaining entity))))


(defun decomposing-description (entity)
  (let ((remaining (decomposing/remaining entity)))
    (cond
      ((< remaining 0.2) '("It is almost completely rotten."))
      ((< remaining 0.5) '("It is partially decomposed."))
      ((< remaining 0.8) '("It has begun to smell.")))))


;;;; Metabolism
(define-aspect metabolizing
  insulation
  energy)


(defgeneric starve (entity))

(defmethod starve ((entity entity))
  (destroy-entity entity))


(defgeneric calculate-energy-cost (entity))

(defmethod calculate-energy-cost ((entity metabolizing))
  (let* ((insulation (metabolizing/insulation entity))
         (base-cost 1.0)
         (temperature-cost (max 0 (* 0.2 (- (abs *temperature*) insulation))))
         (insulation-cost (* 0.1 insulation)))
    (+ base-cost temperature-cost insulation-cost)))


(define-system consume-energy ((entity metabolizing))
  (when (minusp (decf (metabolizing/energy entity)
                      (calculate-energy-cost entity)))
    (starve entity)))


;;; Brains
(define-aspect sentient function)
(define-aspect periodic
  function
  (counter :initform 1)
  next
  min
  max)


(define-system sentient-act ((entity sentient))
  (funcall (sentient/function entity) entity))

(define-system periodic-tick ((entity periodic))
  (when (zerop (setf (periodic/counter entity)
                     (mod (1+ (periodic/counter entity))
                          (periodic/next entity))))
    (setf (periodic/next entity)
          (random-range (periodic/min entity)
                        (periodic/max entity)))
    (funcall (periodic/function entity) entity)))


;;; Age
(define-aspect aging
  (birthtick :initform *tick*)
  (age :initform 0))


(define-system age ((entity aging))
  (incf (aging/age entity)))


;;;; Entities ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Flora
(define-entity tree (coords visible fruiting flavor))
(define-entity fruit (coords visible edible flavor decomposing inspectable))
(define-entity algae (coords visible edible decomposing))
(define-entity grass (coords visible edible decomposing))


(defun make-tree (x y)
  (create-entity 'tree
                 :coords/x x
                 :coords/y y
                 :visible/glyph "T"
                 :visible/color +color-green-black+
                 :fruiting/chance 0.0005
                 :flavor/text '("A tree sways gently in the wind.")))

(defun make-fruit (x y)
  (create-entity 'fruit
                 :coords/x x
                 :coords/y y
                 :visible/glyph "ó"
                 :visible/color +color-pink-black+
                 :edible/energy (random-around 500 10)
                 :decomposing/rate 0.0005
                 :inspectable/slots '(edible/energy)
                 :flavor/text '("A ripe piece of fruit has fallen to the ground.")))

(defun make-algae (x y)
  (create-entity 'algae
                 :coords/x x
                 :coords/y y
                 :edible/energy 50
                 :decomposing/rate 0.003
                 :visible/glyph "`"
                 :visible/color +color-green-black+))

(defun make-grass (x y)
  (create-entity 'grass
                 :coords/x x
                 :coords/y y
                 :edible/energy 20
                 :decomposing/rate 0.001
                 :visible/glyph "\""
                 :visible/color +color-green-black+))


(define-system grow-fruit ((entity fruiting coords))
  (when (randomp (fruiting/chance entity))
    (make-fruit (wrap (random-around (coords/x entity) 2))
                (wrap (random-around (coords/y entity) 2)))))


(defun tree-probability (x y)
  (case (terrain-type x y)
    (:grass 0.01)
    (:dirt 0.001)
    (t 0)))


(defun generate-trees ()
  (iterate
    (for x :from 0 :below +world-size+)
    (iterate
      (for y :from 0 :below +world-size+)
      (when (randomp (tree-probability x y))
        (make-tree x y)))))


(defun grow-algae ()
  (iterate
    (repeat (floor (* +world-size+ +world-size+) 100000))
    (let ((target (random-coordinate :shallow-water)))
      (when target (make-algae (car target) (cdr target))))))

(defun grow-grass ()
  (iterate
    (repeat (floor (* +world-size+ +world-size+) 100000))
    (let ((target (random-coordinate :grass)))
      (when target (make-grass (car target) (cdr target))))))


;;; Fauna
(define-entity creature
    (coords visible sentient flavor metabolizing aging inspectable)
  (name :accessor creature-name :initarg :name)
  (directions :accessor creature-directions :initarg :directions))

(define-entity corpse
  (coords visible flavor decomposing))


(defparameter *directions*
  (iterate dirs (for dx :from -1 :to 1)
           (iterate (for dy :from -1 :to 1)
                    (in dirs (collect (cons dx dy))))))

(defparameter *default-creature-directions*
  (make-weightlist *directions*
                   (iterate (repeat (length *directions*))
                            (collect 10))))

(defparameter *creature-colors*
  (vector +color-white-black+
          +color-blue-black+
          +color-cyan-black+
          +color-yellow-black+
          +color-green-black+
          +color-pink-black+))

(defparameter *creature-glyphs*
  (vector "@" "$" "?" "!" "&" "+" "☃" "$" "&" "¥"
          "£" "¤" "€" "‡" "¶" "µ" "¢" "¬" "¿" "§"))


(defun creature-mutate-glyph (c)
  (setf (visible/glyph c) (random-elt *creature-glyphs*)))

(defun creature-mutate-color (c)
  (setf (visible/color c) (random-elt *creature-colors*)))

(defun creature-mutate-directions (c)
  (let ((old (creature-directions c)))
    (setf (creature-directions c)
          (make-weightlist (weightlist-items old)
                           (mapcar (lambda (w)
                                     (max 1 (random-around w 1)))
                                   (weightlist-weights old))))))

(defun creature-mutate-appearance (c)
  (if (randomp)
    (creature-mutate-color c)
    (creature-mutate-glyph c)))

(defun creature-mutate-insulation (c)
  (setf (metabolizing/insulation c)
        (max 0 (random-around (metabolizing/insulation c) 1))))

(defun creature-mutate (c)
  (let ((v (random 1.0)))
    (cond
      ((< v 0.40) (creature-mutate-directions c))
      ((< v 0.90) (creature-mutate-insulation c))
      ((< v 0.99) (creature-mutate-color c))
      ((< v 1.00) (creature-mutate-glyph c)))))


(defun creature-should-reproduce-p (c)
  (and (> (metabolizing/energy c) 1000)
       (randomp 0.01)))

(defun creature-should-mutate-p ()
  (randomp 0.6))

(defun creature-reproduce (parent)
  (let* ((energy (floor (metabolizing/energy parent) 2))
         (child (make-creature (coords/x parent) (coords/y parent)
                               :color (visible/color parent)
                               :glyph (visible/glyph parent)
                               :energy energy
                               :insulation (metabolizing/insulation parent)
                               :directions (creature-directions parent))))
    (setf (metabolizing/energy parent) energy)
    (when (creature-should-mutate-p)
      (creature-mutate child))
    (log-message "~A begets ~A." (creature-name parent) (creature-name child))))


(defun creature-move (c)
  (let ((x (coords/x c))
        (y (coords/y c)))
    (destructuring-bind (dx . dy)
        (weightlist-random (creature-directions c))
      (coords-move-entity c (+ x dx) (+ y dy)))))

(defun creature-eat (c food)
  (incf (metabolizing/energy c) (edible/energy food))
  (destroy-entity food))

(defun creature-act (c)
  (let* ((near (nearby c))
         (food (find-if #'edible? near)))
    (cond
      (food (creature-eat c food))
      ((creature-should-reproduce-p c) (creature-reproduce c))
      (t (creature-move c)))))


(defun make-creature (x y &key
                      (directions *default-creature-directions*)
                      (color +color-white-black+)
                      (glyph "@")
                      (energy 3000)
                      (insulation 0))
  (let ((name (random-name)))
    (create-entity
      'creature
      :name name
      :directions directions
      :coords/x x
      :coords/y y
      :visible/color color
      :visible/glyph glyph
      :metabolizing/energy energy
      :metabolizing/insulation insulation
      :sentient/function 'creature-act
      :inspectable/slots
      (list 'name
            (lambda (c) (cons 'directions
                              (weightlist-weights (creature-directions c))))
            'metabolizing/energy 'metabolizing/insulation
            'aging/birthtick 'aging/age)
      :flavor/text
      (list (format nil "A creature named ~:(~A~) is here." name)
            "It likes food."))))

(defun make-corpse (x y color name)
  (create-entity
    'corpse
    :coords/x x
    :coords/y y
    :visible/color color
    :visible/glyph "%"
    :decomposing/rate 0.001
    :flavor/text (list (format nil "The corpse of ~:(~A~) lies here." name))))


(defmethod starve :after ((c creature))
  (log-message "~A has starved after living for ~D tick~:P."
               (creature-name c)
               (aging/age c))
  (make-corpse (coords/x c)
               (coords/y c)
               (visible/color c)
               (creature-name c)))


(defmethod entity-created :after ((e creature))
  (declare (ignore e))
  (incf *population*))

(defmethod entity-destroyed :after ((e creature))
  (declare (ignore e))
  (decf *population*))


;;; Mysteries
(define-entity monolith (coords visible sentient flavor)
  (countdown :initarg :countdown))

(define-entity fountain (coords visible sentient flavor)
  (recent :initform (make-ticklist)))

(define-entity colossus (coords visible periodic flavor))
(define-entity yggdrasil (coords visible periodic flavor))
(define-entity yggdrasil-sapling (coords visible flavor))


(defun monolith-act (m)
  (when (zerop *population*)
    (with-slots (countdown) m
        (case (decf countdown)
          (40 (log-message "The monolith begins to glow."))
          (0 (progn
               (setf countdown 100)
               (_ (make-creature (coords/x m) (1+ (coords/y m)))
                 creature-name
                 (log-message
                   "The monolith flashes brightly and ~A appears in front of it!"
                   _))))))))

(defun fountain-act (f)
  (with-slots (recent) f
    (callf recent #'ticklist-tick)
    (iterate
      (for creature :in (remove-if-not #'creature? (nearby f)))
      (unless (member creature (ticklist-contents recent))
        (creature-mutate-appearance creature)
        (ticklist-push recent creature 1000)
        (log-message "~A drinks from the fountain and... changes."
                     (creature-name creature))))))

(defun colossus-act (c)
  (coords-move-entity c (1+ (coords/x c)) (coords/y c))
  (log-message "The colossus takes a step."))

(defun yggdrasil-act (ygg)
  (let* ((x (coords/x ygg))
         (y (coords/y ygg))
         (nx (random-gaussian-integer x 10))
         (ny (random-gaussian-integer y 10)))
    (unless (or (and (= x nx) (= y ny))
                (find-if #'yggdrasil-sapling? (coords-lookup nx ny)))
      (log-message "A leaf falls from the massive ash tree and flutters to the ground.")
      (log-message "A new tree springs up where it fell!")
      (make-yggdrasil-sapling nx ny))))


(defun make-monolith ()
  (create-entity 'monolith
                 :countdown 50
                 :coords/x 0
                 :coords/y 0
                 :visible/glyph " "
                 :visible/color +color-black-yellow+
                 :sentient/function 'monolith-act
                 :flavor/text
                 '("A sleek, rectangular, octarine monolith stands here."
                   "Who placed it?")))

(defun make-fountain ()
  (let ((loc (random-coordinate :grass)))
    (when loc
      (create-entity 'fountain
                 :coords/x (car loc)
                 :coords/y (cdr loc)
                 :visible/glyph "ƒ"
                 :visible/color +color-white-blue+
                 :sentient/function 'fountain-act
                 :flavor/text
                 '("A marble fountain burbles peacefully.")))))

(defun make-colossus ()
  (let ((loc (random-coordinate :snow)))
    (when loc
      (create-entity 'colossus
                     :coords/x (car loc)
                     :coords/y (cdr loc)
                     :visible/glyph "@"
                     :visible/color +color-white-red+
                     :periodic/function 'colossus-act
                     :periodic/next 1000
                     :periodic/min 2000
                     :periodic/max 10000
                     :flavor/text
                     '("A massive granite statue of an alien being.")))))

(defun make-yggdrasil ()
  (let ((loc (random-coordinate :grass)))
    (when loc
      (create-entity 'yggdrasil
                     :coords/x (car loc)
                     :coords/y (cdr loc)
                     :visible/glyph "Y"
                     :visible/color +color-white-green+
                     :periodic/function 'yggdrasil-act
                     :periodic/next 600
                     :periodic/min 1000
                     :periodic/max 2000
                     :flavor/text
                     '("An immense ash tree."
                       "Its branches touch the stars.")))))

(defun make-yggdrasil-sapling (x y)
  (create-entity 'yggdrasil-sapling
                 :coords/x x
                 :coords/y y
                 :visible/glyph "y"
                 :visible/color +color-green-black+
                 :flavor/text '("An small ash tree bends toward the sun.")))


(defun generate-mysteries ()
  (make-colossus)
  (make-fountain)
  (make-yggdrasil)
  (make-monolith))


;;;; Profiling ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
#+sbcl
(defun dump-profile ()
  (with-open-file (*standard-output* "silt.prof"
                                     :direction :output
                                     :if-exists :supersede)
    (sb-sprof:report :type :graph
                     :sort-by :cumulative-samples
                     :sort-order :ascending)
    (sb-sprof:report :type :flat
                     :min-percent 0.5)))

#+sbcl
(defun start-profiling ()
  (sb-sprof::reset)
  (sb-sprof::profile-call-counts "SILT")
  (sb-sprof::start-profiling :max-samples 50000
                             ; :mode :cpu
                             :mode :time
                             :sample-interval 0.01
                             :threads :all))

#+sbcl
(defun stop-profiling ()
  (sb-sprof::stop-profiling)
  (dump-profile))


;;;; Game State Machine ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defparameter *controls-text*
  '("CONTROLS"
    "  [hjklyubn] move your view"
    "  [HJKLYUBN] move your view faster"
    ""
    "      [wasd] move your cursor"
    "      [WASD] move your cursor faster"
    ""
    "         [+] increase the temperature of the world"
    "         [-] decrease the temperature of the world"
    ""
    "     [space] pause time"
    "         [`] tick time once while paused"
    ""
    "         [Q] quit"
    "         [R] regenerate the world"
    ""
    "         [?]  help"))

(defun render-title ()
  (render
    (write-centered '("S I L T"
                      ""
                      "Press any key to start...")
                    *screen-center-x*
                    (1- *screen-center-y*))))

(defun render-intro ()
  (render
    (write-left (append '("Welcome to Silt."
                          ""
                          "You are the god of a toroidal world."
                          ""
                          "Move your cursor over things to observe them."
                          "")
                        *controls-text*
                        '(""
                          "Press any key to begin."))
                1 1)))

(defun render-help ()
  (render
    (write-left (append *controls-text*
                        '(""
                          "Press any key to continue."))
                1 1)))

(defun render-generate ()
  (render
    (write-centered "Generating world, please wait..."
                    *screen-center-x* *screen-center-y*)))


(defun draw-map ()
  (iterate
    (repeat *screen-width*)
    (for sx :from 0)
    (for wx :from *view-x*)
    (iterate
      (repeat *screen-height*)
      (for sy :from 0)
      (for wy :from *view-y*)
      (for entity = (find-if #'visible? (coords-lookup wx wy)))
      (if entity
        (with-color (visible/color entity)
          (write-string-at (visible/glyph entity) sx sy))
        (multiple-value-bind (glyph color) (terrain-char wx wy)
          (with-color color
            (write-char-at glyph sx sy)))))))

(defun draw-hud ()
  (write-right
    (list
      (format nil "[~D, ~D]" *view-x* *view-y*)
      (format nil "[~D, ~D]" *cursor-x* *cursor-y*)
      (format nil "~D creature~:P" *population*)
      (format nil "~D entit~:@P" (hash-table-count beast::*entity-index*))
      (format nil "~D°" *temperature*)
      (format nil "tick ~D" *tick*)
      (if (equal *timing* (cons 0 0))
        ""
        (format nil "~,5Fms per run over ~D runs"
                (/ (cdr *timing*) internal-time-units-per-second 1/1000)
                (car *timing*)))
      (if (= *frame-skip* 1)
        ""
        (format nil "frameskip: ~D" *frame-skip*)))
    (1- *screen-width*)
    1
    :pad t))

(defun draw-paused ()
  (when *paused*
    (write-right '("            "
                   "   PAUSED   "
                   "            ")
                 *screen-width*
                 (- *screen-height* 3))))

(defun draw-log ()
  (let ((messages (nreverse (ticklist-contents *game-log*))))
    (write-left messages 0 (- *screen-height* (length messages)))))


(defun indent (lines)
  (mapcar (lambda (line) (concatenate 'string "    " line)) lines))

(defun draw-selected ()
  (write-left
    (iterate
      (for entity :in (multiple-value-call #'coords-lookup
                        (screen-to-world *cursor-x* *cursor-y*)))
      (when (typep entity 'flavor)
        (appending (flavor/text entity) :into text)

        (when (typep entity 'decomposing)
          (appending (indent (decomposing-description entity)) :into text))

        (when (typep entity 'inspectable)
          (appending
            (indent
              (iterate
                (with slots = (mapcar (curry #'inspectable-get entity)
                                      (inspectable/slots entity)))
                (with width = (apply #'max
                                     (mapcar (compose #'length #'symbol-name #'car)
                                             slots)))
                (for (label . contents) :in slots)
                (collect
                  (let ((*print-pretty* nil))
                    (format nil "~vA ~A" width label contents)))))
            :into text))

        (collecting "" :into text))
      (finally (return text)))
    1 1 :pad t))


(defun draw-ui ()
  (draw-hud)
  (draw-selected)
  (draw-paused)
  (draw-log))


(defun render-map ()
  (manage-screen)
  (draw-map)
  (draw-ui)
  (charms:move-cursor charms:*standard-window* *cursor-x* *cursor-y*))


(defun press-any-key ()
  (charms:disable-non-blocking-mode charms:*standard-window*)
  (charms:get-char charms:*standard-window*))

(defun handle-input-map ()
  (iterate
    (for key = (charms:get-char charms:*standard-window* :ignore-error t))
    (while key)
    (case key
      ((#\Q) (return :quit))
      ((#\R) (return :regen))
      ((#\?) (return :help))

      ((#\Space) (callf *paused* #'not))
      ((#\`) (when *paused* (tick-world)))

      ((#\+) (incf *temperature*))
      ((#\-) (decf *temperature*))

      ((#\]) (incf *frame-skip*))
      ((#\[) (setf *frame-skip* (clamp 1 100 (1- *frame-skip*))))
      ((#\!) (callf *sleep* #'not))

      ((#\h) (move-view  -5   0))
      ((#\j) (move-view   0   5))
      ((#\k) (move-view   0  -5))
      ((#\l) (move-view   5   0))
      ((#\y) (move-view  -5  -5))
      ((#\u) (move-view   5  -5))
      ((#\b) (move-view  -5   5))
      ((#\n) (move-view   5   5))

      ((#\H) (move-view -30   0))
      ((#\J) (move-view   0  30))
      ((#\K) (move-view   0 -30))
      ((#\L) (move-view  30   0))
      ((#\Y) (move-view -30 -30))
      ((#\U) (move-view  30 -30))
      ((#\B) (move-view -30  30))
      ((#\N) (move-view  30  30))

      ((#\w) (move-cursor   0  -1))
      ((#\a) (move-cursor  -1   0))
      ((#\s) (move-cursor   0   1))
      ((#\d) (move-cursor   1   0))
      ((#\W) (move-cursor   0 -10))
      ((#\A) (move-cursor -10   0))
      ((#\S) (move-cursor   0  10))
      ((#\D) (move-cursor  10   0)))))


(defun tick-flora ()
  (run-grow-fruit)
  (grow-algae)
  (grow-grass)
  (run-rot)
  (run-rot-food))

(defun tick-world ()
  (incf *tick*)
  (run-age)
  (run-consume-energy)
  (tick-flora)
  (run-sentient-act)
  (run-periodic-tick))

(defun tick-log ()
  (setf *game-log* (ticklist-tick *game-log*)))


(defun state-title ()
  (render-title)
  (press-any-key)
  (state-intro))

(defun state-intro ()
  (render-intro)
  (press-any-key)
  (state-generate))


(defun reset-world ()
  (setf *random-state* (make-random-state t))
  (clear-entities)
  (setf *view-x* (wrap (- *screen-center-x*))
        *view-y* (wrap (- *screen-center-y*))
        *game-log* (make-ticklist)
        *cursor-x* 0
        *cursor-y* 0
        *population* 0
        *tick* 0
        *temperature* 0
        *frame-skip* 1
        *paused* nil))

(defun generate-world ()
  (setf *heightmap* (diamond-square (allocate-heightmap)))
  (generate-trees)
  (generate-mysteries)
  (iterate (repeat 1000)
           (tick-flora)))

(defun state-generate ()
  (manage-screen)
  (render-generate)
  (reset-world)
  (generate-world)
  (state-map))


(defun state-map ()
  (charms:enable-non-blocking-mode charms:*standard-window*)
  (state-map-loop))

(defun state-map-loop ()
  (case (handle-input-map)
    ((:quit) (state-quit))
    ((:regen) (state-generate))
    ((:help) (state-help))
    (t (progn
         (unless *paused*
           (iterate (repeat *frame-skip*)
                    (tick-world)
                    (tick-log)))
         (render-map)
         (when *sleep*
           (sleep 0.05))
         (state-map-loop)))))


(defun state-help ()
  (render-help)
  (press-any-key)
  (state-map))

(defun state-quit ()
  'goodbye)


;;;; Run ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun run ()
  (charms:with-curses ()
    (charms:disable-echoing)
    (charms:enable-raw-input :interpret-control-characters t)
    (charms:enable-extra-keys charms:*standard-window*)
    (charms/ll:start-color)
    (init-colors)
    (state-title)))

(defun main ()
  (handler-case
      (progn
        (run)
        (format t "Goodbye.~%"))
    (t (e)
     (declare (ignore e))
     (format t "Something went wrong, sorry.~%"))))


;;;; Scratch ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; (run)
; (start-profiling)
; (stop-profiling)