Monday, February 27, 2012

Clojure Game of Life

This is a Conway's Game of Life in functional style written in Clojure.
You would start it in the repl as

 (game "GUN44.LIF")

where "GUN44.LIF" is a LIF file -- you can find LIF files all over the
internet, e.g. Paul Calahan's great collection
http://www.radicaleye.com/lifepage/


The lif reader demonstrates how to read and process a file in Clojure.



(ns gol
  (:import (java.awt Color Dimension)
           (javax.swing JPanel JFrame Timer JOptionPane)
           (java.awt.event ActionListener KeyListener WindowAdapter))
  (:use lif))

(def frame-millis 30)
(def cell-size 5)
(def width 250)
(def height 160)

(def glider ^ints #{ [1 0] [2 1] [0 2] [1 2] [2 2] })
(def gun ^ints (process-file "../../lifep/GUN44.LIF"))

(defn get-neighbors[ ^ints [ox oy] ]
  (for [x (range (- ox 1) (+ ox 2))
        y (range (- oy 1) (+ oy 2)) ]
    [x y] ))

(defn collect-neighbors [field]
  (distinct (mapcat get-neighbors field)))

(defn count-alive-neighbors [ ^ints field]
  (fn [ cell ] (count (filter field (get-neighbors cell) ))))

(defn survives? [field]
 (let [count-alive-neighbors (count-alive-neighbors field)]
  (fn [ cell ]
    (let [n (count-alive-neighbors cell)]
      (or (= n 3) (= n 4))))))

(defn birth? [field]
  (let [count-alive-neighbors (count-alive-neighbors field)]
    (fn [ cell ]
      (= 3 (count-alive-neighbors cell)))))

(defn apply-rules [filterfunc]
  (fn [ field ]
    (let [dead-cells (remove field (collect-neighbors field))
          survives?  (survives? field)
          birth?     (birth? field)]
      (set (concat
            (filterfunc survives? field)
            (filterfunc birth?    dead-cells))))))

(defn pfilter [pred list]
  (map second (filter first (pmap (fn [cell] [(pred cell) cell]) list))))

(def apply-rules-p (apply-rules pfilter))
(def apply-rules-n (apply-rules filter))

(defn update-field [field]
  (swap! field apply-rules-n))

(defn point-to-screen-rect [pt] 
  (map #(* cell-size %) [(pt 0) (pt 1)  1 1]))

(defn fill-point [^java.awt.Graphics g pt color]
  (let [[x y width height] (point-to-screen-rect pt)]
   (.fillRect g x y width height)))

(defn paint-game [^java.awt.Graphics g field]
  (let [center [(bit-shift-right width 1) (bit-shift-right height 1)]
        color (Color. 15 160 70)]
    (.setColor g color)
    (doseq [point field]
      (fill-point g (vec (map + center point)) color) )))

(defn game-panel [frame field]
(proxy [JPanel ActionListener KeyListener] []  
  (paintComponent [^java.awt.Graphics g]
   (proxy-super paintComponent g)
   (paint-game g @field))
  (actionPerformed [e]
    (.repaint this)
    (println "active cells:" (count @field))
    (time (update-field field)))
  (keyPressed [e] )
  (keyReleased [e])
  (keyTyped [e])
  (getPreferredSize []
    (Dimension. (* (inc width) cell-size)
                (* (inc height) cell-size)))
))

(defn frame-closing [timer] (proxy [WindowAdapter] []
  (windowClosing [e] (do (println "stopped") (.stop timer)))))

(defn game
  ([filename]
  (let [frame (JFrame. "Game of Life")
        field (atom (^ints process-file filename) )
        panel (game-panel frame field)
        timer (Timer. frame-millis panel)]
  (doto panel
   (.setFocusable true)
   (.addKeyListener panel))
  (doto frame
   (.addWindowListener  (frame-closing timer))
   (.add panel)
   (.pack)
   (.setVisible true))
  (.start timer) [timer]))
  ([] (game "../../lifep/GUN44.LIF")))


The lif reader:
(ns lif
  (:import (java.io BufferedReader FileReader))
  (:use  [clojure.string :only (split)]
         [clojure.java.io :only (reader)])
  )

(defn collect-data [[x y] line]
  (remove nil?  (map #(when (= %1 \*) [%2 y]) line (iterate inc x))))

(defn is-data? [line]
  (or (= \. (first line)) (= \* (first line))))

(defn char-to-long [c]
  (Long. (str c)))

(defn is-coords? [line]
  (let [tokens (split line #"\s")]
    (= "#P" (tokens 0))))

(defn get-coords [line]
  (let [tokens (split line #"\s")]
    [(char-to-long (tokens 1)) (char-to-long (tokens 2)) ]))

(defn process-lines [[x y] lines accu]
  (let [line (first lines)]
    (cond (empty? lines)  accu   
          (is-data? line)
          (recur [x (inc y)] (rest lines)
                 (concat accu (collect-data [x y] line)))
          (is-coords? line)
          (recur (get-coords line )(rest lines) accu)
          :else  (recur [x y] (rest lines) accu))))

(defn process-file [file-name]
  (with-open [rdr (reader file-name)]
    (set (process-lines [0 0]  (line-seq rdr) nil))))



Have fun!



update:

I cleaned the mess up a bit. Still not as concise as cgrand's solution. Hope I find the time to check it out and see if it's faster than mine.

2 comments:

  1. See also:

    http://clj-me.cgrand.net/2011/08/19/conways-game-of-life/

    which is a very compact implementation of the game engine, though wihtout a GUI...

    ReplyDelete
  2. cgrand's solution also seems fast ... I'll try it out and let you know.
    Thanks!
    thomas

    ReplyDelete