-
-
Save nivertech/41bdb6216cbb72e276096796ef4bac6a 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) 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 | |
; | |
;🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜 | |
; | |
; Conversion to clojerl - Clojure running on the BEAM | |
; | |
; Download from http://clojerl.org/ make and run with | |
; | |
; bin/clje ants.clje | |
; | |
; 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. | |
; 3. render-place does not paint the pheremone trail - there are too many rectangles | |
; per frame! However the code exists and can be uncommented. | |
; | |
; (c) 2020 Devstopfix | |
; | |
;🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜🐜 | |
(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 5)) ; 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)) | |
(def world-fn | |
(fn* [world] | |
(receive* | |
#erl [:place pid loc] | |
(let [[x y] loc | |
cell (-> world (nth x) (nth y))] | |
(erlang/send pid #erl[:cell cell]) | |
(world-fn world)) | |
#erl [:drop-food loc] | |
(let [[x y] loc] | |
(world-fn (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]) | |
(world-fn 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])) | |
(world-fn))))) | |
#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]) | |
(world-fn (update-in world [x y] update :food dec))) | |
(do | |
(erlang/send pid #erl[:taken false]) | |
(world-fn world))))) | |
:snapshot | |
(do | |
(erlang/send :graphics #erl[:world world]) | |
(world-fn world)) | |
:evaporate | |
(let [map-vec (comp vec map)] | |
(world-fn (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)))))) | |
(def ant-fn | |
(fn* [state] | |
(do | |
(timer/sleep ant-sleep-ms) | |
(-> state (behave) (ant-fn))))) | |
(def evaporator-fn | |
(fn* [] | |
(do | |
(timer/sleep evap-sleep-ms) | |
(erlang/send :world :evaporate) | |
(evaporator-fn)))) | |
(defn run [] | |
(let [pid (erlang/spawn (fn* [] (world-fn (setup))))] | |
(erlang/register :world pid) | |
(erlang/spawn (fn* [] (evaporator-fn))) | |
(doseq [loc (home-places)] | |
(let [a (ant. loc (rand-int dir-count))] | |
(erlang/spawn (fn* [] (ant-fn a))))))) | |
(ns ants.graphics (:use ants)) | |
(def title "Ants Clojure/BEAM") | |
;scale factor for pheromone drawing | |
(def pher-scale 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] | |
(let [pen (wxPen/new (wxBrush/getColour brush))] | |
(wxDC/setBrush dc brush) | |
(wxDC/setPen dc pen) | |
(wxDC/drawRectangle dc #erl[(* x scale) (* y scale) scale scale]) | |
(wxPen/destroy pen))) | |
(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] | |
(let [alpha (int (min 255 (* 255 (/ (:food p) food-scale)))) | |
colour #erl[0x8F 0xB5 0xFE alpha] | |
brush (wxBrush/new colour)] | |
(fill-cell dc x y brush) | |
(wxBrush/destroy brush))) | |
(defn render-pher [dc p x y] | |
(let [alpha (int (min 255 (* 255 (/ (:pher p) pher-scale)))) | |
colour #erl[0x91 0xDC 0x47 alpha] | |
brush (wxBrush/new colour)] | |
(fill-cell dc x y brush) | |
(wxBrush/destroy brush))) | |
(defn render-place [dc p x y state] | |
; (when (pos? (:pher p)) (render-pher dc p x y)) | |
(when (pos? (:food p)) (render-food dc p x y)) | |
(when-let [ant (:ant p)] (render-ant dc ant x y state))) | |
(defn render-bg [dc brush] | |
(let [pen (wxPen/new (wxBrush/getColour brush))] | |
(wxDC/setPen dc pen) | |
(wxDC/setBrush dc brush) | |
(wxDC/drawRectangle dc #erl[0 0 grid-px grid-px]) | |
(wxPen/destroy pen))) | |
(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 frame] | |
(let [dc (wxClientDC/new frame)] | |
(render-bg dc (:bg-brush state)) | |
(render-home dc (:home-brush state)) | |
(doseq [[x row] (map-indexed vector (:world state))] | |
(doseq [[y cell] (map-indexed vector row)] | |
(render-place dc cell x y state))) | |
(wxClientDC/destroy dc))) | |
(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) | |
(wxFrame/centre frame) | |
(wxFrame/show frame) | |
panel)) | |
(def graphics-fn | |
(fn* [state] | |
(receive* | |
#erl[:world world] | |
(do | |
(wxFrame/refresh (:frame state)) | |
(graphics-fn (assoc state :world world))) | |
#erl[:wx id frame other #erl[:wxPaint :paint]] | |
(do | |
(render state frame) | |
(graphics-fn state)) | |
#erl[:wx id #erl[:wx_ref r :wxFrame f] #erl"" #erl[:wxClose :close_window]] | |
true))) | |
(def snapshot-fn | |
(fn* [] | |
(do | |
(timer/sleep ants/animation-sleep-ms) | |
(erlang/send :world :snapshot) | |
(snapshot-fn)))) | |
(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 | |
(ants.sim/run) | |
(let [frame (make-frame) | |
state {:frame frame | |
: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]) | |
:world []}] | |
(erlang/register :graphics (erlang/self)) | |
(erlang/spawn (fn* [] (snapshot-fn))) | |
(graphics-fn state)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment