Intro
Having completed a simple implementation of the game Breakout in Clojure, I decided my next Clojure project should be doing something with ClojureScript. Not only would it provide an opportunity to continue Clojure practice, but I’d be able to leverage existing knowledge I have about JavaScript (probably the language I am most familiar with), and I could possibly use it in the future for a web project. Note that I’ve only tested this with the most recent version of Chrome. Performance is not great right now, but can be much improved. Here’s a demo.
Clojure v ClojureScript
I found the process of building projects, sharing code between Clojure
and Clojurescript, and building the source to be very straightforward. I
used cljsbuild to
accomplish many of these needs. Especially useful was the repl
which
can be used to communicate with ClojureScript code running in the
browser. There were two sources of headaches I had with working with
ClojureScript, as compared to Clojure.
- I had to create compatability namespaces to wrap thigngs that are
available in
clojure.core
but notcljs.core
- I found a couple things (hex-string support) that were supported in clojure.core and not cljs.core, but were not documented.
- When cross-compiling Clojure code for use in ClojureScript, I did not see in the documentation how to make namespace functions exportable. Admittedly, I might find my answer easily if I were to ask on the mailing list. This means I haven’t gotten advanced compiling working yet, and the compiled script file is large at 1MB.
The Game
I decided to work on a clone of the game Tetris Attack, which has provided me with hundreds of hours of entertainment. The object of the game is to match 3 or more blocks. The complexity of the game comes in setting off chain reactions of matches.
Methodology
The game is represented as a simple HashMap of keys and properties, that looks something like the following:
{:grid {:blocks [{:type :#FF0000 :position [1 1]}
{:type :falling :block {...} :falling-to [3 3] :ticks 15}
{:type :swap :blocks [...] :ticks 10}
...]
:rows 10
:cols 6
}
:clock 10}
Blocks are determined by type. The
simplest blocks store their color as their type. Other types have
specific meanings. For example, a “falling” block is one which is
falling down a grid column (as if pulled by gravity). Some of these
blocks have a property called :ticks
, which determines the clock ticks
left for the action to complete. For example, a falling block with 10
ticks left, will take 10 clock ticks to finish falling, at which point
it may be resolved into a normal block. In this way, the entire
game-state is stored, and separate interface functions can work to alter
the game state. In terms of mutability, a single atom in the
entrypoint
namespace stores the entire game state and interface state.
The rendering code is entirely separate and may operate on arbitrary
game states.
Rendering
The rendering code is in the display
namespace in a
ClojureScript-specific file that performs drawing on an HTML canvas
object. This namespace, along with the entrypoint
namespace provide
the specific code necessary to drive the web interface. Although ideally
there’d be a namespace that contains only functions requiring
implementation to support different platforms, display and entrypoint
are not at that point. Still, implementing a version with an interface
using three.js or using Java
Swing should be fairly trivial.
Learning
Most of the projects I do are for learning rather than anything practical. Here are some of the main things I’ve learned about working on this project:
- Some new Clojure techniques, such as using
partial
andinto
, the threading operator, testing withclojure.test
, and some more about how namespacing works. - How to work with ClojureScript - This involves syntax, building, debugging, crossovers, interop, and testing.
- How to implement a game almost entirely using immutable structures.
Media
You can find updated links to media as well as the full source in the project README. Here are (current as of the post date) screenshots and screencasts. Here’s a demo of the project. Please note that the script file is fairly hefty at (1 MB) since I haven’t gotten advanced compiling working yet.