CellSpace game engine 0.2

CellSpace is a game engine based on cellular automata (aka cellular spaces). It was inspired by Boulderdash and similar games, which have entities that are little more than tiles that animate by reacting to their immediate environment.

In the current version, a CellSpace game is specified in a simplistic text language called CellScript. Because the system is very suitable for visual, drag and drop style programming, I also plan to make a GUI for creating games.

How it works

The basic idea of CellSpace is that a level consists of a tile map, with a set of rules that describe what happens when certain patterns of tiles occur. Everything is a tile, including the player and any other animated objects.

For example, if there is a boulder with empty space below it, we want it to fall down. In CellSpace, you specify it like this:
rule: boulderfall . . .    . . .
                  . * .    . - .
                  . - .    . * .
A rule basically says:

If you see this => turn it into this.

Notice the 3x3 grid, which is the standard grid size for a CellSpace rule. Usually the object you want to manipulate is in the middle. The black squares indicate "ignore this cell", and the grey squares indicate empty space (which is just another type of tile in our game, like the boulder).

Right of the graphical representation of the rule, you see the corresponding CellScript statement. The "." indicates ignore, and "-" and "*" resp. indicate empty space and boulder. Linking characters to tile graphics is done using cell: statements.

Whenever the engine sees the 3x3 pattern on the left somewhere on the tile map, it applies (triggers) the rule. This results in the cells being overwritten by the cells on the right hand side. The empty space and boulder are swapped, resulting in the boulder falling down one tile position. Until a boulder hits something other than empty space, it will keep falling down one step per update.

Every time the game updates the screen, all rules are checked against all tiles. The result is written to a new tile grid, which becomes visible only after all tiles were checked. This ensures that rules do not interfere with each other. Otherwise, one rule's input could react on the output of another within a single screen update. Also, when two rules try to write to the same tile, the second rule is blocked, ensuring that no tiles ever get "lost" by overlapping outputs.

In Boulderdash, a boulder will also roll sideways when it is on top of another boulder. This behaviour is also easy to specify:

rule: boulderbounce . . .    . . .
                    . * -    . - *
                    . * -    . . .

Note that the boulder will only roll to the right. We should also specify a rule that makes it roll to the left. Instead of specifying a second rule, in CellSpace we can just say that this rule should be mirrored to create a second rule. In CellScript code, you specify this as: transform: mirx. In case a boulder can roll both to the left and right, one rule is chosen randomly. If you want one rule to have priority over another, you can use the priority: statement.

Now, let's introduce a player that can move around by pressing the cursor keys.
rule: playermove . . .    . . .
                 . @ -    . - @
                 . . .    . . .

This rule will move any player tile on the map to the right. However, we want this only to happen if the right key is pressed. We can do this by specifying an additional condition using the condfunc: statement. A condition is just a Javascript expression that must be true in order for the rule to trigger. There is a built-in function playerdir that we can use. If we specify condfunc: playerdir("right"), then the rule will trigger only if the "right" direction key is pressed (which is in the underlying engine translated to a cursor-right). Note that if we place multiple player tiles on the map, they will all move.

We can now add transform: rot4 which will rotate the rule 90, 180, and 270 degrees. The playerdir function takes the rotation angle as an implicit parameter, and will appropriately translate "right" respectively into "down", "left", and "up".

Let's spice things up with some monsters. Creating a monster that moves around randomly is very easy.
rule: monstermove . . .    . . .
                  . M -    . - M
                  . . .    . . .

If we specify transform: rot4 for this rule, the monster will move randomly in 4 directions. This behaviour does not look very intelligent, so let's say we want the monster to go straight on until it hits something, and then turn. This brings us to another CellSpace feature, namely directions. Each tile has a direction associated with it, which is represented by U, D, L, R for up/down/left/right, and for example UL for the up-left diagonal. Initially the direction is undefined, but we can set it using outdir: and check it using conddir: statements. If we specify conddir: L, the monster will only move left when its direction is left (that is, it is "facing left"). As yet, conddir: only checks the center tile, which is enough for most cases. outdir: takes 9 parameters, specifying the directions to write for each cell in the 3x3 grid when the rule is triggered.

Now, we still have to make the monster turn. We do this with the following cellscript:

rule: monsterturn
. . .   . . .
. M .   . . .
. . .   . . .
outdir:
- - -
- L -
- - -
transform: rot4
This will cause the monster to turn randomly. This rule competes with monstermove, which means one is randomly chosen. If we want monsterturn to trigger only if monstermove cannot be applied, we specify monstermove to have a higher priority using priority: 2 (the default priority is 1). We now have the desired monster behaviour.

A complete game

We will now look at a complete example game (with one level). Download it here: simpleboulder.txt.

A cellscript starts with a preamble which specifies some basic things:

globals:
var gems=0;

gametitle: Simple Boulderdash Clone

gamedesc:
This is an example game.<br>
Pick up all the gems and avoid the monsters.

tilemap: generictileset7.png

empty: .
cell: -   0 - false
cell: *  80 - false
cell: =  86 - false
cell: #  19 - false
cell: @   8 - false
cell: M 147 - true
cell: D 116 - false
The most interesting part is the globals: definition. Here you can define global variables, and functions if you need to. Note the syntax: it's Javascript.

gametitle: and gamedesc: are self-explanatory. Note that line breaks in gamedesc: are specified by <br> statements.

tilemap: specifies a sprite sheet with the tile graphics in it. generictileset7.png is a built-in tileset which contains some nice placeholder tiles you can use to prototype a game. All tiles must be 16x16, packed into the sprite sheet without any space around them. The 16x16 restriction will be removed in future versions of CellSpace.

Then follow the cell definitions. empty: specifies which character to use for the "ignore" cellsym. Each cell: statement is followed by a cellsym (a character denoting the cell), a tile number (indicating the tile to use, counted from the top left), a base rotation ("-" indicates none, "L", "R", "U" indicates left, right, and U-turn respectively. Finally a Boolean value specifies if the tile should be rotated when the cell has a direction associated with it, with Up being the base rotation.

We're finished with the preamble. Let's now specify the rules:

rule: boulderfall
. . .    . . .
. * .    . - .
. - .    . * .
priority: 2

rule: boulderbounce
. . .    . . .
. * -    . - *
. * -    . . .
transform: mirx


rule: playermove
. @ -    . - @
condfunc: playerdir("right")
transform: rot4

rule: playerdig
. @ =    . - @
condfunc: playerdir("right")
transform: rot4

rule: playerget
. @ D    . - @
condfunc: playerdir("right")
transform: rot4
outfunc: gems--

rule: playerpush
@ * -    - @ *
condfunc: playerdir("right")
transform: mirx


rule: monsterorient
. M .    . . .
outdir:
- R -
transform: rot4

rule: monstermove
. M -    . - M
conddir: R
outdir:
- - R
transform: rot4
priority: 2

rule: playerdie
. M @    . - M
priority: 3
transform: rot4
outfunc: lose()
This mostly follows the tutorial. Note that most rules only have a 1x3 cell pattern rather than a 3x3 pattern. This is a shorthand you can use if the top and bottom line are all "ignore" cellsyms.

Also notable is the outfunc: gems-- statement. If the player picks up a gem, the global variable gems is decremented. Similarly, outfunc: lose() calls the built-in lose() function, which causes the level to fail. There is also a win() function, which completes the level.

Finally, we specify the content of a level:

level: #

================================
=@==================*****=======
====*****===========*****=======
====*=D=*=====***===**D**=======
====*=D=*=====*D*===*****=======
====*****=====*D*===*****=======
==============***===========***=
============================*D*=
============================***=
#############################===
======*====*==*====*====*=======
========*==*====*==*=*====*==*==
===#############################
===**======M------------========
===**=------==D==-=====M-------=
===**=-=D==-=====-=====-===D==-=
===**=-====-=----M------======-=
===**=--------===D==-=D-======-=
===**=-==D===-======-==--------=
===**=-======--------=========-=
===**=M-------======----------M=
===**===========================

title: Level one
desc: 
This is level one.<br>
Actually, it's the only level.

init: gems=12
win: gems<=0
The first character after level: is the fill tile to use if tiles are not defined or for tiles outside of the level boundary. The rest of the level statement is self-evident, though note that the maximum level size for the current version is 32x22.

Level also has a title and description. init: specifies one or more Javascript statements for initialising variables, and win: specifies an expression representing a win condition. Also there are lose:, and tick: (not used here), which specifies one or more statements which should be executed at the beginning of each screen update.

There are a couple more useful features you should know about. Firstly, the cellsyms in the left hand side of a rule can include multiple characters, indicating any of these characters triggers the rule. The "!" character, when placed in front of the other characters, indicates "NOT" (all cells EXCEPT the specified cells). For example:

rule: playermovedig
. @ !DM#   . - @
indicates a player that can dig through anything except monsters, gems, and solid walls.

Secondly, there is a probability: statement, which indicates the probability that a rule triggers if all its conditions are met, and delay: which indicates how often the rule should be triggered (with 1 being the fastest, and 3 being the default), enabling objects to move at different speeds.

Finally, there is one more built-in function keypress(string), which can be used to detect a specific key. Use " " for a space, and "up", "down", "left", "right" for cursor keys. "shift", "alt", "ctrl", and "enter" are also recognised.

This concludes the tutorial. Also check out the bigger example game, Candy Cane Dash, which demonstrates most features of the CellSpace engine, and Flush the Fish, which is my latest CellSpace game, featuring water physics.

Download

Download CellSpace here!

Also check out Candy Cane Dash, Flush the Fish, and the generic tileset with index overlay.

It's written in Java. How to use:

java -jar CellSpace.jar MyCellScriptFile.txt

This is very much a prototype/alpha version. There are lots of things to do before version 1.0:

  • enable other screen and tile sizes
  • scrolling
  • smooth movement and animation
  • background images for title screen and levels
  • improve the CellScript parser. For example, add comments, more robust parsing, better error reporting.
  • make a web-based version where you can type the code on a web page
  • more built in functions: count tiles, audio
  • include cell state (a set of numeric variables associated with each cell)
  • clean up the source and release as open source