Building a Clojurescript game: Rendering

This post is the fifth in the series of posts focused on the design and implementation of a port in ClojureScript of the game named Tribolo. You can try the game at the following address.

Our first post described the game and identified the following responsibilities, each of them will be developed in a different post:

  1. Game logic: the rules governing transitions between game states
  2. Rendering: transforming the current game state into HTML and SVG
  3. Store: the in-memory data-base holding the application state
  4. Game loop: coordinates and control of the different aspects
  5. AI logic: selection of the best next move for an AI player

Among these different parts, we already implemented the game logic. The goal of today is to implement the next one, the rendering.

 

Reminder on the target architecture


Our different responsibilities are interconnected through the following high level architecture (you can refer to our first post for more details):

   Store ----------------> Rendering ---------------> Reagent
     ^      (reaction)         ^      (send hiccup)
     |                         |
     |                         | (listen)
     |       (update)          |
     \-------------------- Game loop
                               |
                               |
     ,-------------------------| (use to update store)
     |                         |
     v                         v
 Game logic <-------------- AI logic

The rendering consumes the application state referred as the store and will be refreshed whenever these data change. Identifying the inputs needed for the rendering will later help us design our application state.

 

Overview


Before diving into the implementation, we have to discuss which data we want to display in the user interface. We also have to identify and list all the interactions we want to offer to our user.

 

Inputs of the Rendering

A game is a succession of turns. Each turn represents a valid state of the game. It contains the status of the board, the scores and the next player to play.

The turn is the main piece of data we want to display on the screen. The scores will end up on the top menu of the game, while the board model directly fuels the display of the board view showing the ownership status of each cell.

A transition is an edge between a turn and a turn accessible through one move of the player or an AI bot. Each transition is associated a coordinate. Playing at this coordinate triggers the transition.

Our Tribolo game can offer move suggestions to the human player. The transitions associated to a turn are the natural input data to use to identify and display these suggestions.

 

User interactions

The user plays a move by clicking on a cell of the board. Doing so translates into a event to play at the coordinate associated to the cell.

Playing a move is by far the most important interaction the user has with the game. But the user has additional actions available as well through the top menu:

  • Restarting a new game
  • Toggle the suggestions feature
  • Restarting the same game
  • Going back to our last move

The game loop will be responsible to update the application state by listening to the events associated to each of these actions. To keep our rendering decoupled from the game loop, we define a protocol to hold these actions:

 

Main frame


The goal of the main frame is to assemble the different parts of our user interface: the top menu and the board. The implementation follows from this description and simply delegates the data to the appropriate sub-components:

 
To check that the data sent to the user interface is well-formed, we can define a spec for our main-frame rendering function.

Since main-frame is the sole entry point of the rendering, this spec will also implicitly check the rendering of all sub-components as well. This is the only spec our entire user interface will need. It will allow to catch errors very early in the rendering process.

Note: You can find some other example of usage of spec in the post where we implemented the game logic. We also discussed spec in our previous article.

 

Top Menu


Our top menu will be responsible for the rendering of the scores. It will also render a bunch of buttons to trigger the following actions:

  • Restarting a new game
  • Toggle the suggestions feature
  • Restarting the same game
  • Going back to our last move

The code pretty much follows our description. The main goal of show-top-menu is to delegate the appropriate call-back, plug the appropriate call-backs for each button, and wrap the whole inside div that provides the appropriate styling:

This code makes uses the following helper function to render a button:

 
The rendering of the score is delegated to the show-scores function. We delegate to the player->css-style function the selection of the style of each score. The goal is to highlight the score of the current player.

 

Board


Rendering the board of the game consists in creating a SVG panel and filling it with the SVG representation of each cells.

There are five possible owners for cells in Tribolo: the three players, the walls, and the fifth possibility is that the cell is empty. If the cell is empty, the player can play at the coordinate of this cell, and otherwise cannot. As a consequence, the suggestions only concern the empty cells as well.

This main distinction is visible through the implementation of the render-board function. It delegates the rendering of the empty cells to empty-cell, while the other cells are rendered using rect-cell:

 
The rendering of the cells is pretty simple. The empty-cell is only a rect-cell to which we add the appropriate callback to play at the coordinate associated to the cell.

The rect-cell renders a rectangle in SVG and chooses the appropriate styling. The next section will demonstrate why we choose to base the rendering on CSS style rather than directly picking a color.

 

Animations


If you tried playing the game, you will have noticed that there is a small transition time for the ownership of a cell to change. This small animation allows to highlight the moves of the player and the artificial intelligence bots.

Because these animations stay pretty simple, they are directly implemented using CSS styles. We only have to add the transition-duration attribute on our cells style.

 
We want to avoid the transition effect to occur when creating a new game. To do so, we remove the animation for the empty cells. And to better highlight the suggestion feature, we will tweak the transitions of the help cells as well. It results in the following CSS styles:

 

Conclusion and what’s next


This is it, the whole rendering of the game. Thanks to the use of Reagent, the code is quite short and was really easy to write as well.

Now that we know what the user interface needs as input, we can design our application state accordingly and start thinking about the game loop as well. Both these topics will be the focus of the next post.

Building a ClojureScript Game: Thoughts on Spec

This post is the fourth in the series of posts focused on the design and implementation of a port in ClojureScript of the game named Tribolo.

In our first post, we discussed the game, described its rules and came up with a basic target architecture. In our second post, we tested this architecture on a Proof-Of-Concept. In our last post, we implemented the full game logic.

This post is an interlude on Clojure Spec before going through the remaining of the implementation of the game (Artificial Intelligence, Game Loop, Rendering).

This post is a discussion and not a tutorial. In particular, it aims at discussing how to use Spec correctly but does not aim at providing answer more than questions. It will share my short experience using it, my struggles using it and voice some questions I felt I had no answer to.

As such, any comments or answers to these questions would be greatly appreciated. You can shared in the reddit post linked at the end of the post.

 

Preambule


As Clojure Spec user, you might have heard repeatedly (or read in the rationale and overview) that Spec is not a type system.

But for those like me coming from strongly typed languages, the temptation is there to make it work like a type system anyway. Spec will resist this attempt: trying to do so might prove very frustrating.

One of the best resource available to understand the philosophy behind Clojure Spec is the Spec-ulation keynote from Rich Hickey. In the first 5 minutes of the presentation, Rich Hickey provides us with the motivations of Spec:

  • Providing someone something they can use (not just rules to follow)
  • Making a commitment to deliver some services against some requirements
  • Ultimately helping managing changes in software evoluation

This post will explore these different concerns in the context of the development of a small Clojurescript game like Tribolo.

The first section will talk about the concept of commitment and share some thoughts on my understanding of it. The second section will discuss some problems I encountered trying to use Spec to express commitments. The last section will talk about some tips that helped me understand Spec.

 

Expressing commitment


The first aspect of Clojure Spec has a lot to do with the Design by Contract approach. It stresses commitment to provide some service:

  • We commit to provide outputs and effects (and commit never to provide less)
  • We commit to require some given inputs (and commit never asking for more)

This ressembles Design by Contract in which the pre-conditions (requirements on the inputs) are the terms under which a function promises to deliver its post-conditions (commitment to deliver specific outputs and effects).

 

Avoid over-committing

The notion of commitment has some interesting consequences. Since a commitment is for ever, we have to think about what might change and will not.

Expressing no commitment whatsoever is obviously not very useful for the user. At the other end of the spectrum, excessive commitments can remove interesting degrees of freedom. The resulting rigidity might be a real problem to accommodate for future changes in an API.

There is a trade-off there, a right balance to find. In particular, and for those coming from strongly typed languages, we have to resist the temptation of systematically Spec-ing every keys in a data structure, or automatically enriching the Spec of a map every time we add a new key in it.

 

It is about the client

Since specifications are about commitment to provide a service, Specs are fundamentally oriented toward the client, the user of our code (which might be us).

Again, the temptation is really great to use specs as types and add all of our keys in a keyset specification. Since specs support instrumentation, spec’ing every single detail of our data structures might even help us finding bugs as implementer.

But we have to be careful and think about the promises we make through the specs exposed at the boundaries of our APIs. Coming from a strongly type language past, I felt this is a pretty hard to do.

 

Example of commitment

In the context of Tribolo, we made sure the specification of a turn does not mention the presence of the key :transitions. It was tempting to enrich the map with this key, in particular to get better instrumentation during development.

But considering that this key is an artefact of the implementation, its absence in the specification of ::turn acts as an indication for our user that it cannot be relied on forever.

 
Note: Clojure Spec can ensure that namespace qualified keys do comply to their associated spec even when not listed in a map specification. This clear distinction of membership from compliance allows to have quite good instrumentation of the implementation details but not commit to the presence of the key.

 

Lost in Spec-ulation


Up until now, it makes perfect sense. We can use specs as a way to express an exchange of services with the user of our code. The pre-conditions are the requirements for the post-conditions to be fulfilled.

For example, new-init-turn instantiate the first turn of a Tribolo game and promises to deliver a ::turn. Since we do not want to commit on the presence of the :transitions key in the turn, we omit it from the ::turn spec:

Simple enough. Maybe too simple.

 

Annoying Symmetries

Problems arise when dealing with functions such as next-turn that feature the same spec as input argument and as output. Indeed, and based on the notion of pre-condition and post-condition:

  • Input specs should contain at least what the function require (complying arguments should lead to the service being delivered)
  • Output specs should contain at most what the function outputs (the function can deliver more than the promise service)

In our example, and because we choose to keep :transitions as an implementation detail, satisfying the ::turn spec is not enough for our user to successfully use next-turn. The transitions are a mandatory (but not committed forever) part of the current implementation.

The pre-conditions expressed in the following spec are therefore incomplete, and a client satisfying them might be quite surprised not to get his service in exchange:

 
Improving on the pre-conditions to include the :transitions in the keyset of the ::turn would directly impact the level of commitment on the output, which is something we do not desire. This seems like an impossible deal.

One solution could be to break the symmetry, and require a different spec as input and output. Somehow, it does not seem like the most elegant solution to the problem. We will later propose another solution.

 

Limited Precision

Let us say that we finally commit to the presence of the :transitions key inside the ::turn spec. We avoid the problem raised in the previous section, but there are still some remaining issues.

The ::turn spec is still unable to capture all the pre-conditions needed for next-turn to provide the service it promises. The reason is that there are hidden dependencies between the data inside the turn:

  • The scores are proxy for the number of cells in the board
  • The transitions are based on the state of the board and the player

Generating a sample ::turn, satisfying all the pre-conditions expressed in the keyset, is not enough for the next-turn function do its job properly. Can we refine the pre-conditions with enough precision to circumvent this? And if it was possible, should we even try to?

 

Increasing Precision

Instead of trying to refine our previous spec, it would be much easier to define a valid turn as being the result of a succession of transitions starting from an initial turn. It would avoid duplicating the game logic inside the turn spec.

A valid turn is obtained by chaining next-turn calls on a turn initially created by new-init-turn. In terms of test.check, we could generate a turn from a previous valid turn using the following generator:

We could then chain calls to this generator, using for example the bind combinator from test.check, to randomly generate valid turns that a user could use and rely on.

 

Increasing Precision with specs

We can find ways to express that a valid turn is obtained from a succession of transitions on an initial turn. In particular, type systems are great at ensuring this kind of invariants, by forcing upon the user a desired workflow.

We could for example combine the use of a protocol with Clojure Spec:

  • We create a ITurn protocol with the appropriate functions
  • The ::turn spec checks if the object implements ITurn
  • The ::turn-info spec describes the turn data (board, player and scores)
  • We add a function turn->info to retrieve the ::turn-info from a ::turn
  • We make new-init-turn the only way to create a turn

The following code illustrate how this approach could look like in terms of specs. Since protocols do not support specs directly, we use wrapper functions around the protocol:

 

Precision gain – Observability Loss

Using the protocol design for our turn, we get to keep the symmetry of the next-turn function by introducing a level of indirection. We however lost the property that our turns are just data, degrading the observability of our system.

We might also wonder if this usage of spec is desirable. In particular, there is a section named Informational vs implementational in the Clojure Spec overview that looks to me as a warning which may really well apply here.

 

Thinking Differently


One thing that is clear from using Clojure Spec is that it requires its own way of thinking. It is amazing how some concepts that are hard to express with Spec can be slightly reworked in order to fit with Spec much better.

One clear distinction that I found is pretty useful when thinking about map containing keywords, is deciding whether the keywords have an associated intrinsic meaning to them, or only serve to be associated some data.

In case they do have an intrinsic meaning, it seems best to define a spec for the keyword and use keysets. In case they do not have a meaning, we can use map-of instead. Mixing both approaches almost always lead to mayhem, so there is a choice to make. Let us be more explicit by taking some examples.

 

Binary Tree

Among the many possible representations for binary trees in Clojure, we will choose the following one:

  • A node is a vector containing a value followed by a map of children
  • Inside the map of children:
    • The :left key will be associated the left tree
    • The :right key will be associated the right tree

This would be an example of valid binary tree in this representation.

Note: This example is a bit contrived. We could have used another representation where a vector would hold as first element the value, the indices 1 and 2 matching the left and right sub-trees respectively.

 
The wrong way to provide a spec for this binary tree is to think in terms of a type system. In such a type system, we would define two members, left and right, and give them the type BinaryTree. Here is a spec (that does not work) which illustrates this:

 
Neither :left nor :right have any intrinsic meaning by themselves. Their goal is only to designate which tree corresponds to left or right sub-tree. We could have used positions in a vector to identify the left and right sub-trees instead. So we can try to use map-of instead:

 

Scores

A less contrived example is the representation of the scores inside the Tribolo. We use a map associating an integer value to the player keywords. The role of the keywords are to associate values, and have no intrinsic meaning, hence the use of map-of:

 
Another possibility would have been to introduce specs for each of the player. We could then have defined ::scores as a keyset with all the player keywords in it.

 
One drawback of the first implementation with map-of is that we will not ensure that each of the keys is available in the map. The advantage it has is that it makes use of keywords without having to provide them a meaning individually.

 

Conclusion and what’s next


We are done with this small interlude on Spec.

Reading and experimenting with Clojure Spec made it pretty clear that its usage are quite different from those of a type systems. There is clearly a great deal of experimentation to do to combine efficiently both techniques.

I hope some of these thoughts could serve the reader. I would be especially interested in having some comments from anyone having done experiences with Specs in the following Reddit post.

In particular, handling the symmetry of some methods (like the case of next-turn which I described above) is really much a hard problem to me.

The next posts will resume the implementation of the Tribolo game where we left it, starting with the rendering.