Jake Goulding

Game of Life in Clojure

I’ve been trying to learn Clojure recently, so I wrote Conway’s Game of Life. I’m sure that the code is highly non-idiomatic, but I’d rather get it out into the wild, instead of sitting on my disk.

Note that I have inline tests (the simple println statements) mostly because I wasn’t trying to figure out how to properly test in Clojure (yet).

(life.clj) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
(require '[clojure.set :as set])

;; I feel that there are too many explicit conversions with `set`, but
;; I'm not sure how to remove them.

;; Reusable test cells
(def center {:x 0 :y 0})
(def top-left {:x -1 :y 1})
(def top-right {:x 1 :y 1})
(def bottom-left {:x -1 :y -1})
(def bottom-right {:x 1 :y -1})

(defn neighbors
  "Finds all cells that are neighbors of the given cell"
  [cell cells]
  (letfn [(neighbor? [poss]
            (and (>= (poss :x) (dec (cell :x)))
                 (<= (poss :x) (inc (cell :x)))
                 (>= (poss :y) (dec (cell :y)))
                 (<= (poss :y) (inc (cell :y)))
                 (not (= poss cell))))]
    (set (filter neighbor? cells))))

(println "* neighbors")
;;
(println (= #{bottom-left top-right}
            (neighbors center [bottom-left center top-right])))
;;
(println (= #{center}
            (neighbors bottom-left [bottom-left center top-right])))

(defn kill-off
  "Removes cells that do not have enough neighbors to stay alive"
  [cells]
  (set (remove #(let [n_neighbors (count (neighbors % cells))]
                  (or (< n_neighbors 2)
                      (> n_neighbors 3)))
               cells)))

(println "* kill-off")

;; Cell with no neighbors dies
(println (= #{} (kill-off [center])))

;; Cell with one neighbor dies
(println (= #{} (kill-off [center top-left])))

;; Cell with two neighbors lives
(println (= #{center} (kill-off [center top-left top-right])))

;; Cell with three neighbors lives
(println (= #{center} (kill-off [center top-left top-right bottom-left])))

;; Cell with four neighbors dies
(println (= #{} (kill-off [center top-left top-right bottom-left bottom-right])))

(defn all-neighbors
  "Finds the cells in a 3x3 area around the given cell"
  [{x :x y :y}]
  (for [x-mod (range -1 2)
        y-mod (range -1 2)]
    {:x (+ x x-mod) :y (+ y y-mod)}))

(defn interesting-cells
  "Finds all cells that could potentially change on a given step"
  [cells]
  (reduce set/union (map all-neighbors cells)))

(defn fringe
  "Finds all cells that are currently dead but could come alive"
  [cells]
  (set/difference (set (interesting-cells cells))
                  cells))

(defn come-alive
  "Finds all cells that are currently dead but will come alive"
  [cells]
  (set (remove #(not (== 3 (count (neighbors % cells))))
               (fringe cells))))

(println "* come-alive")

;; Empty cell with no neighbors stays dead
(println (= #{} (come-alive [])))

;; Empty cell with one neighbor stays dead
(println (= #{} (come-alive [top-left])))

;; Empty cell with two neighbors stays dead
(println (= #{} (come-alive [top-left top-right])))

;; Empty cell with three neighbors comes alive
(println (= #{center} (come-alive [top-left top-right bottom-left])))

;; Empty cell with four neighbors stays dead
(println (= #{} (come-alive [top-left top-right bottom-left bottom-right])))

(defn step
  "Returns the cells that are still alive after or came alive during a single time step"
  [cells]
  (set/union (kill-off cells)
             (come-alive cells)))

;; Printing functions

;; Have a 21x21 window into the world
(def min-x -10)
(def max-x  10)
(def min-y -10)
(def max-y  10)

(defn print-cell [cell]
  (if cell
    (print "X")
    (print " ")))

(defn print-row [cells]
  (dorun (for [x (range min-x (inc max-x))]
    (print-cell (first (filter #(= x (% :x)) cells)))))
  (println))

(defn print-cells [cells]
  (println "---")
  (dorun (for [y (reverse (range min-y (inc max-y)))]
           (print-row (filter #(= y (% :y)) cells))))
  (println "---"))

;; Two mildly-interesting start states

(def blinker [{:x -1 :y 0} {:x 0 :y 0} {:x 1 :y 0}])
(def glider [{:x 2 :y 2} {:x 2 :y 1} {:x 2 :y 0} {:x 1 :y 0} {:x 0 :y 1}])

(def initial-state glider)

(def life (iterate step initial-state))

(dotimes [i 50]
  (print-cells (nth life i)))