src/silt/core.clj @ 1ffe4a0cb084

Give Eve a headstart on insulation
author Steve Losh <steve@stevelosh.com>
date Mon, 14 Dec 2015 20:06:25 +0000
parents c3a802d2c176
children 6c03b237b5ec
(ns silt.core
  (:require [lanterna.screen :as s]
            [roul.random :as rr]
            [clojure.pprint]
            [clojure.core.match :refer [match]]
            [clojure.stacktrace :refer [print-stack-trace]])
  (:gen-class))


; Data ------------------------------------------------------------------------
(def fps 50)
(def day (ref 0))
(def world-width 600)
(def world-height 400)
(def tick-delay (atom 50))
(def insulation-cost 0.01)

(def screen-size (atom [0 0]))
(defn handle-resize [cols rows]
  (reset! screen-size [cols rows]))
(def screen (s/get-screen :swing {:resize-listener handle-resize
                                  :palette :mac-os-x
                                  :font ["Menlo" "Consolas"]
                                  :font-size 16
                                  }))

(def pond-count 100)
(def pond-size 3)
(def initial-energy 200.0)
(def hunger-rate 0.25)

(def fruit-energy 50)
(def fruit-rate 0.5)

(def paused (atom false))
(def dirty (atom true))

(def directions [[-1 -1] [ 0 -1] [ 1 -1]
                 [-1  0] [ 0  0] [ 1  0]
                 [-1  1] [ 0  1] [ 1  1]])

(defonce running (atom true))
(def window-loc (ref [0 0]))
(defonce last-timestamp (atom 0))
(def cursor-loc (ref [0 1]))

(defonce terrain (ref {}))
(def terrain-rate 0.8)

(def shrub {:name :shrub
            :glyph "%"
            :styles {:fg :green}
            :energy 0
            :solid true})
(def rock {:name :rock
           :glyph "*"
           :energy 0
           :solid true})
(def water {:name :water
            :glyph "≈"
            :energy 0.05
            :solid false
            :styles {:fg :black :bg :blue}})

(def terrain-objects
  {water 1 rock 20 shrub 80})


(def world-temp (ref 0))
(def clone-energy 100)
(def reproduction-rate 10)

(def animals (ref {}))
(def initial-animals 400)
(def eve
  {:glyph "@"
   :styles {:fg :white}
   :temp 0
   :insulation 2
   :energy initial-energy
   :id "eve"
   :directions [[[-1 -1] 2] [[0 -1] 1] [[1 -1] 2]
                [[-1  0] 2] [[0  0] 0] [[1  0] 2]
                [[-1  1] 2] [[0  1] 1] [[1  1] 2]]
   :loc [0 1]
   })


; Utils -----------------------------------------------------------------------
(defmacro while-let [binding-form & body]
  `(loop []
     (when-let ~binding-form
       ~@body
       (recur))))

(defn random-coord []
  [(rr/rand-int 0 world-width)
   (rr/rand-int 0 world-height)])

(defn normalize-coord [v limit]
  (cond
    (< v 0) (normalize-coord (+ limit v) limit)
    (>= v limit) (normalize-coord (- v limit) limit)
    :else v))

(defn normalize-world-coords [[wx wy]]
  [(normalize-coord wx world-width)
   (normalize-coord wy world-height)])

(defmacro maybe [chance value & body]
  `(fn [~value]
     (if (rr/rand-bool ~chance)
       (do ~@body)
       ~value)))

(defmacro maybe-update-in [test-form m key-vec & body]
  `(if ~test-form
     (update-in ~m ~key-vec ~@body)
     ~m))

(defn dir-add [[x y] [dx dy]]
  (normalize-world-coords [(+ x dx) (+ y dy)]))

(defn neighbors [coord]
  (map (partial dir-add coord) directions))

(defn neighboring-things [thing things]
  (->> thing
    :loc
    neighbors
    (map things)
    (filter identity)))

(defn uuid []
  (str (java.util.UUID/randomUUID)))

(defn mutate-directions [dirs]
  (update-in dirs [(rr/rand-int 0 9) 1] + (rr/rand-int 1 5)))

(defn clamp [v minimum]
  (max v minimum))

(defn mutate-animal
  ([animal] (mutate-animal animal nil))
  ([animal mc]
   (-> animal
     (update :insulation
             (maybe (or mc 10) v
                    (clamp (+ v (rand-nth [-1 1])) 1)))
     (update :directions
             (maybe (or mc 10) v
                    (mutate-directions v)))
     (update-in [:styles :fg]
                (maybe (or mc 0.8) v
                       (rr/rand-nth [:white :blue :green :yellow :red :cyan])))
     (update :glyph
             (maybe (or mc 0.05) v
                    (rr/rand-nth [";" "☃" "$" "&" "!" ":" "¥" "£" "¤" "€"
                                  "‡" "ß" "¶" "µ" "¢" "¬" "¿" "?" "§" "@"]))))))

(defn map-vals [m f]
  (into {} (for [[k v] m]
             [k (f v)])))

(defn to-loc-map [coll]
  (into {} (map (juxt :loc identity) coll)))

(defn to-loc-map-safe [coll]
  (loop [next-thing (first coll)
         remaining (rest coll)
         result (transient {})]
    (if next-thing
      (let [loc (:loc next-thing)]
        (if (result loc)
          (recur (assoc next-thing
                        :loc (dir-add loc (rand-nth directions)))
                 remaining
                 result)
          (recur (first remaining)
                 (rest remaining)
                 (assoc! result loc next-thing))))
      (persistent! result))))

(defn abs [n]
  ; eat shit, clojure
  (if (< n 0) (- n) n))


; Mysteries -------------------------------------------------------------------
(def landmarks
  (ref (to-loc-map
         #{{:name :monolith :loc [0 0]
            :glyph "#" :styles {:fg :black :bg :yellow}
            :description "A sleek, rectangular, octarine monolith.  What is its function?"
            :action
            (fn [self]
              (when (and (rr/rand-bool 0.1)
                         (empty? @animals))
                (ref-set animals {(:loc eve) eve})))}
           {:name :yggdrasil :loc (random-coord)
            :glyph "Y" :styles {:fg :white :bg :green}
            :description "An immense ash tree.  Its branches touch the stars."
            :action
            (fn [{[x y] :loc}]
              (when (rr/rand-bool 0.25)
                (let [dx (rr/rand-gaussian-int 0 10)
                      dy (rr/rand-gaussian-int 0 10)
                      target (normalize-world-coords [(+ x dx) (+ y dy)])]
                  (when-not (= [dx dy] [0 0])
                    (alter terrain assoc target (assoc shrub :loc target))))))}
           {:name :colossus :loc (random-coord)
            :glyph "@" :styles {:fg :black :bg :red}
            :description "A massive granite statue of an alien being."
            :action
            (fn [{[x y :as loc] :loc :as self}]
              (when (zero? (rem @day 1000))
                (let [dest (normalize-world-coords [(inc x) y])]
                  (alter landmarks dissoc loc)
                  (alter landmarks assoc dest (assoc self :loc dest)))))}
           {:name :fountain :loc (random-coord)
            :glyph "ƒ" :styles {:fg :white :bg :blue}
            :description "A marble fountain burbles peacefully."
            :action
            (fn [{:keys [loc]}]
              (doseq [animal (->> loc
                               neighbors
                               (map animals)
                               (filter identity))]
                (alter animals
                       #(update % (:loc animal) mutate-animal 100))))}
           })))


; Animals ---------------------------------------------------------------------
(defn can-reproduce [animal]
  (> (:energy animal) 150))

(defn clone [animal]
  (-> animal
    (assoc :id (uuid))
    (assoc :energy clone-energy)
    (update :loc (fn [[x y]] (normalize-world-coords [(inc x) y])))
    mutate-animal))

(defn reproduce [animal]
  [(update animal :energy - clone-energy)
   (clone animal)])

(defn try-move [orig dir]
  (if (= dir [0 0])
    orig
    (let [dest (dir-add orig dir)]
      (if (or (get-in @terrain [dest :solid])
              (contains? @landmarks dest)
              (contains? @animals dest))
        orig
        dest))))


(defn in-water [{:keys [loc]}]
  (= :water (get-in @terrain [loc :name])))

(defn affect-temp [animal]
  (assoc animal :temp
         (/ @world-temp
            (:insulation animal)
            (if (in-water animal) 5 1))))

(defn fix-temp [{:keys [temp] :as animal}]
  (-> animal
    (assoc :temp 0)
    (update :energy - (* 0.1 (abs temp)))))

(defn find-resources [animal]
  (let [found (->> (neighboring-things animal @terrain)
                (map :energy))]
    (update animal :energy (partial reduce +) found)))

(defn wander [animal]
  (update animal :loc
          try-move
          (rr/rand-nth-weighted (:directions animal))))

(defn hunger [{:keys [insulation] :as animal}]
  (update animal :energy - (+ hunger-rate
                              (* insulation insulation-cost))))

(defn starve [animal]
  (if (< (:energy animal) 0)
    []
    [animal]))

(defn try-reproduce [animals]
  (match animals
    [] []
    [animal] (if (and (can-reproduce animal)
                      (rr/rand-bool reproduction-rate))
               (reproduce animal)
               animals)))

(defn tick-animal [animal]
  (-> animal
    affect-temp
    fix-temp
    wander
    find-resources
    hunger
    starve
    try-reproduce))

(defn tick-animals [animals]
  (to-loc-map-safe (mapcat tick-animal (vals animals))))


; World Generation ------------------------------------------------------------
(defn grow-shrub [shrub]
  (assoc shrub
         :energy fruit-energy
         :styles {:fg :magenta}))

(defn strip-shrub [shrub]
  (assoc shrub
         :energy 0
         :styles {:fg :green}))

(defn tick-shrub [shrub]
  (if (zero? (:energy shrub))
    (if (rr/rand-bool fruit-rate)
      (grow-shrub shrub)
      shrub)
    (if (empty? (neighboring-things shrub @animals))
      shrub
      (strip-shrub shrub))))

(defn tick-terrain [terrain]
  (into {} (for [[loc {:keys [name] :as obj}] terrain]
             [loc (case name
                    :shrub (tick-shrub obj)
                    obj)])))


(defn dedupe-things [things]
  (into {} (map (juxt :loc identity) things)))


(defn generate-terrain []
  (for [x (range world-width)
        y (range world-height)
        :when (rr/rand-bool terrain-rate)]
    (assoc (rr/rand-nth-weighted terrain-objects)
           :loc [x y])))

(defn generate-pond [[ox oy]]
  (for [_ (range 200)
            :let [x (rr/rand-gaussian-int ox (* pond-size 1.5))
                  y (rr/rand-gaussian-int oy pond-size)]]
    (assoc water :loc (normalize-world-coords [x y]))))

(defn generate-water []
  (->> (for [_ (range pond-count)]
         (generate-pond (random-coord)))
    (into #{})
    (apply concat)))


(defn generate-animals []
  (conj (for [_ (range initial-animals)]
          (-> eve
            clone
            (assoc :energy initial-energy)
            (assoc :loc (random-coord))))
        eve))


(defn reset-window! []
  (dosync (ref-set window-loc [0 0])))

(defn reset-terrain! []
  (let [new-terrain (as-> (generate-terrain) t
                      (into t (generate-water))
                      (dedupe-things t))]
    (dosync
      (ref-set terrain new-terrain))
    nil))

(defn reset-animals! []
  (let [new-animals (dedupe-things (generate-animals))]
    (dosync
      (ref-set animals new-animals))
    nil))


; Drawing ---------------------------------------------------------------------
(defn mark-dirty! []
  (reset! dirty true))

(defn calc-screen-coords [[wx wy]]
  (let [[ox oy] @window-loc]
    (normalize-world-coords [(- wx ox) (- wy oy)])))

(defn calc-world-coords [[sx sy]]
  (let [[ox oy] @window-loc]
    (normalize-world-coords [(+ sx ox) (+ sy oy)])))

(defmacro loop-screen [sx sy wc & body]
  `(let [[swidth# sheight#] @screen-size]
    (doseq [~sx (range swidth#)
            ~sy (range sheight#)
            :let [~wc (calc-world-coords [~sx ~sy])]]
      ~@body)))

(defn draw-terrain! [screen]
  (loop-screen
    sx sy wc
    (when-let [thing (@terrain wc)]
      (s/put-string screen sx sy (:glyph thing) (:styles thing)))))

(defn draw-landmarks! [screen]
  (let [[swidth sheight] @screen-size]
    (doseq [{:keys [loc glyph styles]} (vals @landmarks)
            :let [[sx sy] (calc-screen-coords loc)]
            :when (and (< -1 sx swidth)
                       (< -1 sy sheight))]
      (s/put-string screen sx sy glyph styles))))

(defn draw-animals! [screen]
  (let [[swidth sheight] @screen-size]
    (doseq [{:keys [loc glyph styles]} (vals @animals)
            :let [[sx sy] (calc-screen-coords loc)]
            :when (and (< -1 sx swidth)
                       (< -1 sy sheight))]
      (s/put-string screen sx sy glyph styles))))

(defn from-right [screen n]
  (- (nth @screen-size 0) n))

(defn from-bottom [screen n]
  (- (nth @screen-size 1) n))

(defn draw-landmark-description! [screen]
  (when-let [lm (@landmarks (calc-world-coords @cursor-loc))]
    (s/put-string screen 0 0 (:description lm))))

(defn str-animal [a]
  (clojure.string/split-lines (with-out-str (clojure.pprint/pprint a))))

(defn draw-animal-stats! [screen]
  (when @paused
    (when-let [animal (@animals (calc-world-coords @cursor-loc))]
      (doseq [[i line] (map-indexed vector (str-animal animal))]
        (s/put-string screen 0 i line)))))

(defn draw-screen! [screen]
  (when @dirty
    (reset! dirty false)
    (letfn [(put-right [s y]
              (s/put-string screen (from-right screen (.length s)) y s))]
      (s/clear screen)
      (draw-terrain! screen)
      (draw-animals! screen)
      (draw-landmarks! screen)
      (draw-landmark-description! screen)
      (draw-animal-stats! screen)
      (s/put-string screen 0 (from-bottom screen 1) (str " " @day))
      (put-right " SILT  " 0)
      (put-right (str @world-temp "° ") 1)
      (put-right (str (count @animals) "  ") 2)
      (put-right
        (let [[x y] @window-loc] (str x " " y " "))
        (from-bottom screen 1))
      (apply s/move-cursor screen @cursor-loc)
      (s/redraw screen))))


; Input -----------------------------------------------------------------------
(defn move-cursor! [key]
  (dosync
    (commute cursor-loc
             #(let [[x y] %1]
                (case %2
                  (\w \k) [x (- y 1)]
                  (\s \j) [x (+ y 1)]
                  (\a \h) [(- x 1) y]
                  (\d \l) [(+ x 1) y]))
             key))
  (mark-dirty!))

(defn move-window!
  ([key] (move-window! key 1))
  ([key scale]
   (dosync
     (commute window-loc
              #(normalize-world-coords
                 (let [[x y] %1]
                   (case %2
                     :up [x (- y scale)]
                     :down [x (+ y scale)]
                     :left [(- x scale) y]
                     :right [(+ x scale) y])))
              key))
   (mark-dirty!)))

(defn reset-day! []
  (dosync (ref-set day 0)))

(defn reset-temperature! []
  (dosync (ref-set world-temp 0)))

(defn reset-tick-delay! []
  (reset! tick-delay 50))


(defn update-terrain! []
  (alter terrain tick-terrain))

(defn update-animals! []
  (alter animals tick-animals))

(defn update-landmarks! []
  (doseq [lm (vals @landmarks)]
    ((:action lm) lm)))

(defn update-world! []
  (dosync
    (update-animals!)
    (update-terrain!)
    (update-landmarks!)
    (commute day inc))
  (mark-dirty!))


(defn update-temperature! [amt]
  (dosync (commute world-temp + amt))
  (mark-dirty!))

(defn reset-world! []
  (reset-temperature!)
  (reset-tick-delay!)
  (reset-day!)
  (reset-window!)
  (reset-terrain!)
  (reset-animals!)
  (mark-dirty!))

(defn toggle-pause! []
  (swap! paused not))

(defn update-tick-delay! [amt]
  (swap! tick-delay + amt))

(defn handle-input! [screen]
  (while-let [key (s/get-key screen)]
    (case key
      :escape
      (reset! running false)

      (:up :down :left :right)
      (move-window! key 10)

      (\h \j \k \l \w \a \s \d)
      (move-cursor! key)

      \space
      (toggle-pause!)

      \1
      (update-world!)

      (\+ \=)
      (update-temperature! 1)

      (\- \_)
      (update-temperature! -1)

      \R
      (reset-world!)

      nil)))


; Game loop -------------------------------------------------------------------
(defn throttle! []
  (let [current (System/currentTimeMillis)
        sofar (- current @last-timestamp)
        desired (/ 1000 fps)
        wait (- desired sofar)]
    (reset! last-timestamp current)
    (if (> wait 0)
      (Thread/sleep wait))))

(defn tick []
  (Thread/sleep @tick-delay)
  (when (not @paused)
    (update-world!)))

(defn tick-loop []
  (while @running
    (tick)))


(defn main-loop []
  (reset! running true)
  (reset! paused false)
  (reset-world!)
  (future (tick-loop))
  (s/in-screen
    screen
    (apply handle-resize (s/get-size screen))
    (while @running
      (draw-screen! screen)
      (handle-input! screen)
      (throttle!))))


; Scratch ---------------------------------------------------------------------
(defn -main [& args]
  (main-loop))

(comment
  (reset! running false)
  (reset! running true)

  (reset-window!)
  (reset-terrain!)
  (reset-animals!)
  (set! *warn-on-reflection* true)
  (future (main-loop))

  )