Conway's Game of Life in Clojure: Add Glider to Grid

Viewed 97

I'm trying to implement the Game of Life in Clojure. I managed to implement the main logic, but now I'd like to provide a couple of functions that add an object (e.g. a Glider) to the grid.

Here are some functions that work fine so far:

(ns glider.core
  (:gen-class))

(defn new-grid
  "Creates a 2d grid with rows*cols."
  [rows cols]
  (partition cols (take (* rows cols) (repeat false))))

(defn get-coordinates
  "Returns the row/col coordinates of each 2d grid field."
  [grid]
  (let [rows (count grid)
        cols (count (get grid 0))]
    (partition cols (for [r (range rows) c (range cols)] [r c]))))

(defn set-at
  "Sets the field at row/col to state."
  [grid row col state]
  (assoc grid row (assoc (get grid row) col state)))
  1. new-grid creates a grid.
  2. get-coordinates turns the grid with true/false states into a grid of row/col coordinates.
  3. set-at sets a new state to the grid at row/col.

Now I'd like to add a glider. Here's my approach:

(defn add-glider
  "Adds a glider pattern to the lower-right quadrant of the grid."
  [grid]
  (let [rows (count grid)
        cols (count (get grid 0))
        row-offset (/ rows 2)
        col-offset (/ cols 2)
        glider [[false true false]
                [false false true]
                [true true true]]
        glider-coords (get-coordinates glider)
        grid-coords (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)]
    (apply (fn [[r c]] (set-at grid r c true)) grid-coords)))

First, I compute rows/cols, and also the mid-point of the grid (row-offset/col-offset). Second, I build up a glider in a 3x3 field with true/false states. I turn the glider into coordinates (glider-coords), which I then move by the offset (grid-coords).

Now I have a sequence of coordinates. I'd like to set the grid state to true on all these points.

I wrote this program to test it:

(defn -main
  "Creates a grid and adds a glider to it."
  [& args]
  (let [grid (new-grid 8 8)
        grid-with-glider (add-glider grid)]
    (println grid)
    (println grid-with-glider)))

Which gives me the following error message when I run it using lein:

$ lein repl
glider.core=> (-main)
Execution error (ClassCastException) at glider.core/add-glider$fn (core.clj:32).
class clojure.lang.PersistentVector cannot be cast to class java.lang.Number (clojure.lang.PersistentVector is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap')

Line 32 refers to the following code:

grid-coords (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)

My questions:

  1. What does that error message mean?
  2. Is my approach using apply sensible, or how should one tackle such an issue?
1 Answers

The error means that a Number is expected, but an array is provided. Why that happens, we can see if we capture the value of glider-coords, for example by doing the following while debugging:

defn add-glider
  "Adds a glider pattern to the lower-right quadrant of the grid."
  [grid]
  (let [rows (count grid)
        cols (count (get grid 0))
        row-offset (/ rows 2)
        col-offset (/ cols 2)
        glider [[false true false]
                [false false true]
                [true true true]]
        glider-coords (get-coordinates glider)
        _ (def glider-coords-debug glider-coords)
        grid-coords (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)]
    (apply (fn [[r c]] (set-at grid r c true)) grid-coords)))

Now, if you look at the contents, you will see:

> glider-coords-debug
(
  ([0 0] [0 1] [0 2])
  ([1 0] [1 1] [1 2])
  ([2 0] [2 1] [2 2])
)

And doing a similar trick on the next, reveals the value of r:

(map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)

In the first iteration, the value of r is [0 0], which is not a Number, as + expects, and that is the cause of the error message.

Regarding the apply there, it seems very wrong. I would rewrite that piece using reduce + assoc-in, as the idea is to iteratively modify the state by traversing the results. You even don't have to destructure the coordinates vector. Something like

(reduce #(assoc-in %1 %2 true)
            grid
            grid-coords))

There's also an error with get-coordinates- you should return only those indices you want to update (= these, where (get-in grid [r c]) is true).

Here is a fixed version:

(ns glider.core
  (:gen-class))

(defn new-grid
  "Creates a 2d grid with rows*cols."
  [rows cols]
  ;; makes sure its a vector of vectors, to get random access using assoc
  (mapv (fn [_] (into [] (repeat rows false)))
        (range cols))) 

(defn get-coordinates
  "Returns the row/col coordinates of each 2d grid field."
  [grid]
  (let [rows (count grid)
        cols (count (grid 0))]
    (for [r (range rows)
          c (range cols)
          :when (get-in grid [r c])]
      [r c])))

(defn add-glider
  "Adds a glider pattern to the lower-right quadrant of the grid."
  [grid]
  (let [rows (count grid)
        cols (count (grid 0))
        row-offset (/ rows 2)
        col-offset (/ cols 2)
        glider [[false true false]
                [false false true]
                [true true true]]
        grid-coords (->> (get-coordinates glider)
                         (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)])))]
    (reduce #(assoc-in %1 %2 true)
            grid
            grid-coords)))

(defn -main
  "Creates a grid and adds a glider to it."
  [& args]
  (let [grid (new-grid 8 8)
        grid-with-glider (add-glider grid)]
    (println grid-with-glider)))
Related