A PuzzleScript experiment

Controls:

• Move: arrow keys.

• Restart: R

• Undo: Z

• Quit: Q or Escape.

PuzzleScript is a game engine that gets incredible mileage from a simple two-dimensional search-and-replace operation. We build a crude clone that pushes its core concept further:

• Instead of a move phase, we use ordinary patterns to describe movement.

• This obivates the need for the late keyword, as we have full control over when movement occurs.

• We can do without the collisionlayer section because we bring our own collision rules.

• Instead of the dedicated wincondition sublanguage, we use ordinary patterns to describe them by making the win action set a flag indicating victory and an unwin action that clears this flag. (Perhaps win could take an integer argument describing the number of levels to skip, which would allow level branching.)

Controlling movements with ordinary rules allows extended rigid bodies and extended movements. For the latter, to jump exactly 4 tiles in the air, we can place a hidden object 4 tiles away from an obstacle. A rule starts the object moving when the player does, and another rule stops the player when the object does.

Converting PuzzleScript

Our simplistic parser is fussy. A comment is a line starting with an equals sign. Block comments are unsupported.

Whitespace is significant. Tokens in the rules section must be separated by at least one space.

There is no support for the prelude. Comment each of these lines. Also, messages are unimplemented; comment them out.

The order of the OBJECTS matters. The first object is always drawn; it acts as the default background. Later objects are drawn on top of earlier objects if both are present in the same square.

Sprites can be bigger or smaller than 5x5, but ought to be square.

Remove the SOUNDS section. The prototype lacks support for sounds.

Remove the COLLISIONLAYERS section. For each layer that matters to the game, define a new entry in the LEGEND section, replacing each comma with or.

Replace the WINCONDITIONS with rules in the RULES section.

Only 4 sections should remain: OBJECTS LEGEND RULES LEVELS.

Basic Example

The basic example contains the COLLISIONLAYERS:

Background
Target
Player, Wall, Crate

We add a LEGEND entry for the last of the layers. It’s the only one that affects gameplay, so we throw the rest away:

Thing = Player or Wall or Crate

We add a corresponding moving rule. A Thing moves if its destination contains no Thing:

[ > Thing | no Thing ] -> [ | Thing ]

We translate the win condition:

All Target on Crate

to the following rules:

win
[ Target no Crate ] unwin

In other words, we set the victory flag, but then unset it if we find a Target with no Crate on it.

To summarize, after the conversion, we have these rules:

[ > Player | Crate ] -> [ > Player | > Crate ]
[ > Thing | no Thing ] -> [ | Thing ]
win
[ Target no Crate ] unwin

Implementation

For a while, I’ve been seeking an excuse to use Gaussian integers to index a 2D grid. More recently, I’ve been seeking for tests for my Haskell compiler. PuzzleScript appeared on my radar at a perfect time!

My compiler worked well enough (amazingly, I only ran into one bug), though the generated code is slow.

The engine compiles in GHC with this wrapper:

{-# LANGUAGE CPP, BlockArguments, LambdaCase, TupleSections #-}
import Data.Char (chr, ord)
import Data.Foldable (asum)
import Control.Applicative (Alternative(..), liftA2)
import Data.Map ((!), Map, fromList, toAscList, insert)
import qualified Data.Map as M
mlookup = M.lookup

#define Ring Num
#include "zfc.hs"


[+] Show engine

The web frontend code mostly coordinates calls between JavaScript and wasm, though there is some code involving drawing sprites.

[+] Show web frontend

Ben Lynn blynn@cs.stanford.edu 💡