Last active
May 14, 2020 15:53
-
-
Save devstopfix/4bbe8eb45a765198bba9cd11a95a26bf to your computer and use it in GitHub Desktop.
Rich Hickey's ants simulator ported to Clojure on the BEAM
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 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