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.