From Callbacks to Reified Events

Hello again! It's time to dust off this blog and get some posts out.

A lot has changed in Zetawar over the past few months. I've discussed some of those changes in Kickstarter updates, but only at a high level. Here I'm going to dig into them in a bit more detail.

First, let's take a look at the event system.

If you recall, the demo version of Zetawar used simple callback event handlers. With that approach, calling an event handler looked like this:

[:button {:on-click #(handlers/repair conn %)}
 "Repair"]])

And the event handlers looked like this:

(defn repair [conn ev]
  (let [db @conn
         [q r] (first (d/q '[:find ?q ?r
                            :where
                            [?a :app/selected-q ?q]
                            [?a :app/selected-r ?r]]
                          db))]
     (game/repair! conn (app/current-game-id db) q r)
     (clear-selection conn nil)))

The important things to notice here are that the handler takes a reference, 'conn', and performs side effects. This approach has the virtue of being easy to implement, hence it's use in the demo, but it also has several drawbacks. Performing actions via side effects in the call to 'repair!' makes testing and interactive development difficult. Passing in the connection is also problematic since it means theoretically the value of the connection can change during the execution of the function. Though, in practice, JavaScript's single threaded execution model prevents this.

So how can we improve things? Thankfully, the re-frame framework has already solved this problem. We can just follow its lead. Instead of calling handlers directly, we'll dispatch event messages and modify the handlers to accept a DB value rather than a connection reference. Also, instead of performing side effects directly, we'll return a data structure describing the side effects we want executed.

With the new approach, dispatching an event looks like this:

[:button {:on-click #(dispatch [::events.ui/repair-selected])}
 "Repair"])

And handling an event looks like this:

(defmethod router/handle-event ::repair-selected
  [{:as handler-ctx :keys [db]} _]
  (let [game (app/current-game db)
        cur-faction-color (game/current-faction-color game)
        [q r] (app/selected-hex db)]
    {:dispatch [[:zetawar.events.game/execute-action
                 {:action/type :action.type/repair-unit
                  :action/faction-color cur-faction-color
                  :action/q q
                  :action/r r}]
                [::clear-selection]]}))
                
(defmethod router/handle-event ::execute-action
  [{:as handler-ctx :keys [db]} [_ action]]
  (let [game (app/current-game db)]
      ;; Irrelevant code omitted ...
      {:tx     (game/action-tx db game action)
       :notify [[:zetawar.players/apply-action :faction.color/all action]]})))
      

Because of changes to the AI system, the new code splits the event handler in two. The first handler extracts information about the current faction and selection from the DB and turns it into an 'execute-action' event which it returns along with a 'clear-selection' event. For the sake of brevity, most of the code has been omitted from the second handler, but the essentials are still there. It takes a DB value and an 'action' map and returns a transaction and an AI notification. Taken together the returned data describes the actions performed as side effects in the original handler.

So, now our handlers are pure functions. They take values as arguments and return values. This makes them much easier to execute interactively and test, but there's still something missing. How do the returned values change the application state? In the next post we'll examine this question in detail as we look at the event router.

Why Zetawar?

In my last post I promised to dig into the architecture of Zetawar. I'm still planning to do that, but before I get too deep in the technical weeds I thought it might be a good idea to explain why Zetawar exists.

The story begins earlier this year when I wanted to play a round of Weewar (an online turn based strategy game) with my wife. I hadn't played it in several years, but remembered it as a fun game and was looking for something turn based that wouldn't require us both to be available to play at exactly the same time. Unfortunately, I discovered EA had shut down Weewar. I also discovered another game, Elite Command, that suffered a similar fate. And while I don't blame either EA or the author of Elite Command for shutting them down — these things aren't free to maintain — it did seem like a shame that their respective player communities couldn't keep them running themselves.

That's when the idea for Zetawar was born. What if I made a similar web based game that was open source (Zetawar isn't yet, but it will be) and didn't require an application server? It would have some limitations due to the lack of a server side communication channel, but it would be something that players could invest in without worrying about a third party shutting it down.

So, I started writing it. Of course, once you start writing a game, it's impossible to resist trying to implement your favorite features, so Zetawar has a few extra goals beyond just being an open source, app server free, Weewar-like game.

First, it will have a first class bot interface. In college I enjoyed competing in programming competitions and I think programming AI bots that can play against each other is one of the most fun ways to do that. I also believe basing these contests on a fun game that students can enjoy playing with each other outside the programming competition can increase their interest and engagement.

Second, it's going to be as customizable as I can reasonably make it. Unit stats, terrain effects, tilesets, and even some of the game rules will be modifiable by players. This will allow players to help me with things like balancing unit stats and should keep the game interesting for a long time. It will also allow the game to serve as a laboratory of sorts for people interested in designing similar games. Want to make your own tactical strategy game? Prototype it first in Zetawar!

Third, Zetawar is currently and will continue to be written in ClojureScript. ClojureScript is the most enjoyable language I've found for writing web applications, and I want to both continue working with it and promote its adoption. Hopefully Zetawar can serve as a fun example people can point to when they talk about things written in ClojureScript and, once it's open sourced, can be an interesting code base for other ClojureScript enthusiasts to learn from and contribute to.

So that's what I'm hoping to accomplish with Zetawar. If any of those goals interest you, please follow @ZetawarGame to keep updated on how things are progressing, and if you have any feedback either send me a tweet or fill out the feedback form.

Technology Choices

Hello world! Welcome to the Zetawar blog!

A few people have expressed interest in learning a bit more about the technology behind Zetawar, so here goes. In this first blog post I will describe the key libraries I'm using and my rationale for choosing them.

ClojureScript

First off, it's important to note Zetawar is implemented entirely in ClojureScript. However, explaining that decision is more than I want to attempt in this post. Just take it as a given for now, and I'll try to dig into that more in the future.

Datascript

The Zetawar game and user interface state are stored in Datascript. Datascript is an in-memory database that supports ClojureScript and Clojure and implements a large portion of the Datomic API. Datomic in turn is a durable relational database that supports ACID transactions, declarative queries, and has a first class notion of time. Due to it being in-memory only, Datascript does not support durability. It also does not retain a history of all changes to the database like Datomic does, but it implements enough Datomic features to be quite useful in its own right.

I decided to use Datascript primarily for two reasons. First, Datascript provides extremely flexible query functionality. It supports Datalog, which provides SQL like queries, Datomic's pull API, which provides hierarchical data selection, and Datomic's entity API, which provides a lazy map like interface to database entities. This query flexibility means data can be structured based on logical relationships between data elements rather than how the data is accessed. Second, if in the future I want to make a version of the game that has a server component that needs to run some of the game logic, it should be relatively easy to port the game logic from Datascript to Datomic since their APIs are so similar.

Reagent

The Zetawar user interface is rendered with Reagent. Given that I wanted to make Zetawar web based (I'll discuss that decision more in a future post), the declarative React model on which Reagent is based is a natural fit. The game board interface is a pure function of the game state and the performance requirements of a turn based strategy game are not so great that rendering to the DOM (as opposed to Canvas or WebGL) is likely to be a bottleneck.

So why Reagent and not Om, Om.next, or Rum? Mostly it comes down to familiarity. I have used Reagent on other projects and found it to be effective and easy to use. It's also quite straightforward, with the addition of cursors in Reagent 0.5, to mimic the single atom approach of Om if desired. Om.next and Rum both look interesting, and I did seriously consider Om.next, but wasn't convinced that it provided enough benefits over Reagent (for this project anyway) to justify both the risk of using such a new library as well as the extra time it would take to get up to speed on it. I dismissed Rum on similar grounds. Though I do appreciate its minimalist design and hope to try it out on some future project.

Posh

Posh is the glue that connects Reagent to Datascript. It provides a mechanism for specifying Datascript queries and pulls that are only updated when data related to them has changed. Compared to the simplest alternative of rerunning queries on every transaction, this provides a huge performance boost.

As a new and not very well known library, it is likely the riskiest of these choices. But so far, it has worked extremely well. The only issue I have encountered was a bad interaction with the new lazy-by-default reactions in Reagent 0.6.0-alpha. Posh currently assumes that only a single transaction will take place between each evaluation of its reactions. With the new lazy-by-default reactions, if multiple transactions take place before the next render, some Posh queries may fail to run when related data changes. Thankfully, there is a simple workaround, run Reagent's flush in a Datascript transaction listener so that all reactions are evaluated after every transaction. This destroys the performance improvements possible with lazy transactions, but so far this hasn't been a significant issue. Also, it sounds like this problem will be fixed in the next version of Posh, so it may not even be an issue by the time the final version of Reagent 0.6.0 is released.

Conclusion

That's it for now. In my next post I'll dig into how these libraries are tied together in Zetawar.