Cellular Automata Laboratory


Defining Rules in Turbo Pascal

To define a rule in Turbo Pascal®, you write a rulefunction called JCRule which, when called with arguments containing the state of a cell and its neighbors, returns the new state for the cell as an integer from 0 to 255 (the low bit #0 is the state of Plane 0, bit #1 is the state of Plane 1 and so on). You embed this rulefunction in a .PAS file of the form shown below to get a ruleprogram. When you compile and run the ruleprogram, a .JC ruletable is generated. You can compile and run the ruleprogram either from inside Pascal (by pressing Alt-R in Turbo) or from the command line (by entering TPC filename). When you run the ruleprogram, you will see the first prompt:

Rule file name:

Enter the name of the rule file (the extension of ".JC" is supplied automatically; you need not specify it). If there is a problem with your rulefunction you will get some error messages. Otherwise, after a brief delay you will see the second prompt:

Rule file filename.jc generated.
Press Enter to continue:

When you press Enter the program will end. If you ran your program with Alt-R from inside Turbo, you must then use Alt-X to get out of Turbo. Answer y when Turbo asks you if you want to save your .PAS file.

Recapitulating, you might use the Turbo editor to write a program called MyLife.PAS. Then you might leave the editor and compile MyLife.PAS to a file called MyLife.EXE, which you run; alternately you might compile and run MyLife.PAS from within Turbo. When your "MyLife" program runs, it creates a permanent lookup table for MyLife called MyLife.JC. If you created and saved a MyLife.EXE file, you might as well erase it, because all MyLife.EXE does is generate the MyLife.JC file.

And what is a .JC file good for? It is what our JC simulator uses in order to run cellular automata at a good rapid speed. The .JC file codes up a JCRule entry for each of the 64K possible combinations of OldState and EightNeighborhood that a cell might have. A .JC file will not however normally take up 64K bytes of memory because a compression technique is used. ¹

The format of Turbo Pascal units changes from release to release. Since there's no way we can know what release you're using, or what changes may occur in the future, we include the source code CaMake.PAS from which CaMake.TPU is built. Follow the instructions for that came with your version of Turbo Pascal to compile this into a CaMake.TPU file usable with your compiler.

To understand how to write a JCRule function, first we must consider the neighborhood of a cell, as seen by the function through its arguments. If the function is defined as:

FUNCTION JCRule(OldState,NW,N,NE,W,Self,
                         E,SW,S,SE:integer):integer;
BEGIN
               ...
END;

then the arguments represent the neighborhood as follows:

NWNNE
WSelfE
SWSSE

Each of these arguments will be 1 if the low-order bit of the corresponding cell in the neighborhood is on, and 0 if it is off. In addition, the rule function may examine the argument OldState, which contains the full state of the center cell (eight bit planes). Thus, OldState ranges from 0 to 255, with the presence of low bit (also supplied in variable Self) signifying the state of Plane 0. The function defining the rule must examine these input variables, calculate the resulting state for the cell (from 0 to 255), and return that value. The following sample code, including the required declarations and main program, defines the game of Life, proceeding directly from Poundstone's description.

PROGRAM MyLife;
USES JCMake;
{$F+} { Needed so that function can be treated as an object. }
FUNCTION JCRule(OldState,NW,N,NE,W,Self,
                         E,SW,S,SE:integer):integer;

{We sum up the number of firing neighbor cells.  If this
 EightSum is anything other than 2 or 3, the cell gets
 turned off.  If the EightSum is 2, the cell stays in its
 present state.  If the Eightsum is 3, the cell gets turned
 on.}

VAR
     EightSum:integer;
BEGIN
     EightSum := NW+N+NE+E+SE+S+SW+W;
     CASE EightSum OF
          0,1,4,5,6,7,8: JCRule:=0;
          2: JCRule:=Self;
          3: JCRule:=1;
     END
END;

BEGIN {Main program}
     GenRule(JCRule);
END.

If you have Turbo Pascal 5.0 handy, you should try creating and running the MyLife.JC rule right now. Where do you get a file to start work on? One way is simply to type the text of MyLife.PAS in, using the Turbo editor or a word processor. An easier way is to copy one of our .PAS ruleprograms to a file called MyLife.PAS and then make a few changes to "MyLife.PAS" until it looks like the program above.

As it turns out, the Life.PAS program provided with CelLab is similar but not quite the same as MyLife. Our Life is actually the rule "LifeMem," which colors the cells differentially depending on their state in the last generation. But our Life.PAS is quite similar to what you want for MyLife.PAS, so you should copy Life.PAS onto MyLife.Pas and use that as the starting point for your program.

So the steps for running MyLife are as follows: Use the DOS copy command to copy the existing Life.PAS file to a new MyLife.PAS file. Then use the Turbo editor to work on MyLife. Once you have MyLife in shape, compile and run it from within Turbo. If all goes well, MyLife creates MyLife.JC. You leave Turbo, enter JC, and run MyLife.

Starting from the DOS prompt, the keystrokes are:

     (Copy life.pas and enter turbo)
copy life.pas mylife.pas Enter
turbo mylife Enter

     (Edit the file and then press)
Alt-R Enter
     (Answer the first prompt)
mylife Enter
     (Answer the second prompt)
Enter
Alt-X

     (Answer the save? prompt)
y
     (Try the new rule)
ca Enter
l
mylife Enter
F1
Enter

Since the rule for the game of Life doesn't use the bit-planes #1 through #7 at all, the MyLife ruleprogram contains no reference to OldState. Rules which use the higher bit-planes may also be specified straightforwardly by Pascal rule definition functions. For example, here is the definition of Brian's Brain, a rule developed by Brian Silverman and described in [Margolus&Toffoli87], p. 47, as:

The rule involves cells having three states, 0 ("ready") 1 ("firing") and 2 ("refractory"). A ready cell will fire when exactly two of its eight neighbors are firing; after firing it will go into a refractory state, where it is insensitive to stimuli, and finally it will go back to the ready state.

This translates directly into a Pascal program as follows:

PROGRAM Brain;
USES JCMake;
{$F+}
FUNCTION JCRule(OldState, NW,N,NE,W,Self,
                          E,SW,S,SE:integer):integer;

{We use three states 0,1,and 2.  1 always goes to 2 and 2
 always goes to 0.  0 goes to 1 iff there are 2 firing
 neighbors.}

VAR
     EightSum,NewState:integer;
BEGIN
     {First get rid of any extraneous high state bits:
      "3" decimal is "00000011" binary}
     OldState:=OldState AND 3;
     EightSum:=NW+N+NE+E+SE+S+SW+W;
     IF OldState=0 THEN
          IF EightSum=2 THEN NewState:=1 ELSE NewState:=0;
     IF OldState=1 THEN JCRule:=2;
     IF OldState=2 THEN JCRule:=0;
     JCRule:=NewState;
END;

BEGIN {Main program}
     GenRule(JCRule)
END.

It is possible to define much more complicated rules by using the high bits for various bookkeeping memory purposes. Here is an example of a JC that simulates thermally driven random diffusion. The theory of why the program works is explained in the Theory chapter.

PROGRAM Sublime;
{This rule implements the Margolus rule for simulating a gas
of cells diffusing.  Particle number is conserved.  We set
up a permanent lattice of position values that looks like
this:
            0    1    0    1 ..
            2    3    2    3 ..
            0    1    0    1 ..
            2    3    2    3 ..
            :    :    :    :
   This lattice is alternately chunked into
   A blocks: 0    1   and B blocks: 3    2
             2    3                 1    0
and the blocks are Noisily rotated one notch CW or one notch
CCW (short for ClockWise and CounterClockWise)}

USES JCmake;

{$F+}{ Required for function argument to genrule. }

FUNCTION JCRule(Oldstate, NW, N, NE, W, Self, E,
                              SW, S, SE:integer):integer;
{We use the eight bits of state as follows:
   Bit #0 is used to show info to neighbors
   Bit #1 is the gas bit
   Bit #2 is fed by the system Noiseizer
   Bit #3 stores the 4-cell consensus on direction;
          0 is CCW, 1 is CW
   Bits #4 & #5 hold a position number between 0 and 3
   Bits #6 & #7 control the cycle}
VAR
   Cycle,NewCycle,Position,Direction,NewDirection,Noise,Gas,
      NewGas : integer;
BEGIN
     Cycle:=(OldState SHR 6) AND 3;
     Position:=(OldState SHR 4) AND 3;
     Direction:=(OldState SHR 3) AND 1;
     Noise:=(OldState SHR 2) AND 1;
     Gas:=(OldState SHR 1)AND 1;
     NewCycle:=(Cycle+1)MOD 4;
     IF (Cycle=0)OR(Cycle=2) THEN
        BEGIN
          IF Cycle=0 THEN
            {In A block mode set direction to NW's}
             CASE Position OF
                0: NewDirection:=Self;
                1: NewDirection:=W;
                2: NewDirection:=N;
                3: NewDirection:=NW;
             END;
        IF Cycle=2 THEN
           {In B block mode set direction to NW's}
           CASE Position OF
              0: NewDirection:=NW;
              1: NewDirection:=N;
              2: NewDirection:=W;
              3: NewDirection:=Self;
           END;
           JCRule:=(NewCycle SHL 6)OR(Position SHL 4)OR
                   (NewDirection SHL 3)OR(Gas SHL 1)OR Gas
        END
     ELSE
     BEGIN
        IF (Cycle=1) AND (Direction=0) THEN
           {CCW rotation of an A block}
           CASE Position OF
              0: NewGas:=E;
              1: NewGas:=S;
              2: NewGas:=N;
              3: NewGas:=W;
           END;
        IF (Cycle=1) AND (Direction=1) THEN
           {CW rotation of an A block}
           CASE Position OF
              0: NewGas:=S;
              1: NewGas:=W;
              2: NewGas:=E;
              3: NewGas:=N;
           END;
        IF (Cycle=3) AND (Direction=0) THEN
           {CCW rotation of a B block}
           CASE Position OF
              0: NewGas:=W;
              1: NewGas:=N;
              2: NewGas:=S;
              3: NewGas:=E;
           END;
        IF (Cycle=3) AND (Direction=1) THEN
           {CW rotation of an A block}
           CASE Position OF
              0: NewGas:=N;
              1: NewGas:=E;
              2: NewGas:=W;
              3: NewGas:=S;
           END;
        JCRule:=(NewCycle SHL 6)OR(Position SHL 4)OR
                (Direction SHL 3)OR(NewGas SHL 1)OR Noise
     END;
END;

BEGIN     {Main program}
     {Make sure wrap is on}
     WorldType:=1;
     {We set bit #2 to be Noiseized each update}
     RandB:=2;
     RandN:=1;
     {We set a vertical pattern of alternate 0s and 1s in
     bit 4 and a vertical pattern of alternate 0s and 1s in
     bit 5.  This produces a pattern that goes
                   0    1    0    1 ..
                   2    3    2    3 ..
                   0    1    0    1 ..
                   2    3    2    3 ..
                   :    :    :    :    }
     TextHB:=4;
     TextHN:=1;
     TextVB:=5;
     TextVN:=1;
     {The Sublime.JCC colorpalette only shows bit 1}
     PalReq:='Sublime';
     {The starting Sublime pattern is some geometric objects}
     PatReq:='Sublime';
     GenRule(JCRule);
END.

For now don't worry about the intricacy of Sublime's definition of the JCRule procedure. Instead let's focus on the special commands in the Main part of the Sublime program, the part at the end. There are thirteen system-defined global variables that can be set here. To organize the discussion, I put these system variables into three groups: i) PalReq, PatReq, and OCodeReq, ii) RandB, RandN, TextHB, TextHN, TextVB, TextVN, RSeedB, RSeedN, RSeedP, and iii) WorldType.

In brief these global system variables serve the following functions:

i)
StartUp: Setting PalReq, PatReq, and OCodeReq allows you to have your rule start by loading a specific .JCC colorpalette, a specific .JCP start pattern, and a specific .JCO own code evaluator.

ii)
Background. The values of the Rand, Text, and RSeed variables control what we call textures of bits that your rule can automatically load into selected planes. These texture bits are often left invisible.

iii)
Topology: WorldType specifies whether the screenwrap is to be on, and whether you want to consider your rule to be two-dimensional or one-dimensional.

StartUp

PalReq and PatReq are particularly useful for creating rules to be shown by self-running demos such as our JCDEMO.BAT. If PalReq and PatReq are not explicitly set to any values, then the pattern and the colorpalette left over from the last rule are used. If you have just entered JC, then the Default.JCC colorpalette is loaded, and the starting pattern will consist of all zeroes in planes #1 through #7, with random bits of plane #0 turned on. This start will be modified by the texture settings, if any.

If a rule requests a .JCC, .JCP, or .JCO file which is not in the current directory, then JC will show a warning message such as:

Cannot open pattern definition file Soot.jcp
Press any key to continue:

After you press a key, JC will continue, leaving the previous colorpalette, pattern, or no own code evaluator in effect.

Background

Using the Rand global variables, we can have random bits fed into any span of bits that we like. Specifying RandB tells JC which bit to start randomizing at, and RandN tells it how many bits to randomize at each update. Thus if RandB is 2 and RandN is 3, then at each update, JC will put random bits in planes #2, #3, and #4. The density of these random bits will always be 50%, meaning that approximately half of each randomized plane's cells will be set to 0 and half to 1. If you require a randomness of, say, 25% ones, you can simulate it by filling two planes with random bits and looking for the cells that have both bits set to 1. The Text global variables feed in horizontal or vertical texture. TextHB tells what bit to start putting horizontal texture in at, and TextHN tells how many bits. If I have one bit of texture, that means that the texture bit will cycle between 0 and 1. If I were to take TextHB to be 5 and TextHN to be 2, however, I would get two bits of texture, meaning that the fifth and six bits would cycle through 00, 01, 10, 11, 00, 01, 10, 11, and so on across the screen. Vertical texture works the same way, and the combination of horizontal and vertical can produce a more complicated pattern as in Sublime.

The RSeed variables allow you to start up a rule with random seed bits in some planes. If we only want some random bits for the startup, but don't want them to keep coming in later, we use the RSeed variables instead of the Rand variables.

RSeedB tells the system what plane to begin random seeding at, and RSeedN tells it how many planes to seed. In addition, the RSeedP variable allows you to specify the percentage of ones you want. (This is not possible for the Rand variables, which always seed at 50%.) RSeedP can be set to any value between 0 and 255. These settings correspond to a percentage of ones which goes from 0% to 50%. Thus a setting of 255 means 50% ones and 50% zeroes; while a setting of 128 means 25% ones and 75% zeroes. RSeedP works only if RSeedN is 1.

Thus if I set RSeedB to 2, RSeedN to 1, and RSeedP to 128, then plane #2 will be randomized at the start of my program by a pattern that is 25% ones, but it will not be randomized again.

The primary purpose of the Seed option is to make it possible to request a start pattern with randomness in some special planes without having to store the random information as part of the start pattern. Look at Soot or Dendrite for examples of this. The reason you don't want to have to store a .JCP files} file which has random bits in one of its planes is that then the file will be about 64K bytes in size, and will take up more disk space than you really want to give it. Because the Soot pattern gets its "random gas" from the RSeed variables, its .JCP file is only some 2K bytes instead of 64K.

When a rule is running, you can see what kinds of texture the rule requested by looking at the status line.

A special feature of the TextH and TextV textures is that you can't get rid of them through editing or changing patterns. The idea is that if your rule calls for these textures, then it needs them, so they are put back in every time you leave the editor or load a new pattern. The RSeed planes are rerandomized whenever you load a new pattern, but not when you leave the editor.

Topology

WorldType specifies three things: a) Whether your screens wrap around the edges, b) Whether a rule is two-dimensional or one-dimensional rule, and c) How big a neighborhood you want to look at, and how many bits of each neighbor you want to see.

The most commonly used WorldType is 1, which means a two dimensional world with wrap turned on. It was actually unnecessary to set WorldType to 1 in the Sublime rule, because in the absence of any other request, WorldType always defaults to 1. To get a two dimensional world with the wrap turned off, you set WorldType to 0.

If you set WorldType to one of the values 3, 4, 5, 6, 8, or 9, your rule will act on a one-dimensional (1D) world. The one-dimensional rules run quite a bit faster than the two-dimensional rules.

The 1D rules work by first copying each line of the screen onto the line below it, and by then filling in the top line with a new line calculated according to JCRule. This produces a spacetime trail of the 1D rule, with earlier times appearing lower on the screen like geological strata.

Our simulator is built to suck in eight bits of neighborhood information. We allow it to get neighborhood information in several different ways. These ways correspond to possible values of WorldType as listed below:

WorldTypeDimensionalityWrap?NeighborsBits
0 2D NoWrap 8 1
1 2D Wrap 8 1
2 1D NoWrap 8 1
3 1D Wrap 8 1
4 1D NoWrap 4 2
5 1D Wrap 4 2
8 1D NoWrap 2 4
9 1D Wrap 2 4
10 2D Wrap 8 Sum of 8
11 2D Wrap 4 Sum of 4
12 User NoWrap User User
13 User Wrap User User

The order in which we feed variables to the procedure JCRule is very important. The actual names of the variables in the JCRule procedure don't really matter, as these names are local "dummy" variables. When we are in 2D mode, we stick to one set of names that are in fact meaningful. We write:

JCRule(OldState, NW, N, NE, W, Self, E, SW, S, SE)

When we are in one of the three 1D modes it is appropriate to call the neighbor variables something else. Different names are appropriate for the three different cases:

In WorldType 2 & 3 (eight one-bit neighbors) use:

JCRule(OldState, LLLL,LLL,LL,L, Self, R,RR,RRR,RRRR)

In WorldType 4 & 5 (four two-bit neighbors) use:

JCRule(OldState, LL1,LL0,L1,L0, Self, R1,R0,RR1,RR0)

In WorldType 8 & 9 (two four-bit neighbors) use:

JCRule(OldState, L3, L2, L1, L0, Self, R3, R2, R1, R0)

Each L and R variable is to be thought of as holding one bit, as diagrammed below.

Eight Neighbors
Bit 0LLLLLLL LL L Self R RRRRR RRRR

Four Neighbors
Bit 1LL1L1   R1 RR1
Bit 0LL0L0 Self R0 RR0

Two Neighbors
Bit 3L3 R3
Bit 2L2 R2
Bit 1L1 R1
Bit 0L0SelfR0

To give an example of a one-dimensional rule, I give the code for the rule Aurora.PAS below. Aurora uses two four-bit neighbors, so its JCRule definition takes the form

JCRule(OldState, L3, L2, L1, L0, Self, R3, R2, R1, R0)

Within the context of this rule there is no specific "L" variable, so we use the name "L" to stand for the four-bit combination of L3, L2, L1, and L0. That is, we set L to 8*L3 + 4*L2 + 2*L1 + L0 in order to stack the four binary bits L3, L2, L1, and L0 on top of each other to get a four-bit number. And we do the same thing for R. We also get a four-bit variable C for the cell's own state by ANDing OldState with 15. This gets the low four bits out of OldState because 15 in binary is 00001111, and ANDing any of the eight bits B in OldState with a 0 produces 0, while ANDing a bit B with a 1 produces B.

PROGRAM Aurora;

{A one dimensional rug rule with two neighbors, and 4 bits
of each neighbor visible.  This is run as a sixteen state
rule, where:
     NewC = (L + OldC + R) / 3  + 1. }

USES JCMake;
{$F+}  { Required for function argument to genrule. }
FUNCTION JCRule(OldState,L3,L2,L1,L0,Self,
                         R3,R2,R1,R0:integer):integer;
VAR
     L,C,R,Average:integer;
BEGIN
     { Develop 4 bit values of neighbors. }
     L := 8*L3 + 4*L2 + 2*L1 + L0;
     C := OldState AND 15;
     R := 8*R3 + 4*R2 + 2*R1 + R0;
     Average:=(L+C+R)DIV 3;
     JCRule:=Average+1;
END;
BEGIN {Main}
    WorldType := 9;  {  World type:  2 neighbor ring }
    PalReq:='Aurora';
    RSeedB:=0;       { Randomize all four bits at start }
    RSeedN:=4;
    GenRule(JCRule);
END.

In the rule descriptions at the end of this chapter I give an example of a WorldType 5 rule (ShortPi) and an example of a WorldType 2 rule (Axons).

Choosing WorldType 10 or 11 causes JC to evaluate averaging rules. These rules were devised to allow generalizations of the Rug rule of RC. In both of these rules the screen is wrapped. WorldType 10 computes the sum of EveryCell's eight nearest neighbors, and WorldType 11 gets the sum of EveryCell's four nearest neighbors. Since WorldType 11 has less work to do it runs faster than WorldType 10, although both types run slower than do our standard two-dimensional rules.

In the averaging rules, the first argument passed to JCRule holds the low five bits of the EveryCell's old eightbit state, and the second argument passed to JCRule holds the sum of the EveryCell's neighbors. (Eight neighbors in WorldType 10, and 4 neighbors in WorldType 11.) This sum can take as many as eleven bits to write out, which is why we are only allowed to see five bits of EveryCell's old state. The limitation is that our rules use lookup tables whose entries are indexed by sixteen bit "situation" codes.

In WorldTypes 10 and 11, the variables other than the first one are simply placeholders, and have no functionality whatsoever. We simply label them with the letters a through h.

As an example of WorldType 10, here is a program called Heat. A Heat cell takes a straight average of its neighbor cells, except that if a cell has its low bit on, the cell's value is kept fixed. The idea is that this rule is to simulate the heat flow in a metal plate certain of whose locations are kept fixed at certain temperature values.

PROGRAM Heat;
{This is an eightcell averaging rule with zero increment.
Odd states are frozen states and even states generate even
states.  One can reanimate the vacuum by pressing i 6 r 
or i 5 r.  }
USES JCmake;
{$F+}
FUNCTION JCRule(FiveBits,Sum,
                a,b,c,d,e,f,g,h:integer):integer;
BEGIN {Function}
     JCRule:=(Sum SHR 3) AND 254;
     IF odd(FiveBits)THEN
          IF FiveBits<16
               THEN JCRule:=FiveBits
               ELSE JCRule:=128+FiveBits;
END;  {Function}
BEGIN {Main}
     WorldType:=10;
     GenRule(JCRule)
END.  {Main}

WorldTypes 12 and 13 are for "own code rules." Type 12 has wrap turned off, with zero on the boundary; and Type 13 is the torus wrap mode. To run a rule of WorldType 12 or 13, one must have a predefined inner loop function. These inner loop functions have extension .JCO. They are discussed more fully later in this section.

The Rug rule below is an example of a rule of this type. I could have written a similar Rug rule using WorldType 10, but I wanted to have the wrap off. The JC Rug rule calls a function called Semi8.JCO which returns the eleven bit sum of the eight nearest neighbors, so we can use this function to define a rug rule.

PROGRAM Rug;
{This program runs an eightcell averaging rule of eight bits
per cell.  We program it as a nowrap owncode WorldType 12
calling Semi8.JCO.}
USES JCmake;
{$F+}
FUNCTION JCRule(OCValue,
                a,b,c,d,e,f,g,h,i:integer):integer;
BEGIN {Function}
     JCRule:=((OCValue SHR 3)+1) AND 255;
END;  {Function}
BEGIN {Main}
     WorldType:=12;
     OCodeReq:='Semi8';
     GenRule(JCRule)
END.  {Main}

The speed at which the simulator runs depends only on the WorldType you have chosen. It does not depend at all on the complexity of the Pascal rule you write or on the start pattern you select; it is completely constant. Thus, there is no special necessity to make the function that defines the rule efficient--it is executed only to create the rule definition file, then never used again. The paramount consideration in writing a rule is that it be clearly expressed so that you can come back to it later and still be able to tell what you were trying to do.

.PAS rule programs are provided for all the JC demos. A good way to start writing rules of your own is to copy one of our rules onto your own file FIRST.PAS. Then you can use Turbo to edit FIRST.PAS to your own purposes and use Alt-R to run it and generate your FIRST.JC file. If it happens that your rule either 1) fails to define a value of JCRule for some inputs or 2) defines a value of JCRule outside the range 0-255, then you will get an error message when the program tries to generate FIRST.JC. If this happens, change something in your program and try again.

Back to Defining Rules

Onward to Defining Rules in C


Next Previous Contents