This posts walks through building a multiplayer board game using Clojure and HTMX.
Live app link: chain-reaction-app.fly.dev. ( The first page load could take around a minute to load. )
The game is called Chain Reaction. It’s a 2 player game, with each player is assigned a color, and then make moves on a 2d board. The board starts out empty. A player picks a cell, which adds their orb to it. If they pick it again, the number of orb increases. If it crosses a certain threshold, it explodes onto other cells, which itself can explode to other cells. Players take turns picking their cells, and the goal is to have all orbs be of your own color. Refer the link above for the exact rules, or checkout the repo for a demo video.
This project is an excercise to make something with Clojure and HTMX. I wanted to get familiar with the libraries used to create web apps in Clojure.
This post is not a tutorial, but can be a reference or a high level guide for building a similar app. Please look into full stack frameworks such as biff or kit to launch projects quickly with features such as routing, authentication, websockets etc already done.
I used these frameworks in the beginning but was missing a lot of basic knowledge. I wasn’t familiar with the ideas and libraries used by these frameworks. I had trouble adding features as per my needs. I needed a small subset of the functionality these frameworks provide. So I went through their source code, documentation, tutorials etc to make the app from scratch. This post details the libraries, patterns, learning resources etc I used while building this project.
The source code is at kaepr/chain-reaction.
Project Structure #
resources/
public/
js/
css/
...
database/
...
src/
ui.clj
routes.clj
system.clj
handler.clj
middleware.clj
game.clj
dev/
user.clj
deps.edn
The application is a standard multi page web application.
Users can create accounts / log in using simple username
and password
.
Users can play matches
with other players, by creating a room and sending them a room number.
Once two users have joined a room, they can start to play.
I used websockets for the multiplayer part of the app.
I used SQLite for storing user and match data.
The resources
directory also stores any public assets, such as js
or css
files.
The css files are generated using Tailwind CSS cli tool.
Configuration #
The server needs some configuration things before it can start. Such as PORT on which it will receive traffic. The database type and connection. These values will come from the host systems environment variables. Refer this article Configuration in Clojure by Ivan Grishaev for an indepth guide to configuration.
For this project I used Aero.
I have specified my configuration in the below .edn
file.
;; resources/config.edn
{:server
{:port #long #or [#env PORT 3000]}
:db
{:dbtype "sqlite"
:dbname #or [#env DB_NAME "local.db"]}}
It picks PORT
and DB_NAME
from the environment.
If it’s not present then defaults to respective values based on #or
form.
The config is read using read-config
function.
(ns ...
(:require [aero.core :as aero]))
(-> "config.edn"
io/resource
aero/read-config)
Components #
Servers rely on several stateful resources. E.g. the database connection. These services gets created once, and gets passed to other parts of the program.
How these services are started and shutdown also needs to be defined. There’s also ordering between these services, for e.g. a database connection must be present before the http server is started. And the http server must be stopped before the database connection is stopped. Refer How to Structure a Clojure Web App 101 which explains the problem of managing these stateful resources.
Like the article above, I use integrant library to define stateful services and their dependencies.
(defn read-config []
{:app/config (-> "config.edn"
io/resource
aero/read-config)})
(defmethod ig/expand-key :app/config
[_ {:keys [server db] :as _opts}]
{:app/server {:port (:port server)
:handler (ig/ref :app/handler)}
:app/handler {:db (ig/ref :app/db)
:*rooms (ig/ref :app/rooms)}
:app/rooms nil
:app/db db})
I found the above approach for using Aero + Integrant from Ryan Martin’s article “Build and Deploy a Web Apps with Clojure and Fly.io”.
The config read and used as options in the ig/expand-key
method.
This function then returns the system map, which lists all the services in this app.
If a service, for e.g. the server needs the db, it’s added as a key in the map with ig/ref
.
Now the database connection will be initialized before server is started.
The logic for starting and destryoing is defined with ig/init-key / halt-key
methods.
(defmethod ig/init-key :app/server
[_ {:keys [handler port] :as _opts}]
(let [server (jetty/run-jetty
handler
{:port port
:join? false})]
(log/info "Server started on port: " port)
server))
(defmethod ig/init-key :app/handler
[_ opts]
(app-handler/app opts))
(defmethod ig/init-key :app/db
[_ opts]
(let [jdbcUrl (connection/jdbc-url opts)
datasource (connection/->pool HikariDataSource {:jdbcUrl jdbcUrl})
_ (do
(log/info "Database migrations started")
(.migrate
(.. (Flyway/configure)
(dataSource datasource)
(locations (into-array String ["classpath:database/migrations"]))
(table "schema_version")
(load)))
(log/info "Database migrations are done"))]
datasource))
(defmethod ig/halt-key! :app/db
[_ datasource]
(.close datasource))
(defmethod ig/halt-key! :app/server
[_ server]
(log/info "Stopping server !")
(.stop server))
The main method ( used when the application is deployed ) reads the config.
Creates the system and initialized all the services.
If the JVM is shutting down, then it calls halt
method, which turns all the services down.
(defn -main [& _args]
(let [s (-> (system/read-config)
(ig/expand)
(ig/init))]
(.addShutdownHook
(Runtime/getRuntime)
(new Thread #(ig/halt! s)))))
REPL #
The nicest thing about building this project was using the REPL. After some initial setup which was new to me, I never really had to leave the editor.
When I update the logic to any function, I don’t need to have a separate process (such as nodemon) running which restarts my server. I can change dependencies without stopping the server. In cases I have to restart, I can do that from the editor aswell.
dev/user.clj
namespace gets loaded anytime a REPL is started.
The app can be started / stopped by evaluating the forms below.
(ns user
(:require
[chain-reaction.system :as system]
[clojure.tools.namespace.repl :refer [set-refresh-dirs]]
[integrant.core :as ig]
[integrant.repl :refer [go halt reset reset-all set-prep!]]))
(set-prep!
(fn []
(ig/expand (system/read-config))))
(set-refresh-dirs "src" "resources")
(comment
(go)
(halt)
(reset)
(reset-all))
(comment
;; hotload libs
#_(require '[clojure.deps.repl] :refer [add-lib add-libs sync-deps])
#_(add-libs '{org.clojure/data.json {:mvn/version "2.5.1"}})
#_(sync-deps))
There’s also a namespace for starting / stopping the tailwind process which builds CSS.
(ns css
(:require [clojure.java.process :as process]))
(defonce css-watch-ref (atom nil))
(defn css-watch []
(apply process/start
{:out :inherit
:err :inherit} ; prints output to same process as server
["tailwindcss"
"-c" "resources/tailwind.config.js"
"-i" "resources/tailwind.css"
"-o" "resources/public/css/main.css"
"--watch"]))
(defn start-css-watch []
(reset! css-watch-ref (css-watch)))
(defn stop-css-watch []
(when-some [css-watch @css-watch-ref]
(.destroyForcibly css-watch)
(reset! css-watch-ref nil)))
(comment
(start-css-watch)
(stop-css-watch))
I based my deps.edn
and REPL flow using the below resources.
I did have to manually refresh my browser if I made a UI change. Even this can be automated. It’s already implemented in biff. It watches source directory, and reloads the browser window in case of any changes. As I had very few UI components, I didn’t add it, but it would nice for larger projects.
Handling Requests #
A server is listens to requests. It does the parsing, and converts HTTP to a Java object representation. I used jetty as the server. It’s the most common choice so just went with it.
This Java object is converted to a standardized request map, which can be used in Clojure. There’s a adapter library for jetty, which converts the java object to a request map as per the ring specification. This request map is then used by handlers / middleware functions.
- Aditya Athalye | Clojuring the web application stack: Meditation One
- Eric Normand | Clojure web app Step by Step
The above two articles explain in depth on how to create a Clojure Ring app from scratch. Understanding the middleware / handler pattern was crucial. I added custom middleware functions for adding database object to each request, authenticated routes etc.
For routing requests based on path , I used reitit.
;; middleware functions
(defn wrap-db [handler db]
(fn [req]
(handler (assoc req :db db))))
(defn wrap-render-rum [handler]
(fn [req]
(let [response (handler req)]
(if (vector? response)
(-> {:status 200
:body (str "<!DOCTYPE html>\n"
(rum/render-static-markup response))}
(resp/content-type "text/html"))
response))))
;; app
(defn app [{:keys [db] :as opts}]
(ring/ring-handler
(routes/routes opts)
#'routes/default-handler
{:middleware [#(mid/wrap-ring-defaults % db)]}))
;; routes definition
(defn routes [{:keys [*rooms db]}]
(ring/router
[["/" {:get #'handler/home-page
:middleware [mid/wrap-redirect-logged-in]}]
["/sign-up" {:get {:handler #'handler/sign-up-page
:middleware [mid/wrap-redirect-logged-in]}
:post {:handler #'handler/sign-up}}]
["/sign-in" {:get {:handler #'handler/sign-in-page
:middleware [mid/wrap-redirect-logged-in]}
:post #'handler/sign-in}]
["/logout" {:post #'handler/logout}]
["/room" {:middleware [mid/wrap-logged-in]}
["/create" {:post {:handler #'handler/create-room}}]
["/join" {:post {:handler #'handler/join-room}}]
["/play/:id" {:get {:handler #'handler/room-page
:middleware [ring-ws/wrap-websocket-keepalive]}}]]
["/dashboard" {:get {:handler #'handler/dashboard-page}
:middleware [mid/wrap-logged-in]}]]
{:data {:db db
:middleware [#(mid/wrap-db % db)
#(mid/wrap-rooms % *rooms)
mid/wrap-render-rum]}}))
Some handler functions simply return hiccup. The wrap-render-rum
middleware catches that and returns HTML back.
Other handlers use the request body and the database connection for actual app logic. For example, below is the sign up handler.
(defn sign-up [{:keys [db params] :as _req}]
(let [{:keys [username password confirmpassword]} params]
(if (not= password confirmpassword)
(ui/sign-up-form {:error "Passwords are not matching."})
(let [hashed-password (bh/derive password)
{:keys [user okay error]} (db/create-user db {:username username
:password hashed-password})]
(if okay
{:status 200
:headers {"hx-redirect" "/dashboard"}
:session (select-keys (into {} user) [:id :username])}
(ui/sign-up-form {:error error}))))))
User Sign In / Sign Up #
I followed this video by Andrey Fadeev. Refer the video for creating a sign in / sign up flow with Clojure and HTMX.
Some issues I faced.
Sessions not persisting across reload
This was due to how wrap-session
get’s loaded.
My original routes were instantiating a new session store on each request.
A user could create account and login, but then after a reload they were losing access.
Each request created a new store, so the session before was being lost.
ring.middleware.session/wrap-session doesn’t work with reitit #205
The fix was mounting the store outside of the router. Now that only one instance was created, sessions were now persisted.
;; using jdbc store for ring-session in ring wrap-defaults
(defn wrap-ring-defaults [handler db]
(-> handler
(ring-defaults/wrap-defaults (assoc-in ring-defaults/site-defaults
[:session :store]
(jdbc-ring-session/jdbc-store
db
{:table :session_store})))))
;; middleware at top level
(defn app [db]
(ring/ring-handler
(routes/routes db)
#'routes/default-handler
{:middleware [#(mid/wrap-ring-defaults % db)]}))
Database Exceptions
I wanted to add error handling for exceptions a database query might throw.
(defn create-user [db {:keys [username password]}]
(try
(let [user (jdbc/execute!
db
(sql/format {:insert-into [:users]
:columns [:username :password]
:values [[username password]]
:returning :*})
{:builder-fn rs/as-unqualified-kebab-maps})]
{:okay true
:user user})
(catch SQLException e
{:okay false
:error (if (= (.getErrorCode e) 19) ; checking if unique constraint failed
"Username already exists."
"Something went wrong.")})
(catch Exception _e
{:okay false
:error "Something went wrong."})))
jdbc/execute
call is wrapped in a try catch, and returning either :user
or :error
.
I was having trouble in identifying what exact error was thrown.
For example, if somebody tries to sign up with a already existing username, then it throws an exception. ( as username
has unique
constraint. )
Going by the above message and SQLITE Error Codes, I expected the code to be 2067
, but the exception error code was 19
, which is the code denoting any constraint violation.
I am guessing the correct exception code is there, it’s probably present in exception chain.
Right now, I went with using the generic error code.
If I want to be specific in the future, I could do string matching or find the root exception in the chain.
It looks like handling such exceptions will involve manually seeing the error case by case. Different drivers might throw custom exceptions, and are not guaranteed to follow SQL error codes convention. Reference next.jdbc/tips-and-tricks.
*e
var stores the last exception thrown. Helpful for quickly viewing the stack trace.
The Game #
The game itself is quite simple. This is the core game loop, which is run every time a player makes a move.
(defn explode [board color row col]
(let [cell (get-in board [row col])
mass (first cell)
max-mass (max-mass row col)]
(if (<= (inc mass) max-mass)
(assoc-in board [row col] (make-cell (inc mass) color))
(reduce (fn [board [r c]] (explode board color r c))
(assoc-in board [row col] (make-cell))
(neighbors row col)))))
The current number of orbs are checked. If adding one more leads to overflow, then this function is recursively called on the neighbors. Otherwise increase the orb count.
I tried to make this function tail call recursive, but wasn’t able to do it. I am not sure if it’s even possible. Recursive functions which call themselves 0 or 1 times are simple to convert. But the above program, can call itself 0, 2, 3 or 4 times recursively. Maybe there’s a way to convert it, but it might make the code more complex. I really like how simple this code is.
Once the board only has orbs of one given color, then the game is finished.
WebSockets #
room.clj, entity.clj, handler.clj
Users have the option of either creating or joining a room.
Once they create or join a room, they are redirected to
.../room/play/:roomId
These rooms are stored in an atom. It’s initialized during app startup.
;; system.clj
(defmethod ig/init-key :app/rooms
[_ _]
(atom {}))
(defmethod ig/halt-key! :app/rooms
[_ *rooms]
(reset! *rooms {}))
This atom is a just a map, mapping room-id
( a unique string ) to room
map.
It has information of the player inside the room.
The state of the game, the board data etc.
(defn ->player [id username socket]
{:id id
:username username
:socket socket})
(defn ->room [id]
{:id id
:state :not-started
:board (game/empty-board)
:moves 0
:p1 nil
:p2 nil
:log nil})
When a player joins a room, they send a websocket request. Their username and their socket object gets saved in room map. This stored socket is later used to send messages from server to the client.
HTMX Websockets require the server to send messages in form of HTML. The htmx websockets client library then replaces the content based on id
.
(defn render-board [board]
[:div {:class "flex justify-center p-6"
:id "room-game-board"} ; used as identifier to swap board
[:div {:class "w-full max-w-2xl rounded-lg p-4"}
[:div {:class "flex mb-1"}
[:div {:class "w-10"}]
(for [col-idx (range 0 6)]
[:div {:class "flex-1 text-center text-lg font-semibold text-gray-700"}
(str col-idx)])]
(for [row-idx (range 0 9)]
[:div {:class "flex"}
[:div {:class "w-10 flex items-center justify-center text-lg font-semibold text-gray-700"}
(str row-idx)]
[:div {:class "flex-1 grid grid-cols-6"}
(for [col-idx (range 0 6)
:let [cell (get-in board [row-idx col-idx])]]
(render-cell row-idx col-idx cell))]])]])
I initially felt it would be limiting, but it turned out to be so much simpler. I have the entire state of the room in server, so no logic is required on client side.
To send messages from client to server, use ws-send
attribute on an element.
Using hx-vals
we can add any extra data we want.
The value set in hx-vals
will become stringified JSON.
It’s parsed to a clojure map in server handler function.
[:div {:class "..."
:hx-vals (json/write-str {:event-type :cell-click
:row row-idx
:col col-idx})
:ws-send ""
:name "cell-click"
:hx-trigger "click"
:style {:aspect-ratio "1/1"}}
(...)]
;; Server handler function
{:message (fn [socket message]
(let [{:keys [event-type] :as event} (update-keys (json/read-str message) keyword)
player (entity/->player id username socket)]
(condp = event-type)))}
Using atoms for storing game data #
Although it is simple to implement, using atoms for this application might be bad in terms of scaling.
(Not that I am expecting to have any users, but humor me).
Every move is processed inside a swap!
function so that it has a consistent view of the game board.
Once the new state is calculated, it’s written to the specific room.
{"room-id-1" {:board [[...]]}
"room-id-2" {:board [...]}}
Each room or game played is independent in nature.
A move made in one room, should not delay the processing of moves in another room.
But due to the way swap!
works, each move ( from any room ) has the possibility of delaying moves for every other room.
Swap uses compare and swap, which modifies a value only if it hasn’t changed since last time it was last read.
If the board in room-id-1
is updated, then the entire value of the map is updated.
The swap!
checks the overall value.
This means that any move which was still processing, now needs to be recalculated.
Even if the move was for room-id-2
.
It would have been fine if the swap!
call was fast to calculate, but it’s not.
Although it’s a pure function, it’s very CPU intensive.
The board has 6 x 9 cells, each of which explode. In the worst case scenario, let’s say every cell explodes, and recursively explodes. The middle cells explode onto neighboring 4 cells, so it would be O(4^54) calls in worst case.
This will never happen in a real game. The game logic can also be modified to finish the game early. But the problem would still persist. Calculating next state of the board should be independent between rooms.
Deployment #
The Dockerfile first stage is to get the tailwind cli, and generate CSS. Second is building the clojure app.
- Practicalli | Multi-Stage Dockerfile for Clojure
- Ryan Martin | Build and Deploy Web Apps With Clojure and Fly.io
I deployed the app to fly.io with the help from above articles. Turns out that I am still on fly.io ’s legacy hobby plan, so should be able to keep this app deployed for a while.