TopHome
<2024-09-13 Fri>techlisp

Game of Life in Love using Fennel

I am always on the lookout for a new Lisp to try out and Fennel was on front-page of HN. I have never worked with Lua before (which Fennel translates to), so I wanted to try my hand at some sort of Fennel hello world.

As a start, I tried the AwesomeWM with the intention to configure it using Fennel. That didn't happen - the default was in Lua, it has been a few days, and I am having fun with Awesome, I must say.

Note: I am a long time user of dwm, until recently when I switched to Wayland and tried using Hyprland. But, screen recording with OBS works well on X11 and crashes on Wayland, so I am back to X. I was trying to clobber together a tiling window manager setup on the default Gnome, but let me tell you, that is not working out. So, here I am on Awesome.

Anyway, I have a cool Awesome setup going on, but still no channel to use Fennel. This is where, Love comes in. I find out (from a HN thread) that Love + Fennel is often used in the Lisp Game Jams.

Ok, but, what do I build. One of the Fennel tutorials somewhere pointed to building Conway's Life on the TIC-80 and liked that idea.

Here it is. My first ever Fennel program - a Life implementation in 90 lines of Fennel using the Love framework.

(fn create-grid [x y]
  (let [grid {}]
    (for [i 1 x]
      (tset grid i {})
      (for [j 1 y]
        (tset (. grid i) j false)))
    grid))

(fn draw-grid [grid size grid_off]
  (each [i val (ipairs grid)]
    (each [j cell (ipairs val)]
      (love.graphics.rectangle (if cell "fill" "line")
                               (+ (. grid_off 1) (* (- i 1) size))
                               (+ (. grid_off 2) (* (- j 1) size))
                               size size))))

(fn toggle-grid [grid i j]
  (let [curr (. (. grid i) j)]
    (tset (. grid i) j (not curr))))

(fn live-neighbour-count [grid i j]
  (var cnt 0)
  (let [x (length grid)
        y (length (. grid 1))]
    (for [i_ (- i 1) (+ i 1)]
      (for [j_ (- j 1) (+ j 1)]
        (if (and (not (and (= i_ i) (= j_ j)))
                 (. (. grid (+ (% (- i_ 1) x) 1)) (+ (% (- j_ 1) y) 1)))
            (set cnt (+ cnt 1)))))
    cnt))

(fn run-life [grid]
  (var new_grid {})
  (each [i val (ipairs grid)]
    (tset new_grid i {}) 
    (each [j cell (ipairs val)]
      (tset (. new_grid i) j (if cell
                      (case (live-neighbour-count grid i j)
                        (where [c] (< c 2)) false
                        2 true
                        3 true
                        _ false)
                      (if (= (live-neighbour-count grid i j) 3)
                          true
                          false)))))
  new_grid)

(fn love.mousepressed [x y button istouch press]
  (let [i (+ (math.floor (/ (- x (. _G.grid_off 1)) _G.cell_size)) 1)
        j (+ (math.floor (/ (- y (. _G.grid_off 2)) _G.cell_size)) 1)]
    (if (= 1 button)
      (toggle-grid _G.grid i j))))

(fn love.load []
  (set _G.grid (create-grid 20 20))
  (set _G.grid_off [50 50])
  (set _G.cell_size 20)
  (set _G.count 0)
  (set _G.update 0.5)
  (set _G.round 1)
  (set _G.run false))

(fn love.update [dt]
  (set _G.count (+ _G.count dt))
  (if (> _G.count _G.update)
      (do
        (set _G.count 0)
        (if _G.run
            (do 
              (set _G.round (+ 1 _G.round))
              (set _G.grid (run-life _G.grid)))))))

(fn love.draw []
  (love.graphics.print (string.format "Game of Life: %s \n <space> to start/stop, <r> to reset, <q> to quit"
                                      (if _G.run
                                          "running"
                                          "paused"))
                       10 10)
  (love.graphics.print (string.format "Round: #%s" _G.round) 500 10)
  (draw-grid _G.grid _G.cell_size _G.grid_off))

(fn love.keypressed [key]
  (case key
    "q" (love.event.quit)
    "r" (if (not _G.run)
            (love.load))
    "space" (do
              (set _G.run (not _G.run))
              (if _G.run
                  (set _G.round 1)))))

So, the way to get this running is:

  1. Save this into main.fnl.
  2. Compile it to lua using: fennel --compile main.fnl > main.lua.
  3. Run it with love, using: love ..

Apparently Fennel is inspired from Clojure (the [] brackets kinda give that away), though I wouldn't know since I haven't tried out Clojure either. Interestingly, the creator of Fennel went on to create Janet, a neat little Lisp that I do enjoy.

Anyway, here is the good, bad and ugly of this experience.

  1. Ugly: Fennel, rightly so, has no direct support for Globals. Unfortunately, it seemed that the Love approach is based pretty much on globals, so, we use the `G` table pointing to global variables all over the place. :shrug:
  2. Good: the installation is smooth, given Lua, Fennel and Love are all available in standard package managers. A definite positive when you are dabbling with alternative languages.
  3. Good: tables being the primary data structure. Very simple, in a pythonic sense.
  4. Ugly: the Fennel syntax to get/set stuff from tables is not clean, in comparison to base Lua.
  5. Bad: No immutable data structures in Fennel. But weird coming from a Lisp background, but sure.
  6. Good: Love's callback mechanism based structure. Very straightforward to get started and nice to use.
  7. Bad: Love's state management needs global sharing, making Fennel+Love very un-lispy in nature.
  8. Bad: Lua's 1 based indexing takes a bit of time to get used to. Especially, with things like wrapping which is such an easy op with 0 based indexing.
  9. Good: I didn't struggle as much as would have thought with the mixed syntax (the use of {} and [] in a Lisp). This is one of the major reasons I have been avoiding Clojure. Don't get me wrong, I still think that a Lisp should be pure (), but the mixed version was not too bad…

All in all, a nice experience. Looking forward to playing with Fennel more. Like they say (do they?), you can never have enough Lisps.