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:
- Save this into
main.fnl. - Compile it to lua using:
fennel --compile main.fnl > main.lua. - 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.
- 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:
- 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.
- Good: tables being the primary data structure. Very simple, in a pythonic sense.
- Ugly: the Fennel syntax to get/set stuff from tables is not clean, in comparison to base Lua.
- Bad: No immutable data structures in Fennel. But weird coming from a Lisp background, but sure.
- Good: Love's callback mechanism based structure. Very straightforward to get started and nice to use.
- Bad: Love's state management needs global sharing, making Fennel+Love very un-lispy in nature.
- 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.
- 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.