Information Topics


General Information

ZZT World Support

Reference

Miscellaneous

Objects and Behaviors in ZZT Ultra


ZZT allows a designer to customize game behavior to a large extent. But despite the ability to edit worlds and object code, there were always many details in the ZZT and Super ZZT engines over which the designer could never exercise control. There is much nuance to how certain syntax conventions are handled in ZZT-OOP code, as well as very small issues with timing and prefabricated object behaviors.

This page attempts to "read between the lines" and identify the subtleties about ZZT that make the behaviors what they are. ZZT Ultra attempts to reproduce and control as many subleties as possible. As they become known, this page will expand to cover these extra details. With greater knowledge comes a better, more robust copy of zzt_objs.txt.

Of course, if the behavior of everything in ZZT were totally limited to what the original editor could produce, the implementation requirements for ZZT Ultra would be relatively simple. But other editors were made and hidden features and exploits were discovered, which means ZZT Ultra needs to account for all the idiosyncrasies. So, hats off to folks like Kevin Vance for finding out critical details, but curse the same people for upping the number of known requirements that must be accounted for. If that makes any sense.




Object-Attribute Table


  N
U
M
B
E
R
C
H
A
R
C
O
L
O
R
N
O
S
T
A
T
A
L
W
A
Y
S
L
I
T
D
O
M
I
N
A
N
T
C
O
L
O
R
F
U
L
L
C
O
L
O
R
T
E
X
T
D
R
A
W
C
U
S
T
O
M
D
R
A
W
B
L
O
C
K
O
B
J
E
C
T
B
L
O
C
K
P
L
A
Y
E
R
P
U
S
H
A
B
L
E
S
Q
U
A
S
H
A
B
L
E
C
Y
C
L
E
S
T
E
P
X
S
T
E
P
Y
H
A
S
O
W
N
C
H
A
R
H
A
S
O
W
N
C
O
D
E
C
U
S
T
O
M
S
T
A
R
T
EMPTY0001 000010010       
FAKE27178141 000000010       
FLOOR47176141 000000000       
WATERN4830251 010000000       
WATERS4931251 010000000       
WATERW5017251 010000000       
WATERE5116251 010000000       
SOLID21219141 000001100       
NORMAL22178141 000001100       
BREAKABLE23177141 000001100       
INVISIBLE280141 000001000       
FOREST20176321 010001000       
WATER191761591 000001100       
LAVA19111781 000001100       
RICOCHET3242101 010001100       
BOARDEDGE1219141 000001100       
_TEXTBLUE73 311 010101100       
_TEXTGREEN74 471 010101100       
_TEXTCYAN75 631 010101100       
_TEXTRED76 791 010101100       
_TEXTPURPLE77 951 010101100       
_TEXTBROWN78 1111 010101100       
_TEXTWHITE79 151 010101100       
_BEAMHORIZ33205141 000001100       
_BEAMVERT43186141 000001100       
AMMO513231 010001010       
TORCH615761 110001000       
GEM74141 000001011       
KEY812151 000001010       
ENERGIZER1412751 010001000       
BOULDER24254141 000001110       
SLIDERNS2518141 000001120       
SLIDEREW2629141 000001120       
DOOR910151 000011100       
LINE31249141 000011100       
WEB63249141 000010000       
PASSAGE112401270 101011100 300000
CLOCKWISE16179140 000001100 300100
COUNTER1792140 000001100 200100
STAR1547120 010001000 100100
BULLET18248150 010001001 100000
STONE6465150 010001000 100100
SCROLL10232150 010001010 100011
OBJECT361140 000001100 100111
BOMB1311140 000001120 600100
DUPLICATOR12250150 010001100 300100
BLINKWALL29206140 000001100 100000
BEAR3415360 010001011 300000
RUFFIAN355130 010001011 100000
SLIME3742140 000001000 300000
SHARK3894150 010001100 300000
SPINNINGGUN3924140 000001100 200100
PUSHER4016140 000001100 400100
LION41234120 010001011 200000
TIGER42227110 010001011 200000
ROTON59148130 010001011 100000
SPIDER6215140 000001000 100000
DRAGONPUP6014840 010001011 100100
PAIRER6122910 010001011 200000
HEAD44233140 000001001 200000
SEGMENT4579140 000001001 200000
TRANSPORTER30179150 000001120 200100
PLAYER42310 111001010 100100

There are some special combinations of these attributes that should be taken into consideration:

  • BLOCKOBJECT + PUSHABLE: Typical push behavior results in an object that is reasonably "solid" but movable.
  • BLOCKOBJECT + PUSHABLE + SQUASHABLE: Often used for enemies. This type of object is pushable, but can be killed from push operations that lack movement clearance.
  • BLOCKOBJECT + (not PUSHABLE) + SQUASHABLE: Some enemies, such as HEAD and SEGMENT, can be squashed by a push operation, but cannot be pushed. They are always killed by a push operation, even if there is movement clearance. However, these types will rigidly block attempts to move items through a TRANSPORTER.
  • (not BLOCKOBJECT) + (PUSHABLE): Nonblocking types that serve as a valid push destination.
  • (not BLOCKOBJECT) + (not PUSHABLE): Some nonblocking types, such as FLOOR, can be moved to normally but cannot serve as a valid push destination. These types therefore "fence in" pushables.
  • CUSTOMDRAW + (not HASOWNCHAR): The $CUSTOMDRAW routine sets the initial CHAR member to the default for the type.
  • CUSTOMDRAW + HASOWNCHAR: The $CUSTOMDRAW routine sets the initial CHAR member to the CHAR member of the object itself.



"No-stat" Types


Most of the types in ZZT and Super ZZT that lack status element definitions are "items" and "terrains" per the original game editors. These tend to have little or no ZZT-OOP code within zzt_objs.txt.

EMPTY

The least remarkable type of all. However, it is remarkable in one very important respect: color still matters. Background is forced to be drawn black even if the color attribute has non-black background, and the foreground color is still relevant to placement and change events (e.g. RED EMPTY versus GREEN EMPTY).

ZZT Ultra forces the background color to be black when it loads a legacy world file. The foreground color (lower 4 bits) is left alone.

ZZT Ultra treats the EMPTY type as the main type code, which means its code acts as a general recipient for dispatched messages that do not have specific types as a target. A great deal of ZZT Ultra's business logic is therefore handled in the ZZT-OOP code for EMPTY. None of this code actually deals with the characteristics of the EMPTY square itself, because other types usually conduct simple tests for EMPTY instead of using dispatched messages.

FAKE

Very similar to EMPTY; this is a non-blocking square that shows a message when the PLAYER crosses it for the first time.

The type yields to pushables in such a way that it is overwritten. Once the square is uncovered following such an overwrite, it becomes EMPTY; the FAKE type is lost.

FLOOR

Very similar to EMPTY; this is a non-blocking square.

This type does not yield to pushables except in special cases.

WATERN, WATERS, WATERW, WATERE

Very similar to FLOOR; these are non-blocking squares.

These types do not yield to pushables except in special cases.

The PLAYER executes a water-current-following turn as part of its $WALKBEHAVIOR handler. This movement occurs in addition to any ordinary PLAYER movement that might have also happened during the same clock cycle. With the exception of sound played on movement, this automatic movement is nearly synonymous with ordinary PLAYER movement.

SOLID, NORMAL

These "impassible" blocking types constitute stock wall-building material in ZZT boards. Functionally, they are identical, but they are identified differently in ZZT-OOP and have different appearance.

BREAKABLE

This blocking type is similar to SOLID in functionality. Other types will often destroy BREAKALBE types. The most common contexts are bullet collision, star collision, and bomb explosion. Bears also remove BREAKABLE types on contact.

INVISIBLE

This blocking type is similar to SOLID in functionality. The only major difference is that the PLAYER changes a touched INVISIBLE type to NORMAL.

FOREST

This blocking type is similar to SOLID in functionality. The only major difference is that the PLAYER can pass through FOREST. A message is shown once when the PLAYER passes through FOREST for the first time.

When passing through FOREST, the PLAYER leaves behind EMPTY in ZZT and FLOOR in Super ZZT.

WATER, LAVA

This "mostly" blocking type is similar to SOLID in functionality. The PLAYER cannot move over it.

BULLET and STAR can pass through this type as if it were non-blocking. This is accomplished in ZZT Ultra via #FORCEGO.

When ZZT Ultra loads a world, it swaps in the appropriate type based on whether a ZZT or Super ZZT world is loaded: WATER for ZZT, LAVA for Super ZZT. This also determines which message is shown when the PLAYER contacts the type.

RICOCHET

This blocking type is similar to SOLID in functionality. Bullets behave in interesting ways when they strike a blocking type near a RICOCHET.

BOARDEDGE

This blocking type is similar to SOLID in functionality. Normally, the type is not designed to be seen, due to its presence as a "virtual perimeter" for the board.

When the PLAYER contacts a BOARDEDGE, it attempts to transition to a linked board if one is defined in the movement direction.

_TEXTBLUE, _TEXTGREEN, _TEXTCYAN, _TEXTRED, _TEXTPURPLE, _TEXTBROWN, _TEXTWHITE

These blocking types are similar to SOLID in functionality. They have the TEXTDRAW attribute, which flips the meaning of character and color attribute.

These types were not exposed in the original ZZT-OOP namespace, hence the underscore prefixes.

_BEAMHORIZ, _BEAMVERT

These blocking types are similar to SOLID in functionality. Generated and removed by BLINKWALL types, they are not inherently harmful, but rather just a momentary solid rendition of a beam after it has been traced from the turret.

These types were not exposed in the original ZZT-OOP namespace, hence the underscore prefixes.

AMMO

This is a blocking, pushable type. The PLAYER will collect this type upon contact, adding to the inventory.

TORCH

This is a blocking type. The PLAYER will collect this type upon contact, adding to the inventory.

GEM

This is a blocking, pushable, squashable type. The PLAYER will collect this type upon contact, adding to the inventory. A BULLET destroys a GEM if fired by the PLAYER; all other BULLETs simply stop without destroying the GEM.

While ZZT does not create status element objects for GEM, Super ZZT does. In ZZT Ultra, the status element information for GEM, if it exists, is discarded when a board is loaded initially. To deal with the issue of replacing the contents underneath the type upon destruction or collection, ZZT Ultra uses the same algorithm for GEM as it uses for AMMO and other collectibles near FLOORs.

KEY

This is a blocking, pushable type. The PLAYER will collect this type upon contact, adding to the inventory, if the inventory slot for the color is not already exhausted. If the inventory is exhausted, the PLAYER will not move, showing a message instead.

ENERGIZER

This is a blocking type. The PLAYER will collect this type upon contact, setting the property ENERGIZERCYCLES to 80 and the ENERGIZED global variable to 1. Collection of an ENERGIZER also sends #ALL:ENERGIZE.

BOULDER

This is a blocking, pushable type. There is little else to say about the type.

SLIDERNS

This is a blocking, pushable type. The $PUSHBEHAVIOR dispatch routine accepts only push directions heading north or south; east and west directions are rejected.

SLIDEREW

This is a blocking, pushable type. The $PUSHBEHAVIOR dispatch routine accepts only push directions heading east or west; north and south directions are rejected.

DOOR

This is a blocking type. The PLAYER will cross this type if a key of the same color is in inventory. When opening the door, the space is replaced by EMPTY or FLOOR depending on which type was under the player earlier.

The $CUSTOMDRAW dispatch routine ensures that the DOOR is drawn with a white foreground (15) and background matching the supposed color of the DOOR. It is necessary to define $CUSTOMDRAW because the foreground color must match for the purpose of #CHANGE, etc.

LINE

This is a blocking type. It resembles SOLID in functionality.

The $CUSTOMDRAW dispatch routine is complicated. If no surrounding linking types are detected, the type is drawn as an ordinary dot. If there are adjacent linking types, double-line drawing characters are drawn such that anywhere from one to four directions are effectively "networked."

Linking types include LINE and BOARDEDGE.

WEB

This is a non-blocking type. It resembles FLOOR in functionality.

The $CUSTOMDRAW dispatch routine is complicated. If no surrounding linking types are detected, the type is drawn as an ordinary dot. If there are adjacent linking types, single-line drawing characters are drawn such that anywhere from one to four directions are effectively "networked."

Linking types include WEB, BOARDEDGE, and either of these types if they happen to exist under a status element. Note that the Super ZZT behavior for linking WEB to PLAYER was somewhat inconsistent due to the fact that updates to linked types did not always occur promptly as a result of scrolling the screen. ZZT Ultra does not try to reproduce "partial" web-tearing.




"Stat" Types


The following types are represented by status elements.

PASSAGE

This is a blocking, full-color, always-lit type. It resembles SOLID in functionality. The PLAYER navigates to the board number identified by the P3 member upon contact.

During board transition, the PLAYER will not change from the last established position in the target board if no passage of the same color is located there. If there is a passage of the same color, the PLAYER starts on top of this passage.

If there are multiple passages of the same color in the target board, the last passage in the "top-down-then-right" scan order is picked as the destination for the player.

Super ZZT tweaks the passage destination if there is same-board passage navigation with multiple passages of the same color within the board. The passage picked will never be the source passage unless it is the only one of that color in the board. Thus Super ZZT allows "two-way" passages to proliferate within any given board.

The $CUSTOMDRAW dispatch routine ensures that a normal PASSAGE is drawn with a white foreground (15) and background matching the supposed color of the PASSAGE. It is necessary to define $CUSTOMDRAW because the foreground color must match for the purpose of #CHANGE, etc.

However, drawing will be slightly different if a non-standard foreground color is used (not 15). The original foreground color as set in the editor is displayed in such a circumstances, UNLESS an action occurs to modify the passage color later, in which case, the foreground color reverts to 15.

CLOCKWISE

This is a blocking type with its own defined character. It resembles SOLID in functionality. With each iteration, this conveyor type rotates the surrounding 8 types in a clockwise direction if possible.

Rotation only occurs for a surrounding type if two conditions are met:

  • Type must be pushable.
  • Destination location must be EMPTY.

The original ZZT behavior for conveyors was rather buggy. Pushables without status element representation could be moved easily, but those with status elements sometimes had their internal values corrupted. ZZT Ultra does not try to reproduce these bugs (e.g. spontaneously activated bombs).

COUNTER

This is a blocking type with its own defined character. It resembles SOLID in functionality. With each iteration, this conveyor type rotates the surrounding 8 types in a counterclockwise direction if possible.

Rotation only occurs for a surrounding type if two conditions are met:

  • Type must be pushable.
  • Destination location must be EMPTY.

It is notable that COUNTER has CYCLE=2 while CLOCKWISE has CYCLE=3. This is rarely important for board design purposes, but it is very important for the purpose of reproducing the original behavior.

The original ZZT behavior for conveyors was rather buggy. Pushables without status element representation could be moved easily, but those with status elements sometimes had their internal values corrupted. ZZT Ultra does not try to reproduce these bugs (e.g. spontaneously activated bombs).

STAR

This is a blocking projectile object with its own defined character. With each iteration, the STAR modifies its character and color. With every other iteration, the STAR attempts to move towards the player (#TRY SEEK). It makes as many as 50 move attempts before it dies.

The STAR will move in a pushing fashion as it tracks the player. There are some special collision behaviors:

  • PLAYER: Damage player and die.
  • BREAKABLE: Destroy breakable and die.
  • WATER/LAVA: #FORCEGO to destination.

The default timeout value (P2) for STAR is actually zero, which means it remains for a full 255 move attempts if created using #PUT or #BECOME. The #THROWSTAR command sets P2 to 50, which is a more reasonable timeout.

BULLET

This is a blocking projectile object. With each iteration, the BULLET travels on a straight trajectory until it hits something.

The BULLET has a wide variety of special collision behaviors:

  • Non-blocking type: Move normally.
  • PLAYER: Damage player and die.
  • BREAKABLE: Destroy breakable and die.
  • GEM: Die, destroying GEM if BULLET originated from PLAYER (P1=0).
  • WATER/LAVA: #FORCEGO to destination.
  • BULLET: The "BULLET hitting a BULLET" behavior is strange and inconsistent. If the BULLET originated from the PLAYER (P1=0), the BULLET absorbs the other BULLET and continues forward. Otherwise, the BULLET kills both the other BULLET and itself. The reasoning here probably has something to do with giving the user a slight advantage in a firefight, even though it is still possible for an enemy BULLET to cancel the PLAYER's BULLET if the iteration order of the objects happens to allow this.
  • RICOCHET: Invert direction and repeat tests in opposite direction.
  • Side-aligned RICOCHETs: Rotate direction opposite RICOCHET and repeat tests in rotated direction.
  • OBJECT: Send the OBJECT the SHOT message and die.
  • Killable monsters: If BULLET did not originate from the PLAYER (P1=1), the following tests are skipped and the BULLET dies.
  • HEAD: Give 1 point, kill creature, and die.
  • BEAR, LION, DRAGONPUP, PAIRER, SPIDER: Give 2 points, kill creature, and die.
  • RUFFIAN, ROTON, TIGER, SEGMENT: Give 3 points, kill creature, and die.
  • All other blocking types: Die.

The evaluation of RICOCHETs cannot cycle infinitely in the highly unusual circumstance of a bullet surrounded on every side by some combination of RICOCHETs and other blocking types. If RICOCHET evaluation would potentially cycle infinitely, the BULLET simply dies, instead.

For BULLETs fired by the player (P1=0), the world property CURPLAYERSHOTS is decremented when the BULLET dies. This is compared against the board property MAXPLAYERSHOTS when evaluating whether or not the PLAYER can shoot for boards with an upper shot limit.

STONE

This is a blocking, pushable object. The PLAYER will collect the STONE instead of pushing it, which adds to the "Z" inventory.

A STONE's code only displays flashing colors and uppercase letter characters.

SCROLL

This is a blocking, pushable object. The PLAYER will collect the SCROLL instead of pushing it.

A SCROLL has a minimal amount of default code, which is reserved for the display of flashing colors. The SCROLL is dispatched the $DISPSCROLL message by the PLAYER as a way to activate the custom portion of the code. After the dispatched message is over, the PLAYER destroys the scroll.

ZZT Ultra sets $SCROLLMSG global variable to 1 if there are enough lines within the resulting text to show a large scroll interface. The PLAYER will not step over the SCROLL in such a circumstance, although it will still kill the SCROLL afterwards. Conversely, if there were only enough lines to display a toast message, the PLAYER will destroy the SCROLL immediately, replacing the SCROLL's square.

OBJECT

This is a blocking object. The PLAYER sends it the TOUCH message upon contact.

An OBJECT has a minimal amount of default code. The initial behavior is to move immediately to :$REALSTART, the label signifying the start of the custom portion of the code. This is the same location where the #RESTART command will go to.

The $WALKBEHAVIOR dispatched message handler attempts to move in the FLOW direction. This will always work if a non-blocking type exists in that direction. For ZZT worlds, a blocking type in the FLOW direction will invoke #DONEDISPATCH and jump to the THUD label if it exists. For Super ZZT worlds, #TRY FLOW is used, which can execute push operations; the THUD label is only jumped to if no pushing is possible.

Obviously, an OBJECT is a fairly useless type without its custom code portion.

BOMB

This is a blocking, pushable object. The PLAYER activates a dormant BOMB upon contact. All subsequent contact from the PLAYER pushes the BOMB. Non-PLAYER movement pushes the BOMB without activating it.

A BOMB, once activated, ticks from 9 down to 2 at CYCLE=6. Once the BOMB reaches 1 (not zero for some reason), it explodes. A BOMB uses the P1 member to store the countdown status. A value of P1=0 signifies a dormant BOMB.

When the explosion occurs, the BOMB is no longer pushable.

An explosion is characterized by the deployment of the BOMB mask in a #FORMASK loop. Killable types are replaced with breakables of random coloration. After a cycle of waiting, the BOMB then removes all breakables at the same BOMB mask, replacing them with EMPTY, and it then dies.

If the PLAYER is at a BOMB mask square at the time of the explosion, the PLAYER takes damage.

If an OBJECT is at a BOMB mask square at the time of the explosion, the OBJECT is sent the BOMBED message.

The following types are killed by a bomb explosion.

  • EMPTY: No points.
  • BREAKABLE: No points.
  • GEM: No points.
  • BULLET: No points.
  • STAR: No points.
  • HEAD: Gives 1 point.
  • BEAR: Gives 1 points.
  • LION: Gives 1 points.
  • DRAGONPUP: Gives 2 points.
  • PAIRER: Gives 2 points.
  • SPIDER: Gives 2 points.
  • RUFFIAN: Gives 2 points.
  • TIGER: Gives 2 points.
  • ROTON: Gives 3 points.
  • SEGMENT: Gives 3 points.
DUPLICATOR

This is a blocking object. A DUPLICATOR imposes a cycle of (9 - P2 * 3) as it modifies its character to a larger "circle." After the last "circle" expansion, the DUPLICATOR captures a CLONE in the source direction, then attempts to #PUT the CLONE in the opposite direction.

Placement is successful if either the existing object(s) can be pushed forth to make room for the CLONE, or the destination square is nonblocking. If unable to place the CLONE at the destination, an alternate sound is played and nothing else happens.

There are a total of 6 idle turns between duplication attempts.

BULLET, STAR, and various enemies invoke their "collision" behaviors when the DUPLICATOR duplicates its clone and the PLAYER is standing directly in front of the DUPLICATOR, which both damages the PLAYER and kills the source.

When a PASSAGE is duplicated and any PLAYER instance (real or PLAYER clone) is standing directly in front of the DUPLICATOR, the "hacked passage navigation" behavior is invoked. This is a quirk exploited by some ZZT adventures, which has the effect of changing the board without the user's direct navigation action. ZZT Ultra acknowledges this type of hack by setting the $TELHACK global variable to an object pointer to the PASSAGE.

BLINKWALL

This is a blocking object. A BLINKWALL periodically generates a beam in the step direction, killing or harming certain types in its path. The pattern is repeated in a series of on-off events.

The first part of a BLINKWALL routine is the wait until the P1 period is finished. P1 is set to the "period" which acts as a cyclical phase before the first beam-generation event.

Next, the beam is generated. The beam kills (and keeps going) when it encounters the following types: GEM, BEAR, LION, TIGER, RUFFIAN, HEAD, SEGMENT, ROTON, DRAGONPUP, PAIRER, SLIME, and BULLET. The EMPTY type is simply overwritten by the beam.

All other types stop the beam. The PLAYER will stop the beam only if the strike would not bump the PLAYER out of the way.

If the PLAYER is in the path of the beam, the PLAYER takes damage and is bumped either clockwise or counterclockwise to the beam direction, based on which square is EMPTY. If both these perpendicular directions are blocked, the behavior will depend on the BLINKWALLBUMP property. If BLINKWALLBUMP=0, the PLAYER takes damage but is not bumped (the beam stops at the PLAYER). If BLINKWALLBUMP=1, the PLAYER's health is reduced to zero instead of normal damage being taken. Note that the RESTARTONZAP board property prevents the PLAYER from having health drained all the way, whether or not bumping to adjacent squares would be possible.

After waiting (P2 * 2 + 1) iterations, the BLINKWALL erases its own beam, replacing it with EMPTY. It then waits another (P2 * 2 + 1) iterations before returning to the beam-generation event.

BEAR

This standard enemy is blocking, pushable, and squashable. BEAR moves at cycle 3 and tracks towards the PLAYER even when the PLAYER is energized.

The "sensitivity" or P1 determines the maximum number of "minor vector" squares away from the PLAYER the BEAR must be before a tracking movement occurs. At P1=8, the BEAR can only track the player at a direct-aligned condition. At P1=0, the BEAR can track the player for a "minor vector" difference as far as 8 squares away.

When a BEAR tracks the PLAYER, it always tries to move horizontally before trying to move vertically, if sensitivity would place the BEAR in range of both dimensions.

If a BEAR contacts the PLAYER, the PLAYER takes damage and the BEAR dies. If a BEAR contacts a BREAKABLE, the BEAR dies, taking the BREAKABLE with it.

RUFFIAN

This standard enemy is blocking, pushable, and squashable. If a RUFFIAN moves into the PLAYER, the RUFFIAN dies and damages the PLAYER.

A RUFFIAN moves at cycle 1 in fits and starts. The RUFFIAN will move in the step direction for a loosely-regulated walking period, then switch to no movement for another loosely-regulated resting period. This movement/resting behavior switches based on a probability, controlled by P2, or resting time: the probability of a change is (9 - P2) / 17.

The intelligence (P1) of a RUFFIAN determines the probability that a change to movement would choose SEEK as opposed to a random direction: (1 + P1) / 9.

SLIME

This standard enemy is blocking, although the PLAYER destroys a SLIME upon contact, turning it into a BREAKABLE of the same color.

A SLIME waits (P2 * 3 + 3) turns before spreading to the four surrounding directions, with a random drop of turns by 1 imposed as a way of staggering fast spawn rates. Such a spread event places a new SLIME at all surrounding, non-blocking squares, which match the rate of the current SLIME. It should be noted that the first successfully found non-blocking square is actually moved towards, leaving behind a BREAKABLE of the same color. In this manner, the SLIME will actually move instead of spawning a new SLIME. For all subsequent non-blocking squares found, a new SLIME is placed.

If a SLIME has no non-blocking squares surrounding it, it becomes a BREAKABLE of the same color.

One quirk of the staggered iteration order for objects is that SLIMEs tend to multiply far more quickly than usual at the highest speed when expansion is open-ended (all directions, or moving around a corner) as opposed to steady-state (only one direction generally available in a narrow tunnel). The reason this occurs is that newly-placed SLIMEs may or may not be iterated near or on the same clock cycle as the original SLIME, but this only becomes observable as a "speed-up" when enough space exists to have a cascading effect from so many new SLIMEs.

SHARK

This standard enemy is blocking. Unlike most other enemies, there is no way to destroy a SHARK under most circumstances. A SHARK remains in WATER or LAVA for all of its movements, unless it is attacking the PLAYER. If the player is at the destination, the SHARK disappears, damaging the PLAYER.

A SHARK moves at cycle 3. The intelligence (P1) of a SHARK determines the probability that movement would choose SEEK as opposed to a random direction: (1 + P1) / 10.

SPINNINGGUN

This standard enemy is blocking. A SPINNINGGUN decides at cycle 2 to fire at the PLAYER based on its P1 and P2 members. P1 controls intelligence, which determines probability that a shot will be fired towards SEEK versus in a random direction. P2 contains firing information, which determines both the likelihood of a bullet being fired and the projectile type to be fired.

The probability that the SPINNINGGUN will pick a SEEK direction is (1 + P1) / 9. However, if a SEEK direction is picked, the gun will not actually fire unless the PLAYER is within a minor vector of 2 squares difference. If SEEK is not picked, the gun is free to fire in any direction.

The value of P2 is split into two different sections. The first section (the lower 7 bits) determines the probability of firing: (1 + (P2&127)) / 9. The second section (the highest bit) determines the projectile: BULLET=0; STAR=1.

PUSHER

This object is blocking, and exists only to push or squash pushables in the step direction. A PUSHER moves at cycle 4, and will push if it can, pausing indefinitely if it cannot.

LION

This standard enemy is blocking, pushable, and squashable. A LION moves at cycle 2. If the LION touches the PLAYER, it dies and the PLAYER takes damage.

The intelligence (P1) of a LION determines the probability that movement would choose SEEK as opposed to a random direction: (1 + P1) / 10.

TIGER

This standard enemy is blocking, pushable, and squashable. A TIGER moves at cycle 2. If the TIGER touches the PLAYER, it dies and the PLAYER takes damage.

The intelligence (P1) of a TIGER determines the probability that movement would choose SEEK as opposed to a random direction: (1 + P1) / 10.

Unlike LIONs, TIGERs might choose to shoot instead of move based on specific conditions. If a TIGER is within a minor vector of 2 squares difference, the TIGER will try to shoot a projectile towards the PLAYER. Note that this shoot direction is never wild or ENERGIZED-affected; it is always towards the PLAYER, even for a TIGER at lowest intelligence.

If a shot can be taken, P2 is used. The value of P2 is split into two different sections. The first section (the lower 7 bits) determines the probability of firing versus moving: (1 + (P2&127)) / 28. The second section (the highest bit) determines the projectile: BULLET=0; STAR=1.

ROTON

This standard enemy is blocking, pushable, and squashable. If a ROTON moves into the PLAYER, the ROTON dies and damages the PLAYER.

A ROTON moves at cycle 1 in an extremely agressive and jittery fashion. The intelligence (P1) of a ROTON determines the probability that movement would choose SEEK as opposed to a random direction: (1 + P1) / 10.

Additionally, the ROTON's direction, after being picked above, is subject to a RNDP rotation based on the switch rate (P2). The probability of this "rotated" movement is the following: 1 / (10 - P2).

SPIDER

This standard enemy is blocking. If a SPIDER moves into the PLAYER, the SPIDER dies and damages the PLAYER.

A SPIDER moves at cycle 1 in an extremely agressive and jittery fashion. The intelligence (P1) of a SPIDER determines the probability that movement would choose SEEK as opposed to a random direction: (1 + P1) / 10.

Unlike ROTONs, SPIDERs can only move along webs or into the PLAYER. A SPIDER will always try additional adjacent webs if it cannot move in a previously desired direction, so it never stops moving unless it is totally isolated (webless).

DRAGONPUP

This standard enemy is blocking, pushable, and squashable. A DRAGONPUP only hurts the PLAYER if the PLAYER collides with it; the enemy never moves on its own. Instead, it simply animates its character.

The editor parameters don't actually mean anything.

PAIRER

This standard enemy is blocking, pushable, and squashable. A PAIRER only hurts the PLAYER if the PLAYER collides with it; the enemy never moves on its own. PAIRERs appear to be an unused enemy template in Super ZZT; they were never used in any official world.

The editor parameters don't actually mean anything.

HEAD

This standard enemy is blocking and squashable. A centipede HEAD dies if it touches the PLAYER, damaging the PLAYER. During board initialization, a HEAD connects to adjacent SEGMENTs to form full centipedes. ZZT Ultra does not honor previously set linkages; it always re-links centipedes in an automated fashion when a board is loaded for the first time.

The intelligence (P1) of a HEAD determines the probability that movement would turn towards the PLAYER if aligned vertically or horizontally. This turn decision is independent of the turn decision made by deviance (P2). When aligned with the PLAYER, the probability of seeking PLAYER is (1 + P1) / 10. Intelligence is never evaluated when the PLAYER is not aligned with the HEAD.

Deviance determines the probability that the HEAD will spontaneously turn in a random direction if it can. The chances of such a turn are P2 / 25. Note that most of the time, a centipede cannot turn unless it is in an open area.

If a HEAD becomes trapped on all sides, it performs an inversion operation. This is characterized by each linked member flipping their LEADER and FOLLOWER members, the last linked SEGMENT becoming a HEAD, and the HEAD becoming a SEGMENT. The last transmuted SEGMENT selects the opposite of the last direction it had moved as its initial direction as a new HEAD.

When a HEAD moves, it drags all connected SEGMENTs with it, such that each SEGMENT replaces the position of the one in front of it.

When a HEAD hits the PLAYER when it has connected SEGMENTs, the SEGMENT after it becomes a new HEAD immediately.

SEGMENT

This standard enemy is blocking and squashable. A centipede SEGMENT does not actually move on its own; it is moved indirectly by a connected HEAD.

A SEGMENT performs a periodic "keep alive" check for its LEADER. If it finds that the LEADER member is no longer valid, it becomes a new HEAD, with an initial direction of the last direction it had moved. Additionally, the SEGMENT will attempt to link to other unattached SEGMENTs if its own FOLLOWER is not valid.

TRANSPORTER

This type is blocking and "pushable" in a special way. A TRANSPORTER's $PUSHBEHAVIOR routine performs a complex evaluation of pushability as a way of determining the next available push evaluation location. The only action directly executed by the TRANSPORTER is a character animation cycle.

There are two dispatched message handlers that TRANSPORTER supports: the $PUSHBEHAVIOR handler, which is well-known, and the FINDDEST handler, which is used only by the PLAYER when locating a specific warp destination. Both of these routines perform roughly the same battery of tests, but FINDDEST stops when it finds a destination as opposed to the complex, nested evaluation required by $PUSHBEHAVIOR.

When FINDDEST is dispatched, the global variables $X and $Y are set to the valid destination coordinates if navigation through the TRANSPORTER is possible. If it is not possible, these variables are instead set to -1, -1.

The $PUSHBEHAVIOR handler leaves $PUSH at zero if pushing through the TRANSPORTER is deemed impossible. If pushing is deemed possible, $PUSH is set to 3, with $PUSHDESTX and $PUSHDESTY set to the immediate destination coordinates for objects pushed through the TRANSPORTER. $PUSHDIR is not modified as part of the evaluation.

When testing for navigation, the first step is to identify if the push direction matches the TRANSPORTER direction. If the condition is not met, the routine returns immediately with failure.

The next step is a test for blocked condition in the square just beyond the TRANSPORTER. If not blocked, return with success.

The next step is a test for pushability in the square just beyond the TRANSPORTER. Note that it is safe to invoke the conditional SAFEPUSH1 from inside this handler because ZZT Ultra preserves and restores values of $PUSH, $PUSHDIR, $PUSHDESTX, and $PUSHDESTY. If the square is pushable, return with success.

The next step locates the nearest opposite-facing TRANSPORTER. If no such TRANSPORTER can be found, return with failure.

If an opposite-facing TRANSPORTER is found, go back to the "test for blocked condition" step and repeat the process as many times as necessary until either a valid destination is reached or no open opposite-facing TRANSPORTER can be found.

PLAYER

We have saved the best for last. The PLAYER is blocking and pushable, and it has extremely complicated code.

ZZT Ultra locates and remembers the active PLAYER object whenever switching to a new board. Thus, the $PLAYER global variable will always point to a valid PLAYER in the board.

A single iteration of the PLAYER's object has it checking for a wide variety of conditions, flags, modes, etc. This is because the PLAYER may or may not be free to perform many different types of tasks, movements, and damage-taking reactions based on whether a title screen is shown, whether the game is paused, whether the PLAYER is dead, and other conditions.

The PLAYER's behavior, as implemented in ZZT Ultra, is summarized in the following "pseudocode" lines.

  • If $MUSTRESTART set, the PLAYER is warped back to the entry square saved in board properties PLAYERENTERX and PLAYERENTERY, whereupon the game is paused. $MUSTRESTART is then cleared.
  • $PMOVESOUND is set to 1.
  • If $PLAYERMODE is set to 3 ("ZZT title-screen mode"), done with iteration.
  • If the property ENERGIZERCYCLES is zero, the ENERGIZED flag is cleared, and the PLAYER is vulnerable again.
  • If the property ENERGIZERCYCLES is above zero, the counter is decreased by one, and the PLAYER's character is animated.
  • If the property ENERGIZERCYCLES reaches 10, the "ending energizer" sound is played, interrupting the standard energizer music.
  • If $PLAYERMODE is set to 2 ("Dead mode"), go to the DEADLOOP.
  • If $PLAYERMODE is 3 or 4 (one of the "title screen" modes), done with iteration.
  • If $TORCHCYCLES is 1, darken the area around the player with the TORCH mask, play the torch-out sound, and hide the torch progress bar.
  • If $TORCHCYCLES is above zero, the counter is decreased by one, and the torch progress bar is updated.
  • If $INITDIR is set, the movement direction (.DIR1) is posted from $INITDIR, and the movement in this direction is attempted (go to DOMOVE).
  • Read the input using #PLAYERINPUT. If a shot was fired (.DIR2 posted), go to DOSHOOT. If a move was taken (.DIR1 posted), go to DOMOVE.
  • If the board is dark, display the "torch needed" status message if it had not been displayed yet.
  • Done with iteration.
  • $WALKBEHAVIOR dispatch handler: This handler does not actually use the preexisting step direction; it instead sets .DIR1 if a WATERE, WATERS, WATERW, or WATERN type exists under the PLAYER. If no such type exists under the PLAYER, the handler just returns. If there is such a type, #DISPATCHDONE is called, $PMOVESOUND is set to 0, and control jumps to DOMOVE.
  • DOMOVE label: This routine checks the move-to type to decide what to do.
    • GEM: Go to GETGEM.
    • AMMO: Go to GETAMMO.
    • TORCH: Go to GETTORCH.
    • ENERGIZER: Go to GETENERGIZER.
    • STONE: Go to GETSTONE.
    • KEY: Go to GETKEY.
    • FAKE: Go to MOVEFAKE.
    • Blocking square: Go to MOVEBLOCKED.
  • If none of the above types or conditions are satisfied, the PLAYER takes an ordinary move to the next square. The camera chases the PLAYER (for Super ZZT) and the TORCH mask is also moved to chase the PLAYER.
  • LEAVEPASSAGE label: This code handles the generic passage-leaving update to the old PLAYER square after movement is finished. If the PLAYER had just left a PASSAGE ($PASSAGEEMERGE is set to 1), the old square is replaced by a passage of the correct color and P3 destination. Nothing special happens in terms of replacement if $PASSAGEEMERGE is set to zero. $PASSAGEEMERGE is always set to zero as part of this code, and then the iteration is done.
  • MOVEBLOCKED label: This routine checks the move-to type, which is definitely blocking, to decide what to do.
    • BULLET, STAR: Go to MAYBEMOVEHURT.
    • BEAR, RUFFIAN, LION, TIGER, HEAD, SEGMENT, ROTON, DRAGONPUP, PAIRER, SPIDER: Go to MOVEHURT.
    • SLIME: Go to KILLSLIME.
    • INVISIBLE: Go to INVISBLOCKED.
    • LAVA: Go to LAVABLOCKED.
    • FOREST: Go to MOVEFOREST.
    • BOARDEDGE: Go to EDGENAV.
    • PASSAGE: Go to PASSAGENAV.
    • DOOR: Go to MOVEDOOR.
    • SCROLL: Go to MOVESCROLL.
    • OBJECT: Go to OBJECTTOUCH.
    • TRANSPORTER: Go to MOVETRANSPORTER.
    • BOMB: Go to MOVEBOMB.
  • If none of the above types or conditions are satisfied, test if the square is pushable. If nonblocking, move normally and end the iteration. If not pushable, play the push sound anyway for boulders and sliders and end the iteration. If so, proceed with the push attempt (go to DOPUSH label).
  • DOPUSH label: Play push sound, move PLAYER, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
  • COLLECTMOVE label: This is a general "collection-replacement" routine for establishing what to put under the square after the PLAYER leaves the square previously occupied by the item. The idea is that EMPTY is always left (with the same color as the picked-up item) if the current .UNDERID for the PLAYER is EMPTY. However, the current .UNDERID for the PLAYER, if not EMPTY, is instead placed with the current .UNDERCOLOR. This effectively "paints" the nearest floor tile for AMMO, GEM, etc. After the placement is done, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
  • GETGEM label: Display gem-collection message if never seen before, increase GEMS property by 1, add to HEALTH property by GEMHEALTH config property, play collection sound, and go to COLLECTMOVE.
  • GETAMMO label: Display ammo-collection message if never seen before, increase AMMO property by 1, play collection sound, and go to COLLECTMOVE.
  • GETTORCH label: Display torch-collection message if never seen before, increase TORCHES property by 1, play collection sound, and go to COLLECTMOVE.
  • GETENERGIZER label: Display energizer-collection message if never seen before, set ENERGIZERCYCLES to 80, set ENERGIZED flag, play energizer sound, and go to COLLECTMOVE.
  • GETSTONE label: Display stone-collection message, increase Z property by 1, play collection sound, and go to COLLECTMOVE.
  • GETKEY label: Establish foreground color of key, then test inventory to see if the $KEYNLIMIT config property will allow inventory to hold one more key of this color. The inventory property that stores the key is KEY$$COL, where $COL is set to the foreground color of the key (a number from 0 to 15). For example, KEY12 represents a RED KEY.
  • If the key inventory is full for that color (default config setting only allows a maximum of 1), no move will occur. A message is displayed, a sound indicating max reached is played, and the iteration ends.
  • If the key inventory would allow collection, inventory is updated, a message is displayed, a collection sound is played, allows a maximum of 1), no move will occur. A message is displayed, a sound indicating max reached is played, and control goes to COLLECTMOVE.
  • OBJECTTOUCH label: This is very straightforward. The OBJECT at the destination is sent the TOUCH message. The iteration ends.
  • KILLSLIME label: This is very straightforward. A "die" sound effect is played, and the SLIME at the destination is changed to a BREAKABLE. The iteration ends.
  • MAYBEMOVEHURT label: For BULLETs and STARs, the PLAYER will overrun and take damage only if the projectile is not on top of LAVA/WATER. If the projectile is on top of LAVA/WATER, no movement occurs (the iteration ends). Otherwise, go to MOVEHURT.
  • MOVEHURT label: This is a generalized routine for a potentially damage-inducing move into a harmful projectile or enemy.
  • If $PLAYERMODE is not 1, movement occurs to the destination, killing the object at the destination, without any damage to the PLAYER. Go to LEAVEPASSAGE.
  • If ENERGIZED, movement into enemies and projectiles kills them without damage being taken, and points are awarded based on the type. Move PLAYER, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
  • At this point, the PLAYER takes damage according to the PLAYERDAMAGE config property. If this would drain the HEALTH property to zero or lower, go to LASTMOVEDIE. Otherwise, play hurt sound, reset TIME board property, set $MUSTRESTART to 1 if RESTARTONZAP board property is 1, move PLAYER, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
  • At this point, the PLAYER takes damage according to the PLAYERDAMAGE config property. If this would drain the HEALTH property to zero or lower, go to LASTMOVEDIE. Otherwise, play hurt sound, reset TIME board property, set $MUSTRESTART to 1 if RESTARTONZAP board property is 1, move PLAYER, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
  • $RECVHURT dispatch handler: This message is dispatched when some remote action hurts the PLAYER, such as running out of time, BULLET hit, or BLINKWALL zap.
  • If $PLAYERMODE is not 1, return.
  • If $MUSTRESTART is 1, return.
  • If ENERGIZED, return.
  • At this point, the PLAYER takes damage according to the PLAYERDAMAGE config property. If this would drain the HEALTH property to zero or lower, go to LASTMOVEDIE. Otherwise, play hurt sound, reset TIME board property, set $MUSTRESTART to 1 if RESTARTONZAP board property is 1, and return.
  • LASTMOVEDIE label: Play "game over" sound, get out of dispatch mode, set GAMESPEED property to maximum (zero), set $PLAYERMODE to 2 (dead mode).
  • DEADLOOP label: Show "game over" message and end iteration.
  • INVISBLOCKED label: Show message indicating blockage, change the touched INVISIBLE to NORMAL, play sound, and end iteration.
  • LAVABLOCKED label: Show message indicating blockage, play sound, and end iteration.
  • MOVEFAKE label: Show "fake wall" message if never shown before, move PLAYER, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
  • MOVEFOREST label: Show "forest" message if never shown before, move PLAYER, play sound, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE. The type dropped under the FOREST, as well as the sound played, will vary depending on whether or not the game is ZZT or Super ZZT. ZZT drops EMPTY and plays a single sound, while Super ZZT drops FLOOR and plays one of eight tones cycled in a round-robin fashion.
  • MOVEDOOR label: Test if inventory for a matching key color exists. If there is a match, decrease the appropriate property by 1, display message, and go to COLLECTMOVE. If no match, show a different "locked" message, play a different sound, and end iteration.
  • EDGENAV label: Establish source and destination boards. Nothing happens (ending iteration) if there is no board linkage in the direction of navigation.
  • Update the source board's PLAYERENTERX and PLAYERENTERY properties; these form the new starting position for zap-reentry.
  • Remove the TORCH mask aura if it exists.
  • Change the board to the destination. Dispatch to the main type code the EVALMOVEXY message, which returns the following codes:
    • $RESULT=0: Can definitely move to destination square.
    • $RESULT=1: Might be able to move to destination square.
    • $RESULT=2: Definitely cannot move to destination square.
  • If unable to move to destination square, change board back to source, re-instate TORCH mask aura if present, and end iteration.
  • If able to move to destination square, reset TIME to zero, set PLAYER location in destination to destination square, update PLAYERENTERX and PLAYERENTERY board properties to destination square, impose TORCH mask aura if applicable, and invoke board-to-board transition effect. Update time-oriented GUI label and end iteration.
  • PASSAGENAV label: Establish source and destination boards. The passage P3 member contains the destination board number. Play passage-nav sound. Remember passage color.
  • Update the source board's PLAYERENTERX and PLAYERENTERY properties; these form the new starting position for zap-reentry.
  • Remove the TORCH mask aura if it exists.
  • Change the board to the destination. Scan through all status elements in the destination until a same-color passage is found. If one is found, set $PASSAGEEMERGE to 1, and start the PLAYER on that spot. If one is not found, do not move the PLAYER at the destination.
  • Reset TIME to zero, update PLAYERENTERX and PLAYERENTERY board properties to destination square, impose TORCH mask aura if applicable, and invoke board-to-board transition effect. Update time-oriented GUI label, pause the game, and end iteration.
  • MOVESCROLL label: Play scroll sound, then dispatch the $DISPSCROLL message to the SCROLL's code.
  • If the variable $SCROLLMSG is set to 1, end iteration. The immediately following action by the PLAYER will be a replacement of the destination square with EMPTY (but not a movement to this destination). This only happens after the user closes the scroll interface.
  • If the variable $SCROLLMSG is set to 0, immediately replace the destination with EMPTY, then move to it (the message from the SCROLL will be a simple toast message). Adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
  • MOVEBOMB label: Check if bomb is activated. If not, dispatch the ACTIVATE message to the bomb, then end iteration. If activated already, go to DOPUSH.
  • DOSHOOT label: This is the main handler for the player's shots.
  • If no shots can be fired on the board, show a message if never shown before, then end iteration.
  • If a shot fired would exceed the number of shots allowable for the board, end iteration.
  • If no AMMO is left, show a message if never shown before, then end iteration.
  • If not blocked in firing direction, create a new "player-owned" BULLET that flies in the firing direction. Deduct AMMO property by 1, play sound, and end iteration.
  • If blocked in firing direction, evaluate the type. This "point-blank behavior" needs to take into account a variety of different firing contexts, as "shot-hit" behavior needs to happen despite the fact that a BULLET is not often created at this range.
    • LAVA/WATER: Forcibly generate the BULLET anyway; go to DOSHOOT.
    • GEM, BREAKABLE: Go to ELIMNEXT.
    • OBJECT: Go to SENDSHOT.
    • BULLET: Go to ELIMBULLET.
    • HEAD, SEGMENT, BEAR, LION, TIGER, DRAGONPUP, PAIRER, SPIDER, ROTON, RUFFIAN: Go to ELIMENEMY.
    • All else: End iteration; no shot is fired.
  • SENDSHOT label: If the config property POINTBLANKFIRING is set to 1, the OBJECT is sent the SHOT message and AMMO is reduced by 1. Note the original ZZT didn't allow for this. End iteration.
  • ELIMNEXT label: Reduce AMMO by 1. Remove type, play breakable-hit sound, and end iteration.
  • ELIMBULLET label: Reduce AMMO by 1. Update overall BULLET count for board, remove type, play breakable-hit sound, and end iteration.
  • ELIMENEMY label: Reduce AMMO by 1. Dispatch remote-death message to enemy, remove type, play enemy-death sound, and end iteration.
  • MOVETRANSPORTER label: Dispatch FINDDEST to the TRANSPORTER. If the values of $X and $Y come back -1, no navigation is possible (end iteration). If navigation is possible, play transporter sound, update new PLAYER position, adjust camera and TORCH mask aura, and end iteration.



GUI Behavior


All the original GUI templates are stored within zzt_guis.txt, which ZZT Ultra loads automatically. The GUIs have the following names:

  • ZZTTITLE: The "ZZT" title screen right-hand GUI.
  • ZZTGAME: The "ZZT" main game right-hand GUI.
  • PROVING: The "Super ZZT" full-screen title GUI for Proving Grounds.
  • FOREST: The "Super ZZT" full-screen title GUI for Lost Forest.
  • MONSTER: The "Super ZZT" full-screen title GUI for Monster Zoo.
  • SZTINTRO: The "Super ZZT" intro screen GUI frame.
  • SZTGAME: The "Super ZZT" main game GUI frame.

The GUI labeling system and key input system are fundamentally attached to the active GUI itself, which supports all the key presses and momentary updates in the original user interfaces for ZZT for Super ZZT.

ZZT Ultra also uses seamless changes when a new GUI is displayed. Moving between 40-column and 80-column mode is instantaneous.

When a GUI is drawn, the viewport (the portion allocated to showing gridded data for the board) is not updated automatically. An update or transition effect will need to occur to make this shown, such as #UPDATEVIEWPORT, #DISSOLVEVIEWPORT, or #SCROLLTOVISUALS.

Most of the momentary GUI updates are implemented in the form of GUI labels, to which one of three things can be drawn: text, pens, and bars. See #SETGUILABEL, #DRAWPEN, and #DRAWBAR for more information.

ZZT Ultra does not support the original editor GUIs. The rationale for this decision is the basic difference in architecture with ZZT Ultra--the editor GUIs that are supported reflect new capabilities as opposed to old capabilities.




Timing Issues


The timing for the original ZZT games relied upon the slowest setting of the internal clock to time the gameplay iterations. This value was 65536, which, when figured into the internal clock's frequency-calculation formula, gives us 1193180 / 65536 = 18.2 Hz.

There were only four speeds implemented in practice in ZZT: 6.067 Hz (the "slow" throttled speed), 9.1 Hz (the "normal" throttled speed), 18.2 (the "fast" throttled speed), and an unthrottled "fastest" speed. The default setting for ZZT was 9.1 Hz for gameplay iterations (what #CYCLE 1 would yield). For Super ZZT, there was no speed control setting available in the GUIs, which made 9.1 Hz the only speed for that engine.

The "nine-speed pen selection" for ZZT is highly misleading. The reason only four speeds were implemented in practice? Likely, the delay function used in the original program did not entertain the idea that the timer had limited resolution. If a timer is only resolute to 18.2 Hz, interstitial speeds are not possible and end up being rounded as multiples of the resolution's interval (in this case, 1 / 18.2 = 0.05494505 seconds). If one needs an interval at, say, 0.025 seconds, the engine would not have been able to support it.

Something that has caused a lot of confusion in attempts to re-create ZZT on non-DOS platforms: the order in which the status elements are iterated. All objects are stored in a buffer, which increases in size when new objects are created (e.g. bullets fired), and decreases in size when objects are destroyed (e.g. enemies killed). Of primary importance is what happens to the profile of the buffer when an object is removed (dies): is everything "moved down" once to fill the gap, or is the last status element relocated to the gap?

The behavior of ZZT and Super ZZT appears to be such that status elements created earlier in the timeline have lower iteration order than those created later. This implies that the buffer used did not leave "empty holes" after object destruction, but in fact shrunk the buffer using end-move or tail-relocation.

In ZZT Ultra, a compromise solution is picked. Removed status elements are kept in the buffer until the entire iteration is over for all status elements on that turn, with only a few exceptions. But there is also another ID-based tracking system, which assigns a constantly-increasing unique identifier to all status elements as they are created. This ends up having the same effect when scanning through existing objects and creating new ones.

The "exceptions" only apply to various visual effects that must be handled immediately: scroll interfaces and screen transition effects. The reason for the exceptions is that some actions change the overall game mode away from the iteration of real-time objects, so iteration cannot continue for the remainder of the turn.

There is a property, LEGACYTICK, which queues the delays between objects somewhat differently than the counter internally kept by each object. If the property is 1, the original ZZT modulo of 420 is used to queue when an object iterates using a special formula:

(tick % CYCLE == indexInSEBuffer % CYCLE)

The tick increases from 1 to 420, and it ends up iterating objects in a very specific order that some ZZT worlds rely upon. Those objects with a CYCLE of zero are never iterated.




ZZT-OOP Script Processing


ZZT-OOP syntax and command behavior seem simple on the surface. In reality, though, the ZZT and Super ZZT behavior had many fine details that require great precision during interpretation. Various bugs and features of ZZT-OOP's original operation are reproduced in ZZT Ultra out of necessity, because so many ZZT and Super ZZT games have been made that rely upon these quirks.

$WALKBEHAVIOR iteration frequency

When an OBJECT is subject to movement on its #WALK iteration, the movement will only occur as frequently as the cycle. The $WALKBEHAVIOR message is dispatched after successful execution of the object's main iteration. Thusly, an OBJECT that walks east at cycle 3 would spend 2 cycles waiting for every 1 cycle moving east.

Turn limits based on object command count

The original ZZT-OOP had a mechanism for ending turns early even if movement commands were not present. ZZT Ultra honors this mechanism only for types with the HASOWNCODE attribute set. For all other types, no such mechanism applies (although the property DETECTSCRIPTDEADLOCK can still catch infinite looping behavior).

The "magic number" for ending turns early is 32. The following commands count towards this number. If 32 is reached, the turn ends. The OBJMAGICNUMBER property can modify this number.

  • WALK
  • ENDGAME
  • END
  • RESTART
  • LOCK
  • UNLOCK
  • BIND
  • SEND
  • PUT
  • SHOOT
  • THROWSTAR
  • CHANGE
  • CHAR
  • CYCLE
  • SET
  • CLEAR
  • GIVE
  • TAKE
  • ZAP
  • RESTORE
  • IF
  • PLAY

Some movement commands unequivocally end the turn immediately:

  • GO
  • FORCEGO
  • TRY (if move successful)
  • /
  • ?
Text display immediately preceding #DIE or #BECOME

ZZT Ultra does not conclusively remove status elements from the system until the turns are over for all objects for the frame. Of course, the presence of a large scroll interface displayed with potential links means that it is not appropriate to "kill" an object if its links would save it from dying.

If #DIE or #BECOME appears in code immediately after a large text interface must be shown, turns are ended just before the #DIE or #BECOME. If no links are followed, the #DIE or #BECOME command is handled later, during a future frame.

#BIND action subtlety

When a #BIND statement is encountered, ONAME member is deleted if it had existed, instruction pointer is moved to the starting point for the new code, and turns continue into the #RESTART position for the new code (there is no inherent end of turn).

Label optimization

ZZT Ultra optimizes some label navigation operations at run-time. This includes #SEND messages to self, #DISPATCH messages to the main type code, and #SWITCHTYPE and #SWITCHVALUE labels. Optimization will not occur for sent or dispatched messages to other non-specific types, and will not occur for any type with the HASOWNCODE attribute.

#SEND messages to object names

When sending messages to all objects matching an object name (or ALL, or OTHERS), the lock status is respected for the target, unless the sender and receiver objects are identical. Thus an object could call #SEND ALL:stuff and still jump to the stuff message, even if locked.

Validity of coordinate pairs when applied to board edge

The following contexts do not allow the board edge (X=0, X=SIZEX, Y=0, or Y=SIZEY) to be referenced:

  • PUSHATPOS: Always returns false for border
  • SPAWN: Reports invalid coordinates
  • SHOOT: No shot taken
  • THROWSTAR: No shot taken
  • OBJAT: Returns -1
  • LIGHTEN: Has no effect
  • DARKEN: Has no effect
  • CLONE: Does not change CLONE type
  • SETREGION: Does not change region

The following contexts do allow the board edge (X=0, X=SIZEX, Y=0, or Y=SIZEY) to be referenced:

  • SETPOS: It is generally a bad idea to place an object on the border, but it can be useful. Objects can move from the board edge to an adjacent on-board square as a way of quickly "unhiding" them.
  • TYPEAT: Returns BOARDEDGE
  • COLORAT: Returns color; generally not useful
  • LITAT: Returns lit status; generally not useful
  • ANYTO: Evaluates BOARDEDGE type
  • TYPEIS: Evaluates BOARDEDGE type
  • BLOCKEDAT: Evaluates BOARDEDGE type
Special inherent push behavior

When movement commands execute push operations, there are many "gray areas" where pushing needs to be reconciled against types, stat/non-stat presence, squashing, etc.

The $ALLPUSH global variable governs point-blank squashing behavior for the commands /, ?, #GO, and #TRY. Setting the global variable to 1 allows squashing for all squares, including the type at point-blank range, while setting this to 0 allows squashing for all squares except for the type at point-blank range. Note that this condition is only checked if squashing needs to happen; these commands only squash if they have no movement clearance for pushing without squashing.

No provision is made to use $ALLPUSH for the command #PUSHATPOS.

Status elements inherently violate the pushing rules when pushed over types such as FLOOR. Because a status element can exist "above" another type as it moves over it, the combination of (not BLOCKOBJECT) + (not PUSHABLE) is treated as if it were (not BLOCKOBJECT) + (PUSHABLE). Primarily, this is used to ensure that PLAYER and other objects can be pushed over FLOOR and WATERn.

ZZT Ultra's push test loop and push operation loop have a failsafe maximum of 1000 iterations. Typical board extents are far smaller than this length. The purpose of the failsafe is to prevent infinite cycling in case a $PUSHBEHAVIOR handler fails to advance properly.

The FLAGS member

It is rare that the FLAGS member of an object will need to be read, since flags are normally relevant only to the engine itself. If one needs to read the flags, they are interpreted as follows:

  • FL_IDLE=1: Set if object is in an idle state.
  • FL_LOCKED=2: Set if object is in a locked state.
  • FL_PENDINGDEAD=4: Set if an object is about to be destroyed, but cannot be destroyed immediately due to scroll interface text that immediately precedes a #DIE, etc.
  • FL_DEAD=8: Set if object was destroyed, but is still within the status element buffer in preparation for removal at the end of turns for the game tick iteration.
  • FL_GHOST=16: Set if object has "ghost" status, remaining in the board without being linked to the grid itself.
  • FL_NOSTAT=32: Set if object has "statless" status, a special state used to maintain compatibility with ZZT types that are supposed to have status elements, but have stat information removed in the world file. An object with FL_NOSTAT is not counted as part of the status element total for the board, and it is generally used in conjunction with FL_LOCKED and FL_IDLE.
  • FL_DISPATCH=64: Set if the object is currently in a dispatched message handler. Note that this flag is cleared if #DONEDISPATCH is called.
  • FL_UNDERLAYER=128: Set if the object has "under-layer" status. This is used in conjunction with FL_GHOST when an object is forced under another object via the UNDER or OVER pseudo-directions. When there is no other object at the square, FL_UNDERLAYER and FL_GHOST are automatically cleared and the object resumes normal execution.
#ZAP detailed label-finding behavior

The general behavior for #ZAP is to find the first matching label and change it to a comment.

In reality, the label-finding behavior was extremely buggy in the original ZZT engines, and only partially deterministic. Reproducing the original ZZT behavior for #ZAP of labels is unfortunately a very difficult task, because the bugs must be reproduced exactly. Failure to have the bugs present will make worlds unplayable.

When searching for labels from #ZAP, an exact string match (case insensitive) will always count. However, it is also possible for an inexact match to count, as well. If the tested label is longer than the checked text, it will still count as a match if:

  • testedLabel.substr(0, len(checkedText)) matches checkedText
  • The character located at testedLabel[len(checkedText)] is not alpha or underscore

Thus, #ZAP abc would match not just the label :abc, but also :abc1, :abc123, :abc9876, etc.

#RESTORE detailed comment-finding behavior

The general behavior for #RESTORE is to find all comments that match a similar pattern and change them to labels, with specific early-out conditions for the restoration loop.

In reality, the comment-finding behavior was extremely buggy in the original ZZT engines, and only partially deterministic. Reproducing the original ZZT behavior for #RESTORE of labels is unfortunately a very difficult task, because the bugs must be reproduced exactly. Failure to have the bugs present will make worlds unplayable.

When searching for comments from #RESTORE, the results will depend heavily on the characteristics of the loop iteration. For example, the first iteration has relatively simple logic, but subsequent iterations change the logic significantly. The reason for this was ZZT's failure to secure separate string copying buffers for tested source and destination strings, resulting in implicit overwrites that the author likely never intended.

For iteration #1 of the restoration loop, restoration works more or less as the inverse of #ZAP. A case-insensitive exact match results in restoration, while a "starts with text" match is governed by the following rules:

  • testedComment.substr(0, len(checkedText)) matches checkedText
  • The character located at testedComment[len(checkedText)] is not alpha or underscore

For iterations #2 and up, is the same as iteration #1 with one notable difference. If the ZZT-OOP line immediately following the comment line is alphanumeric text, the label is not restored, and the restoration loop early-outs.

Thus, the apparent lack of consistency is actually consistent, after all, but in ways that most people would never be able to guess on their own. For this reason, it is strongly advised that future ZZT-OOP programming not rely upon too-similar zapped labels or labels that contain numbers.

Return to Table of Contents



This page is Copyright © 2016 Christopher Allen.