Game of Life in Clojure

I’ve implemented a Game of Life in Clojure and would like to understand what I can do better, especially in terms of idiomatic Clojure (without losing readability/maintainability) of the current version.

This is my first “serious” Clojure program (beyond hello world).

Things that I’m specifically interested in:

  • Anything that’s not idiomatic and thus should be changed/improved.
  • Whether P (Point) should be a record ((defrecord Point (x y)) (defn P (x y) (Point. x y))) instead.
(ns com.nelkinda.game-of-life)

(def rules {
    :survival #{2 3}
    :birth #{3}
})

(defn P (x y) {:x x :y y})
(defn neighbors (cell)
    (def neighbors-of-origin #{(P -1 -1) (P -1 0) (P -1 1) (P 0 -1) (P 0 1) (P 1 -1) (P 1 0) (P 1 1)})
    (defn plus (cell1 cell2) {:x (+ (:x cell1) (:x cell2)) :y (+ (:y cell1) (:y cell2))})

    (map #(plus cell %) neighbors-of-origin))

(defn next-generation (life)
    (defn is-alive (cell) (contains? life cell))
    (defn is-dead (cell) (not (is-alive cell)))
    (defn dead-neighbors (cell) (filter #(is-dead %) (neighbors cell)))
    (defn live-neighbors (cell) (filter #(is-alive %) (neighbors cell)))
    (defn count-live-neighbors (cell) (count (live-neighbors cell)))
    (defn born (cell rules) (contains? (:birth rules) (count-live-neighbors cell)))
    (defn survives (cell rules) (contains? (:survival rules) (count-live-neighbors cell)))
    (defn dead-neighbors-of-living-cells () (mapcat #(dead-neighbors %) life))
    (defn surviving-cells () (filter #(survives % rules) life))
    (defn born-cells () (filter #(born % rules) (dead-neighbors-of-living-cells)))

    (into #{} (concat (surviving-cells) (born-cells))))

Test Implementation:

(use 'com.nelkinda.game-of-life)
(use 'clojure.test)

(def universe (atom #{}))

(defn parse
    ((spec) (into #{} (parse (clojure.string/split spec #"") 0 0 '())))
    ((spec x y set) (case (first spec)
        "." (parse (rest spec) (+ x 1) y set)
        "*" (parse (rest spec) (+ x 1) y (cons (P x y) set))
        "n" (parse (rest spec) 0 (+ y 1) set)
        (nil "") set
        (throw (AssertionError. (str "Wrong input: '" (first spec) "'"))))))

(Given #"the following universe:" (spec)
    (reset! universe (parse spec)))

(Then #"the next generation MUST be:" (spec)
    (assert (= (reset! universe (next-generation @universe)) (parse spec))))

Test Specification:

Feature: Conway's Game of Life

  Rules of Conway's Game of Life
  > The universe of the _Game of Life_ is an infinite, two-dimensional orthogonal grid of square cells.
  > Each cell is in one of two possible states:
  > * Alive aka populated
  > * Dead aka unpopulated
  >
  > Every cell interacts with its eight neighbors.
  > The neighbors are the cells that are horizontally, vertically, or diagonally adjacent.
  > At each step in time, the following transitions occur:
  > 1. Underpopulation: Any live cell with fewer than 2 live neighbors dies.
  > 1. Survival: Any live cell with 2 or 3 live neighbors survives on to the next generation.
  > 1. Overpopulation Any live cell with more than 3 live neighbors dies.
  > 1. Reproduction (birth): Any dead cell with exactly 3 live neighbors becomes a live cell.

  Scenario: Empty universe
    Given the following universe:
    """
    """
    Then the next generation MUST be:
    """
    """

  Scenario: Single cell universe
    Given the following universe:
    """
    *
    """
    Then the next generation MUST be:
    """
    """

  Scenario: Block
    Given the following universe:
    """
    **
    **
    """
    Then the next generation MUST be:
    """
    **
    **
    """

  Scenario: Blinker
    Given the following universe:
    """
    .*
    .*
    .*
    """
    Then the next generation MUST be:
    """
    ***
    """
    Then the next generation MUST be:
    """
    .*
    .*
    .*
    """

  Scenario: Glider
    Given the following universe:
    """
    .*
    ..*
    ***
    """
    Then the next generation MUST be:
    """
    *.*
    .**
    .*
    """
    Then the next generation MUST be:
    """
    ..*
    *.*
    .**
    """
    Then the next generation MUST be:
    """
    .*
    ..**
    .**
    """
    Then the next generation MUST be:
    """
    ..*
    ...*
    .***
    """

Repository URL in case you want to clone it yourself: https://github.com/nelkinda/gameoflife-clojure