Clojure Bites - Mazeboard 0

by FPSD — 2023-11-18


Intro

Welcome back to Clojure Bites!

This is a new format compared to the previous issues, instead of focusing on one specific topic, it will be more like a diary of what I am learning while resurrecting an old game I was working on some time ago. After spending a significant amount of time on the backend, I was stuck at the user facing part, basically for two reasons:

The first problem was not a big one, with the help of Rum as the React wrapper and Citrus for state management I was making progress even if slowly. The realtime communication part instead was a big blocker, it is ok if the frontend sucks as long as you can play the game! Time passed, I've lost interest and the project has been parked until now; at least I've gained a bit of experience with client/server realtime communication, lets see how far I will get this time :)

The plan now is to, mostly, start from from scratch and implement the game focusing on the frontend first, and back again to the backed later. A great source of inspiration is Parens of the dead, I like how they have structured their code to have a thin (dump?) client backed by a fully tested logic implemented in the backend; if something will look wrong or dumb in my implementation I'll be the guy to blame, not them ;)

The game

The game should be a sorts of rogue like board game where players explore a maze, with the goal to find a treasure and bring it back to to starting position. The board is composed by squares in a NxN grid, each square have randomly generated connections to nearby squares which can be used to move towards the treasure and back. Each turn players will flip a coin (or roll a dice) to get the possible action, move to a next square or rotate a square of the board (useful to make it harder to reach the goal to other players).

Frontend stack

The first change from the previous attempt is the build system, switching from figwheel-main to shadow-cljs. The main reason is that, to my understanding, it is easier to work with JS libraries with shadow that with figwheel; in another project I was using PixiJS and the version available in cljsjs repo was a bit outdated and I don't want to be in this situation again.

Having covered the build system it is now time to pick something to render the game. The first version of this project was built on Rum because it felt simple enough, Reagent was another (more popular) option that for some reason did not click for me, I totally overlooked Quiescent or Om(next). Newer candidates in this space are Helix or UIx if you after mature and up to date React wrappers, dumdom if you want to stick to virtual dom but you are not interested/experienced in React, and finally htmx if you want to stick to the hypermedia concept. All are strong and valid options for one reason or another but again I want to favor simplicity and dumdom being mostly focused on the rendering part, looks like the best candidate for my needs.

Last bit of tooling I am introducing to my stack is Portfolio, which is a sorts of Storybook for ClojureScript, in few words it makes it possible to test UI components in isolation and can be used as a documentation.

Setting up the development environment

To recap, I am using shadow-cljs as the build system, dumdom as the rendering library and portfolio to preview/debug components; the first step is to setup the project i.e. create a shadow-cljs.edn:

{:dependencies [[cjohansen/dumdom "2023.10.13"]
                [no.cjohansen/portfolio "2023.04.05"]
                [cider/cider-nrepl "0.41.0"]
                [refactor-nrepl/refactor-nrepl "3.9.0"]]

 :source-paths ["src/cljs" "src/cljc"] ;; where to look for sources

 :dev-http {8080 ["public" "classpath:public"]} ;; "classpath:public" is required to server Portfolio's resources

 :nrepl {:middleware [cider.nrepl/cider-middleware
                      refactor-nrepl.middleware/wrap-refactor]
         :port 50655} ;; customize the nrepl session that shadow-cljs will run 

 :builds {:game
          {:target :browser
           :output-dir "public/js"
           :asset-path "/js"
           :modules {:main {:init-fn mazeboard.ui.core/init}
                     :portfolio {:init-fn mazeboard.ui.portfolio/init
                                 :depends-on #{:main}}}}}}

Generally speaking this is just a common shadow-cljs config, and you should rely on the (extensive) shadow-cljs docs to create your own, but there are a couple of tweaks that are worth mentioning in the context of this app:

I have setup two pages (or views), one for the game and for portfolio, the first one will serve the game and the other one (guess what?) portfolio UI. Given the provided build settings we will have main.js for our app code and portfolio.js for portfolio; you want to have an html file embedding main.jsfor the main app and another embedding portfolio.js for (well...) the Portfolio UI. Given that the portfolio key in modules depends on main module the custom view (or html) for Portfolio must also include main to be able to access your app's components, something like:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Portfolio Canvas</title>
  </head>
  <body>
    <main id="canvas" role="main"></main>
    <script src="/js/main.js"></script>
    <script src="/js/portfolio.js"></script>
  </body>
</html>

With this setup it is possible to access the game code at the root of the dev webserver and preview components at /portfolio.html for quick dev and tests.

It is also worth mentioning that Portfolio UI can be customized quite a bit, for now my portfolio namespace is as as simple as referencing the scenes to render and start the UI adding my custom CSS to render my components:

(ns mazeboard.ui.portfolio
  (:require [portfolio.ui :as ui]
            [mazeboard.ui.scenes.game-scene]
            [mazeboard.ui.scenes.profile-scene]))

::mazeboard.ui.scenes.game-scene
::mazeboard.ui.scenes.profile-scene

(defn init
  []
  (ui/start! {:config {:css-paths ["/css/style.css"]}}))

Update 2023-11-18 12:30 CET

Thomas Heller pointed out in a comment on Reddit that the shadow-cljs build settings are not ideal because:

A new version of the :builds map is the following

{:game
 {:target :browser
  :output-dir "public/js"
  :asset-path "/js"
  :compiler-options {:infer-externs :auto :output-feature-set :es6}
  :modules {:main {:init-fn mazeboard.ui.core/init}}}
 
 :portfolio
 {:target :browser
  :output-dir "public/portfolio-js"
  :asset-path "/portfolio-js"
  :compiler-options {:infer-externs :auto :output-feature-set :es6}
  :modules {:main {:init-fn mazeboard.ui.portfolio/init}}}}

Now to run/watch both the game and portfolio we have to run

$ shadow-cljs watch game portfolio

At this point it will be possible to access the game at http://localhost:8080 and portfolio at http://localhost:8080/portfolio.html. This is how the game and portfolio will look when the build will finish (sorry for the terrible graphics...)

Game Game

Portfolio rendering a board row Portfolio rendering a board row

Portfolio rendering the full board Portfolio rendering the full board

First impressions

The first objective of this reboot was to setup a frontend first development environment, focusing on getting a view of the game board and preparing the work for future development. So far I am quite satisfied with the result, being able to develop components in isolation and preview them both in Portfolio and the app.

Future work

Next step would be to setup a playable version of the game, loosely based on the approach show in Parens of the dead but with a twist: instead of setting up a backend I am planning to simulate the command/event based approach solely in the frontend for now, putting the logic in cljc files so that, when it will come the time to have a backend, I can reuse most of it. So far the dev experience has been great and I am hyped to work on and see the results of the next steps!

Sources of this project can be found here.

Mazeboard related posts