Skip to content

Instantly share code, notes, and snippets.

@devstopfix
Last active May 14, 2020 15:53
Show Gist options
  • Save devstopfix/4bbe8eb45a765198bba9cd11a95a26bf to your computer and use it in GitHub Desktop.
Save devstopfix/4bbe8eb45a765198bba9cd11a95a26bf to your computer and use it in GitHub Desktop.
Rich Hickey's ants simulator ported to Clojure on the BEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Ant sim ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Copyright (c) 2008 Rich Hickey. All rights reserved.
; The use and distribution terms for this software are covered by the
; Common Public License 1.0 (http://opensource.org/licenses/cpl.php)
; which can be found in the file CPL.TXT at the root of this distribution.
; By using this software in any fashion, you are agreeing to be bound by
; the terms of this license.
; You must not remove this notice, or any other, from this software.
;
; Original Clojure JVM code :- https://gist.github.com/michiakig/1093917
; Video of the original code :- https://www.youtube.com/watch?v=dGVqrGmwOAw
; Announcment :- https://groups.google.com/forum/#!msg/clojure/Zq76uzzkS8M/UzfXj9jKyw4J
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Ant sim ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Conversion to clojerl - Clojure running on the BEAM
;
; Download from http://clojerl.org/ make and run with
;
; bin/clje -m ants
;
; The code is ported directly. The only differences are:
;
; 1. the Clojure JVM code modelled Ants as threads, we use one process per Ant on the BEAM
; 2. Clojure updates state using STM and this wrapper:
;
; (defmacro dosync [& body]
; `(sync nil ~@body))
;
; This allows updates to two agents in a transaction. We use optimistic locking.
; A process may inspect the world and make a decision. If two ants come to the
; same decision (pick up the same piece of food, or move to the same square) when
; they come to action the decision they will send a message to the world process
; which serializes the actions - the actions will then succeed or fail and the ant
; will adjust acordingly. ie the first ant will get the food, the second will have
; to re-evaulate their decision on their next turn.
;
; (c) 2020 Devstopfix and Juan Facorro
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Ant sim ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ns ants)
;dimensions of square world
(def dim 80)
;number of ants = nants-sqrt^2
(def nants-sqrt 7)
;number of places with food
(def food-places 35)
;range of amount of food at a place
(def food-range 100)
(def home-off (quot dim 4))
;evaporation rate
(def evap-rate 0.99)
(def animation-sleep-ms (quot 1000 10)) ; 5 fps
(def ant-sleep-ms (quot 1000 25)) ; 25 fps
(def evap-sleep-ms 1000)
(ns ants.sim (:use ants))
(defrecord cell [food pher home]) ; may also have ant
(defn new-world [dim]
(apply vector
(map (fn [_]
(apply vector (map (fn [_] (cell. 0 0 false))
(range dim))))
(range dim))))
(defn evaporate [cell] (update cell :pher * evap-rate))
(defn world-fn
[world]
(receive*
#erl [:place pid loc]
(let [[x y] loc
cell (-> world (nth x) (nth y))]
(erlang/send pid #erl[:cell cell])
(recur world))
#erl [:drop-food loc]
(let [[x y] loc]
(recur (update-in world [x y] update :food inc)))
#erl [:move-ant pid ant from to]
(let [[x1 y1] from [x2 y2] to]
(if (-> world (nth x2) (nth y2) (:ant))
(do
(erlang/send pid #erl[:moved false])
(recur world))
(do
(erlang/send pid #erl[:moved true])
(-> world
(update-in [x1 y1] update :pher inc)
(update-in [x1 y1] dissoc :ant)
(update-in [x2 y2] assoc :ant (select-keys ant [:dir :food]))
(recur)))))
#erl [:take-food pid loc]
(let [[x y] loc]
(let [available-food (-> world (nth x) (nth y) (:food))]
(if (pos? available-food)
(do
(erlang/send pid #erl[:taken true])
(recur (update-in world [x y] update :food dec)))
(do
(erlang/send pid #erl[:taken false])
(recur world)))))
:snapshot
(do
(erlang/send :graphics #erl[:world world])
(recur world))
:evaporate
(let [map-vec (comp vec map)]
(recur (map-vec (partial map-vec evaporate) world)))))
(defn place [loc]
(erlang/send :world #erl[:place (erlang/self) loc])
(receive* #erl[:cell cell] cell))
(def home-range (range home-off (+ nants-sqrt home-off)))
(defn home-places [] (for [x home-range y home-range] [x y]))
(defn make-home [world]
(reduce
(fn [world loc]
(-> world
(update-in loc assoc :home true)
(update-in loc assoc :ant {:dir (rand-int 4)})))
world
(home-places)))
(defn make-food [world]
(reduce
(fn [world n]
(let [loc [(rand-int dim) (rand-int dim)]
f (-> food-range (rand-int) (inc))]
(update-in world loc assoc :food f)))
world
(range food-places)))
(defn setup [] (-> dim (new-world) (make-home) (make-food)))
;dirs are 0-7, starting at north and going clockwise
;these are the deltas in order to move one step in given dir
(def dir-delta {0 [0 -1]
1 [1 -1]
2 [1 0]
3 [1 1]
4 [0 1]
5 [-1 1]
6 [-1 0]
7 [-1 -1]})
(def dir-count (count dir-delta))
(defn bound
"returns n wrapped into range 0-b"
[b n]
(let [n (rem n b)]
(if (neg? n)
(+ n b)
n)))
(defn wrand
"given a vector of slice sizes, returns the index of a slice given a
random spin of a roulette wheel with compartments proportional to
slices."
[slices]
(let [total (reduce + slices)
r (rand total)]
(loop [i 0 sum 0]
(if (< r (+ (slices i) sum))
i
(recur (inc i) (+ (slices i) sum))))))
(defn delta-loc
"returns the location one step in the given dir. Note the world is a torus"
[[x y] dir]
(let [[dx dy] (dir-delta (bound dir-count dir))]
[(bound dim (+ x dx)) (bound dim (+ y dy))]))
(defn rank-by
"returns a map of xs to their 1-based rank when sorted by keyfn"
[keyfn xs]
(let [sorted (sort-by (comp float keyfn) xs)]
(reduce (fn [ret i] (assoc ret (nth sorted i) (inc i)))
{} (range (count sorted)))))
; Ant process tracks the location of an ant, and controls the behavior of
; the ant at that location
(defrecord ant [loc dir]) ; may also have food
(defn take-food [loc]
(erlang/send :world #erl[:take-food (erlang/self) loc])
(receive* #erl[:taken f] f))
(defn move-ant [ant from to]
(erlang/send :world #erl[:move-ant (erlang/self) ant from to])
(receive* #erl[:moved b] b))
(defn ant-place [ant dir-delta]
"Find a place that an ant could move to"
(-> (:loc ant)
(delta-loc (dir-delta (:dir ant)))
(place)))
(defn turn [ant amt]
(let [dir (bound dir-count (+ (:dir ant) amt))]
(assoc ant :dir dir)))
(defn turn-about [ant] (turn ant (quot dir-count 2)))
(defn try-move-ant [ant]
(let [loc-ahead (delta-loc (:loc ant) (:dir ant))]
(if (move-ant ant (:loc ant) loc-ahead)
(assoc ant :loc loc-ahead)
ant)))
(defn behave [ant]
(let [loc (:loc ant)
p (place loc)
ahead (ant-place ant identity)
ahead-left (ant-place ant dec)
ahead-right (ant-place ant inc)
places [ahead ahead-left ahead-right]]
(if (:food ant)
; going home
(cond
(:home p)
(do
(erlang/send :world #erl[:drop-food loc])
(dissoc ant :food))
(and (:home ahead) (not (:ant ahead)))
(try-move-ant ant)
:else
(let [ranks (merge-with +
(rank-by #(if (:home %) 1 0) places)
(rank-by :pher places))]
(([try-move-ant #(turn % -1) #(turn % 1)]
(wrand [(if (:ant ahead) 0 (ranks ahead))
(ranks ahead-left) (ranks ahead-right)]))
ant)))
; foraging
(cond
; found food in the wild?
(and (pos? (:food p)) (not (:home p)))
(if (take-food loc)
(-> ant
(assoc :food true)
(turn-about))
ant)
(and (pos? (:food ahead)) (not (:home ahead)) (not (:ant ahead)))
(try-move-ant ant)
:else ; wander
(let [ranks (merge-with +
(rank-by :food places)
(rank-by :pher places))]
(([try-move-ant #(turn % -1) #(turn % 1)]
(wrand [(if (:ant ahead) 0 (ranks ahead)) (ranks ahead-left) (ranks ahead-right)]))
ant))))))
(defn ant-fn
[state]
(timer/sleep ant-sleep-ms)
(recur (behave state)))
(defn evaporator-fn
[]
(timer/sleep evap-sleep-ms)
(erlang/send :world :evaporate)
(recur))
(defn run []
(let [pid (erlang/spawn #(world-fn (setup)))]
(erlang/register :world pid)
(erlang/spawn #(evaporator-fn))
(doseq [loc (home-places)]
(let [a (ant. loc (rand-int dir-count))]
(erlang/spawn #(ant-fn a))))))
(ns ants.graphics (:use ants))
(def title "Ants Clojure/BEAM 2020")
;scale factor for pheromone drawing
; 20.0 is the default,
; nil disables painting)
(def pher-scale 20.0) ; 20.0
;scale factor for food drawing
(def food-scale 30.0)
;pixels per world cell
(def scale 5)
(def grid-px (* dim scale))
(defn fill-cell [dc x y brush pen]
(wxDC/setBrush dc brush)
(wxDC/setPen dc pen)
(wxDC/drawRectangle dc #erl[(* x scale) (* y scale) scale scale]))
(def s2 (quot scale 2))
(def s4 (dec scale))
(defn render-ant [dc ant x y state]
(let [[hx hy tx ty] ({0 [s2 0 s2 s4]
1 [s4 0 0 s4]
2 [s4 s2 0 s2]
3 [s4 s4 0 0]
4 [s2 s4 s2 0]
5 [ 0 s4 s4 0]
6 [ 0 s2 s4 s2]
7 [ 0 0 s4 s4]}
(:dir ant))
pen (if (:food ant) (:ant-with-food-pen state) (:ant-pen state))]
(wxDC/setPen dc pen)
(wxDC/drawLine dc #erl[(+ hx (* x scale)) (+ hy (* y scale))]
#erl[(+ tx (* x scale)) (+ ty (* y scale))])))
(defn render-food [dc p x y brush pen]
(let [alpha (int (min 255 (* 255 (/ (:food p) food-scale))))
colour #erl[0x8F 0xB5 0xFE alpha]]
(wxBrush/setColour brush colour)
(fill-cell dc x y brush pen)))
(defn render-pher [dc p x y brush pen]
(let [alpha (int (min 255 (* 255 (/ (:pher p) pher-scale))))
colour #erl[0x91 0xDC 0x47 alpha]]
(wxBrush/setColour brush colour)
(fill-cell dc x y brush pen)))
(defn render-place [dc p x y state brush pen]
(when (and pher-scale (pos? (:pher p))) (render-pher dc p x y brush pen))
(when (pos? (:food p)) (render-food dc p x y brush pen))
(when-let [ant (:ant p)] (render-ant dc ant x y state)))
(defn render-bg [dc brush pen]
(wxDC/setPen dc pen)
(wxDC/setBrush dc brush)
(wxDC/drawRectangle dc #erl[0 0 grid-px grid-px]))
(defn render-home [dc brush]
(let [x (* home-off scale) y x
w (* nants-sqrt scale) h w
pen (wxPen/new #erl[0x58 0x81 0xD8])]
(wxDC/setPen dc pen)
(wxDC/setBrush dc brush)
(wxDC/drawRectangle dc #erl[x y w h])
(wxPen/destroy pen)))
(defn render [state]
(let [dc (:bitmap-dc state)
brush (:cell-brush state)
pen (:cell-pen state)]
(render-bg dc (:bg-brush state) pen)
(doseq [[x row] (map-indexed vector (:world state))]
(doseq [[y cell] (map-indexed vector row)]
(render-place dc cell x y state brush pen)))
(render-home dc (:home-brush state))))
(defn make-frame []
(let [server (wx/new)
frame (wxFrame/new server -1 title #erl( #erl[:size #erl[grid-px grid-px]]))
panel (wxPanel/new frame 0 0 grid-px grid-px)]
(wxWindow/fit frame)
(wxWindow/setBackgroundStyle panel 2) ; flicker free
(wxFrame/connect frame :close_window)
(wxFrame/connect panel :paint #erl(:callback))
(wxFrame/centre frame)
(wxFrame/show frame)
[frame panel]))
(defn* handle_info
([#erl[:world world] state]
(render state)
(wxFrame/refresh (:canvas state))
#erl[:noreply (assoc state :world world)])
([_ state]
#erl[:noreply state]))
(defn* handle_event
[#erl[:wx id #erl[:wx_ref r :wxFrame f] #erl"" #erl[:wxClose :close_window]]
state]
(erlang/halt 0)
#erl[:stop :closed state])
(defn handle_sync_event
[wx obj state]
(let [dc (wxPaintDC/new (:canvas state))]
(wxDC/drawBitmap dc (:bitmap state) #erl[0 0])
(wxPaintDC/destroy dc))
:ok)
(defn snapshot-fn
[]
(timer/sleep ants/animation-sleep-ms)
(erlang/send :world :snapshot)
(recur))
(defn fat-pen [r g b]
(let [pen (wxPen/new #erl[r g b])]
(wxPen/setWidth pen 2)
pen))
; Run simulation and graphics until the user closes the window
(defn init [_]
(let [[frame canvas] (make-frame)
bitmap (wxBitmap/new grid-px grid-px)
state {:canvas canvas
:bitmap bitmap
:bitmap-dc (wxMemoryDC/new bitmap)
:ant-pen (fat-pen 0 0 0)
:ant-with-food-pen (fat-pen 0x58 0x81 0xF8)
:bg-brush (wxBrush/new #erl[238 238 238])
:border-pen (wxPen/new #erl[238 238 238])
:home-brush (wxBrush/new #erl[221 221 221 0])
:cell-brush (wxBrush/new #erl[0 0 0])
:cell-pen (wxPen/new #erl[0 0 0 0] #erl(#erl[:width 0]))
:world []}]
(erlang/register :graphics (erlang/self))
(erlang/spawn #(snapshot-fn))
#erl[frame state]))
(ns ants)
(defn -main []
(ants.sim/run)
(wx_object/start :ants.graphics {} #erl())
(receive*))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment