Conway's Game of Life


(require '[reagent.core :as r :refer [atom]])

(def app-state (atom {:status "not started"}))

(def world-size 10)

(defn wrap-coordinate [n]
  (mod n world-size))

(defn wrap-square [[x y]]
  [(wrap-coordinate x) (wrap-coordinate y)])

(defn wrap-squares [lives]
  (map wrap-square lives))

(def lives
  (atom #{}))

  (defn neighbors [[x y]]
    (for [dx [-1 0 1]
          dy [-1 0 1]
        :when (not (= 0 dx dy))]
    [(+ x dx) (+ y dy)]))

(defn step [lives]
  (set (for [[pos live-neighbors] (frequencies (mapcat neighbors lives))
             :when (or (= 3 live-neighbors)
                       (and (contains? lives pos)
                            (= 2 live-neighbors)))]
         pos)))

(defn step! []
  (reset! lives (step @lives)))

(defn rect-cell [x y]
  [:rect.cell
   {:x (+ 0.05 x) :width 0.9
    :y (+ 0.05 y) :height 0.9
    :fill "white"
    :stroke-width 0.025
    :stroke "black"}])

(defn dead [x y]
  [:rect
   {:width 0.9
    :height 0.9
    :fill "white"
    :x (+ 0.05 x)
    :y (+ 0.05 y)
    :on-click
    (fn click-square [e]
      (if (= (:status @app-state) "not started")
        (swap! lives conj [x y])
        (js/alert "Hands off! You are not God.")))}])

(defn life [x y]
  [:rect
   {:width 0.9
    :height 0.9
    :fill "purple"
    :x (+ 0.05 x)
    :y (+ 0.05 y)
    :on-click
    (fn click-square [e]
        (swap! lives disj [x y]))}])

(defn render-board []
  (into
    [:svg.board
      {:view-box (str "0 0 " world-size " " world-size)
       :shape-rendering "auto"
       :style {:max-height "500px"}}]
           (for [x (range world-size)
                 y (range world-size)]
             [:g
              [rect-cell x y]
              (let [x-pos (mod x world-size) y-pos (mod y world-size)]
                (if (some #{[x-pos y-pos]} (wrap-squares @lives))
                [life x y]
                [dead x y]))])))

(defn game []
  [:center
   [:h1 "Conway's Game of Life"]
   [:div [:button
    {:on-click
     (fn step-click [e]
       (swap! lives step)
       (swap! app-state assoc-in [:status] "started"))}
    "Step"]
    [:button
     {:on-click
      (fn step-click [e]
        (reset! lives #{})
        (swap! app-state assoc-in [:status] "not started"))}
     "Reset"]]
   [:div [:button
    {:on-click
     (fn step-click [e]
       (reset! lives
         #{[3 4] [4 4] [5 4] [5 3] [4 2]}))}
    "Glider"]
    [:button
     {:on-click
      (fn step-click [e]
        (reset! lives
          #{[4 3] [5 3] [7 2] [5 5] [8 3] [7 4] [5 4]
            [6 5] [4 4] [7 3] [6 2] [6 4]}))}
     "Spaceship"]
   [:button
    {:on-click
     (fn step-click [e]
       (reset! lives
         #{[5 7] [4 3] [3 4] [5 3] [3 2] [4 6] [5 5]
           [5 6] [5 2] [3 1] [5 1] [4 5] [3 3] [5 4]
           [3 5] [4 8] [4 1] [3 6] [5 8] [3 8] [4 4] [3 7]}))}
    "Penta-decathlon"]]
   [:div [render-board]]
   [:p (str "Current residents:" @lives)]])

[game]