Fun Life

Computer simulations can be excellent teachers. This essay explores the beauty of one such simulation - Conway's Game of Life aka simply Life. We will describe the game rules, see it in action and reflect on it. Finally, I share notes from my implementation. We also get a sense of how animations can be fun i.e implemented in a functional style :)



How does it work?

Imagine a 4 by 4 grid with 16 cells like the one above. Each cell may be in an "alive" state or a "dead" state as the game begins. This is the "beginning of life" configuration or "gen0". From gen0, each subsequent generation, gen1, gen2 and so on evolves based on 3 rigid rules.

The cells in the example snapshot above are laid out in a mini chess board configuration. That is half (8 cells) are dead in white and half are alive in black.

Rules

  1. Any live cell with 2 or 3 live neighbours survives to the next generation.

  2. All other live cells perish as if by overpopulation or underpopulation.

  3. Any dead cell with 3 live neighbours becomes a live cell as if by reproduction.





The grid dimensions are flexible. In this implementation, it is 30 by 30. Thus there are 900 cells and the animation evolves 5 generations a second. That is, this game produces a new generation every 200 ms. Each frame in the animation is a new generation. To say it simply, we animate this @ 5 fps. Play the game. The only control you have is to reset the game. When you do that, you simply start over with another random gen0. Technically such games are called 0 player games - a fancy name for screensavers.


Once you try out a few sessions, the inherent beauty of this game pops out. Like how sensitive the output is to initial conditions. Or how unpredictable/ uncontrollable the output is. Some initial states stagnate, some flourish while others oscillate. It's fascinating how such complex emergent behaviour can unfold in front of you from 3 tiny rules.


Also, since it's such an abstract simulation, you cannot stop your mind from wandering over its implications to any domain that is governed by rules and some form of progressive evolution. Ranging from natural selection, flight of birds, geology, paths of rivers, societies, tribes, teams and even to personal self help (with habits as rules). Really an eye opener, no wonder it's aptly named the Game of Life !


Notes from implementation

I used the excellent ClojureScript animation library quil to implement this. Quil itself is a clojure implementation over Processing-js library

The first thing to do is figure out how to represent the game. Any generation can be represented as a vector of booleans. There is no other information needed to represent the alive/dead status of the entire grid. This is how a 2 by 4 grid can be represented in a chessboard configuration - as a vector with 8 booleans.

My standard disclaimer. Don't be thrown off if you are seeing ClojureScript syntax for the first time. ClojureScript uses Lisp's s-expression syntax. This syntax has huge payoffs when you use it to build UIs. You can get a quick tour of the syntax here.

(let [rows 2 cols 4]
    (vec (apply concat
                (for [x (range rows) y (range (/ cols 2))]
                  (let [v (if (odd? x) true false)]
                    [v (not v)])))))

=> [false true false true true false true false]

Representing the board as data turned out to be easy. Next, there needs to be a "binding" from the game's data to its actual rendering. Lets see how we can render a real 8 by 8 chess board in quil.

(defn draw-example-5 []
  ;draw a chess board
  (q/background 255)
  (let [rows 8 cols 8
        cell-lifes-vec
        (vec (apply concat
                    (for [x (range rows) y (range (/ cols 2))]
                      (let [v (if (odd? x) true false)]
                        [v (not v)]))))
        cell-width  (quot (q/width) cols)
        cell-height  (quot (q/height) rows)]

    (doseq [[i elem] (map-indexed vector cell-lifes-vec)]
      (let [y-pos (quot i cols)
            x-pos (mod i cols)]

        ;if elem color the cell black (populated), else leave it white (unpopulated)
        (q/fill
         (if elem 0 255))

        (q/rect (* x-pos cell-width) (* y-pos cell-height) cell-width cell-height)

        ;contrast for text
        (q/fill
         (if elem 255 0))

        (q/text i (+ 7 (* x-pos cell-width)) (+ (* y-pos cell-height) 15))))))


Following the above snippet, you orient yourself as to how our vector indices work in relation to the x and y coordinates to anchor the 64 q/rects and q/texts.


Finally lets animate the game.

Quil has an excellent middleware named "fun-mode" for animating things in functional style. The way it works is you set up the game's initial representation as the starting point data. Then you specify just 1 function, that takes the data you currently have in its argument and gives back the data that you want rendered in the next frame as its return value. That's it ! This is almost like how the famous method reduce available in most languages would work over a collection in functional style, eliminating the need to implement loops. The difference is the analogous reduce of fun-mode is always arity 1, unlike the typical reduce that has an arity of 2

In other words, with just 1 function, you render frames ad infinitum in functional style. Damn too good to be true !!

(def rows 30)
(def cols 30)

(def reset (atom false))

(defn random-state []
  {:cell-life-status (vec
                      (repeatedly (* rows cols) #(if (pos? (rand-int 2)) true false)))})

(def state (random-state))

(defn setup []
  (q/frame-rate 5)
  (q/color-mode :hsb)
  (q/no-stroke)
  state)

(defn get-8-neighbours [idx cell-lifes-vec]
  (let [left-edge?  (zero? (mod idx cols))
        right-edge?  (zero? (mod (inc idx) cols))]
    [;up
     (get cell-lifes-vec (- idx cols))

    ;right up
     (when (not right-edge?)
       (get cell-lifes-vec (inc (- idx cols))))

    ;right
     (when (not right-edge?)
       (get cell-lifes-vec (inc idx)))

    ;right down
     (when (not right-edge?)
       (get cell-lifes-vec (inc (+ cols idx))))

    ;down
     (get cell-lifes-vec (+ cols idx))

    ;left down
     (when (not left-edge?)
       (get cell-lifes-vec (dec (+ cols idx))))

    ;left
     (when (not left-edge?)
       (get cell-lifes-vec (dec idx)))

    ;left up
     (when (not left-edge?)
       (get cell-lifes-vec (dec (- idx cols))))]))

(defn gen-next [idx itm cell-lifes-vec]
  (let [live-count (get (frequencies (get-8-neighbours idx cell-lifes-vec)) true false)]
    (if  itm
       ;any live cell with 2 or 3 live neighbours survives to next generation.
      ;all other live cells perish as if by overpopulation or underpopulation
      (if (or (< live-count 2) (> live-count 3)) false true)
       ;any dead cell with three live neighbours becomes a live cell as if by reproduction.
      (if (= 3 live-count) true false))))

(defn update-state [state]
  (if @reset (do (reset! reset false) (random-state))
      (assoc state :cell-life-status
             (vec
              (map-indexed
               (fn [idx itm] (gen-next idx itm (:cell-life-status state)))
               (:cell-life-status state))))))

(defn draw-state [state]
  (q/background 240)
  (let [cell-width (quot (q/width) cols)
        cell-height (quot (q/width) rows)]
    (doseq [[i elem] (map-indexed vector (:cell-life-status state))]
      (let [y-pos (quot i cols)
            x-pos (mod i cols)]
        (q/fill
         (if elem 0 255))
        (q/rect (* x-pos cell-width) (* y-pos cell-height) cell-width cell-height)))))


update-state is the function to drool over. It takes the old state as an argument and returns the new state to be rendered in the next frame. Though a loop is not explicitly specified, the animation basically is just a loop whose body is this function. We need to make sure we keep things simple here and don't write any inefficient or blocking code.

the gen-next function, codifies the 3 rules.

It uses the get-8-neighbours helper function to enquire the dead/alive status across all 8 neighbours of a given cell.

The draw-state function is similar to the implementation in draw-example-5 we saw in rendering the chess board. All drawing concerns are restricted to this function. Basically this is how we bind the data to a frame and "paint" the frame's visual. Again, we need to ensure we don't write any inefficient or blocking code here.

The reset functionality is coded using reagent hiccup.. :::clojure (defn example-2 [] [:div.buttons [:button.button.is-dark {:on-click #(reset! reset true)} "Reset"]])

It is that much easier to implement UI using reagent hiccup. All we need to do is manipulate a global atom reset accessible from quil's update-state. Thus through a single atom (fancy name for global variable in clojure) we integrate react (the layer underneath reagent) and quil without much ceremony.