A puzzlescript file is divided into the following sections.
See Rules 101 for an introduction to rules.
Rules are about pattern replacement. In the simplest case there's a left side, and a right side. The engine looks for occurrences of the left side, possibly in many different orientations, and when it finds one, it replaces the contents with what's on the right. (There can also be rule prefixes and commands, but we'll get to those.)
Let's look at the standard sokoban example more closely.
RULES
=====
[ > Player | Crate ] -> [ > Player | > Crate ]
The left-hand side specifies what the rule looks for. It's looking for two cells, side by side, one having a player, and one having a crate. There's an arrow next to the player, and it's pointing at the crate - that means that we are looking for a player that's trying to move towards the crate. Directional arrows are relative - if you want absolute directions, for gravity, for instance you can use absolute directions such as up, down, left, and right.
There is no arrow next to crate, this means that the pattern doesn't care whether or not its moving. If you wanted to specifically search for crates that were not moving, you would do it like this.
[ > Player | stationary Crate ] -> [ > Player | > Crate ]
If you want to search if two things overlap, just mention them side by side.
[ Player Spikes ] -> [ DeadPlayer Spikes ]
If one wanted to match any player movements, not just movements towards the crate, one could replace the arrow with the moving keyword.
[ MOVING Player | stationary Crate ] -> [ moving Player | moving Crate ]
Note that the compiler can infer that the crate should be made to move in the same direction to how the player is moving.
What if you want to search for somewhere there isn't a crate? You can use the no keyword.
[ < Player | no Crate ] -> [ < Player | Crate ] (leave a trail of crates behind you)
It's possible for a rule to search for several disconnected bits. For instance, you could have a bird that vanishes whenever the cat tries to jump up:
[ up Cat ] [ Bird ] -> [ up Cat ] [ ]
It's possible to restrict rules to only operating in particular directions - for instance, the following removes anything that steps on lava:
down [ Mortal | Lava ] -> [ Corpse | Lava ]
The direction down indicates what direction the pattern will be oriented.
You can also have ellipses in patterns, that allow for them to be variable length. Here's a lava gun that turns anyone in line with it into a Corpse:
late [ LavaGun | ... | Mortal ] -> [ LavaGun | ... | Corpse ]
If the compiler was dumb, it would decompile this into something like this, and never finish compiling.
(92) late [ LavaGun | Mortal ] -> [ LavaGun | Corpse ]
(92) late [ LavaGun | | Mortal ] -> [ LavaGun | | Corpse ]
(92) late [ LavaGun | | | Mortal ] -> [ LavaGun | | | Corpse ]
(92) late [ LavaGun | | | | Mortal ] -> [ LavaGun | | | | Corpse ]
(92) ... etc. But fortunately it's smarter than that.
A technical point that might occasionally be relevant: ellipsis-based rules search from smallest to biggest.
Also, it's possible to use two ellipses in a single pattern, such as stuff like this.
(92) [ > Player | ... | Crate | ... | Crate ] -> [ > Player | ... | > Crate | ... | Crate ]
(Please be a little bit careful with using multiple ellipses in a single rule - it might get a bit slow).
If we had the following rule
[ > Player | > Crate ] -> [ > Player | Crate ]
it would tell us to remove the movement from crate. However, the following one would not change anything:
[ > Player | Crate ] -> [ > Player | Crate ]
The rule is - the only things that are removed are things that are referred to, and that goes for both movements and objects.
If you want to specify that an object of a particular type be removed, without referring to it on the left-hand side, you can use the no key word:
[ > Player | ] -> [ > Player | no Wall ]
The standard defined behaviour per the PuzzleScript documentation is that "Each rule gets applied in turn as often as it can be". A rule with a `once` prefix simply executes once instead. Even if "something happens", the rule is executed just the once.
This turns out to useful in situations where after running the rule, the result is such that the rule would still apply. Without this feature, it's necessary to use movement flags or lay down temporary objects to achieve the same effect. There is a new game Bridges to show it in use. Here is how the code looks (from Bridges).
Edit// calculate a count for each cell, using the once prefix so the rule applies only once
[ cell ] -> [ cell c0 ]
up once [ cell count | singley ] -> [ cell count1 | singley ]
down once [ cell count | singley ] -> [ cell count1 | singley ]
left once [ cell count | singlex ] -> [ cell count1 | singlex ]
right once [ cell count | singlex ] -> [ cell count1 | singlex ]
up once [ cell count | doubley ] -> [ cell count2 | doubley ]
down once [ cell count | doubley ] -> [ cell count2 | doubley ]
left once [ cell count | doublex ] -> [ cell count2 | doublex ]
right once [ cell count | doublex ] -> [ cell count2 | doublex ]
There are also some other ways to do randomness. See Randomness for a detailed explanation.
Sometimes just replacing tiles with tiles is not enough, you want other things to happen. That's what commands are for. They go after the right-hand side.
As an example, you can install a checkpoint, so that when people hit reset they're taken back to this point rather than the start of the level:
late [ Player SavePoint ] -> checkpoint
In several games, it's important that if the player can't move, nothing else can happen either - so that you can't make time tick forward by repeatedly walking into walls. There's a prelude setting for this, require_player_movement, but you can also simulate it as follows.
[ Player ] -> [ Player Shadow ]
late [ Player Shadow ] -> cancel
late [ Shadow ] -> [ ]
At the start of the turn, you put a counter on the square the player's in, and if they're still together after the movement phase, then cancel the whole move.
You can combine several commands together, so you could do this if you wanted to play a sound effect as well.
late [ Player Shadow ] -> checkpoint sfx3
You can have as many as you like, but if any command has a text argument, it must the the last on the line because it will swallow the rest.
You can have combine commands with regular rules, if you want.
[ > Player | Person ] -> [ < Player | Person ] message Woah, buddy, back off!
[ > Player | Person ] -> [ < Player | Corpse ] again message Working on it...
Editrandom [ no Sheep ] -> [ Sheep ] again
Again is a moderately intelligent command - it only triggers if changes happen - so it won't loop infinitely if it's doing nothing.
Edit
// mainline for game
[ player ] -> gosub player move
[ player ] -> gosub show lit
[ player ] -> gosub check win
subroutine player move
// rules for moving player
subroutine show lit
// rules for updating the board
subroutine check win
[ player ] -> [ player won ]
[ cell no lit ][ player won ] -> [ cell ][ player ]
[ won ] -> sfx1
[ won ] -> status You win! Press R to play again, or Esc to select level.
Editlate [ Player One ] -> GOTO Section 1
late [ Player Hub ] -> GOTO Hub
[ > Player | Crate ] -> [ > Player | > Crate ]
late [ Player | Crate ] -> log The Player is next to a Crate
late [ Player | Wall ] -> log The Player is next to a Wall
There are a few things that go in the Rules section but are not really rules.
Just because you can, it doesn't mean that you should, but if you want to do something realtime have a look here.
If you had a rule that said "if there are two walls next to the player, destroy it", that would be tough. You can do it with tricks, like putting temporary counters down as markers, but it's not hooked up that way.
If you can think of an interesting use for diagonal directions, please do let me know.