The Code


    0d=d:IFdVDUd:a=POINT(32*POS,31-VPOS<<5):RETURNELSEMODE9:GCOL-9:CLG:O
FF:d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=28VDUPOS,15,VPOS,24;11,26:IF0E
LSEIFa=0PRINT:UNTIL0ELSEUNTILVPOS=25:v=ABSRNDMOD7:i=0:VDU4895;3:REPEATm=
9-INKEY6MOD3:FORr=TRUETO1:t=rANDSGNt:IFt=rCOLOURv-15:VDUrEORm:i+=m=7AND9
-6*r:IF0ELSEFORn=0TO11:d=n/3OR2EORd:GOSUBFALSE:IF1<<(n+i)MOD12AND975AND&
C2590EC/8^vVDU2080*ABSr;:t+=a:IF0ELSENEXT,:VDU20:UNTILt*LOGm:UNTILVPOS=3

This is the code for Rheolism - a one line Tetris like game. What follows is an explanation of how it works.

Overview

Procedurally the code falls into three major sections:

The state of the board is stored on the screen. Other information is stored in program variables and in the graphics state maintained by the operating system.

Knowing a little about the workings of BBC BASIC and graphics on Acorn computers, will be helpful in understanding the program. The program is rather intimate with the workings of the interpreter and the graphics system. The following sections provide some background and details about both the interpreter and the graphics system. If you are already familiar with BBC BASIC programming or just want to get a broad view, you might like to skip to the later sections. You can always read about the details later.

The BBC BASIC Interpreter

A BASIC program is a sequence of numbered lines of code stored in ascending order. Each line of the program is stored with a line number and length of the line. Each line of code can contain multiple statements. The code is stored in plain ASCII, except for the keywords and line numbers. Keywords are stored as one (or sometimes two) byte tokens, and line numbers in the code are stored in four bytes.

The interpreter works by scanning over the program one byte at a time, executing the statements as it goes. Variable names are hashed, and only created when they are first assigned to. Attempting to use a variable that doesn't exist causes an error. No parsing is done before execution. All the work of understanding the structure of the program is done during execution. Errors in syntax are checked for during execution.

Colons are used to separate statements when multiple statements are written on a single line. They can be omitted after certain statement keywords that don't take an argument.

Variables are followed by a % or a $ to indicate whether they are integer or string variables respectively. Without such a suffix, they are assumed to be floating point variables. A LET statement allows assignment to these variables, and they represent their value when used in an expression. The LET keyword in assignments can be and normally is omitted in BBC BASIC.

Numbers are written in decimal by default; hexadecimal numbers may be entered by introducing them with "&". Binary numbers are prefixed by %. So 123, &7B, %1111011 all express the same number. We use this convention of prefixing a number with "&" to indicate a hexadecimal number in the explanation of the code below.

In an expression, some keywords represent built in functions or operators. Here are some of the built-in functions used in this program:

ABS x
absolute value of x.
SGN x
the sign of x: -1, 0, 1 if x is <, = or > 0 respectively.
INKEY x
used to read the keyboard.
TRUE
A pseudo variable, returns -1.
FALSE
returns 0.
RND
returns a random 32 bit number (when used with no arguments).
POS
a pseudo variable - returns horizontal position of the text cursor
VPOS
the vertical position of the text cursor.
POINT(x,y)
the colour of the pixel at [x,y].

Some statements and functions expect integer values. If a floating point value is used, it is automatically converted to an integer, by rounding towards zero. Statements that expect single-byte or two-byte arguments will first convert to an integer before using the bottom byte(s). Integer values are all stored 32-bit two's complement.

Boolean expressions, as in BCPL, are just integer expressions. Comparisons evaluate to -1 for TRUE, or 0 for FALSE. The infix binary operators AND, OR and EOR (exclusive or) and the unary operator NOT, perform bitwise operations on their arguments, and are used both as bitwise operators and as boolean operators. The statements that take conditions evaluate the condition as an integer and treat any non-zero value as true.

Conditions in BASIC are often handled by optionally jumping to a different line. In a one line program this is not possible, and fortunately BBC BASIC allows one line IF statements. An IF statement looks like this:


IF {condition} THEN {statements} ELSE {more statements}

or


IF {condition} THEN {statements}

During execution, first the condition is evaluated. If the condition is non-zero, execution continues after the THEN token. If the condition evaluates to zero, the interpreter scans for the ELSE token (or end of the line if it can't find the ELSE), and continues from there. In both cases when the interpreter starts executing, it does not remember it is executing conditional code. When the end of the line is reached the interpreter continues on the next line. This also has to happen when the ELSE keyword is encountered during execution. The THEN keyword is optional, and so is the ELSE clause.

In a one line program, we want to be able to execute some statements conditionally, but continue execution on the same line. If we ever execute an ELSE, execution will finish, there not being a next line to move on to. On the other hand, we need the ELSE clause there so that execution can continue if the condition should be false. The solution is to write:


IF 0 ELSE

at the end of the conditional statements. So a condition looks like this:


IF {condition} {statements}:IF 0 ELSE ...

If the condition is true, the statements are executed, and then the second IF statement. This second IF has a false condition which causes execution to be resumed after the ELSE. If the original condition is false, execution skips to the same ELSE, as the interpreter doesn't treat IF in a special way when scanning for the ELSE. In both cases execution continues after the ELSE clause.

Loops are also dealt with in a simple way. However, the interpreter has to remember some state to allow loops to nest properly. When a loop is started, the position of the start of the loop is remembered on a stack. An example of this is a REPEAT UNTIL loop, which looks like:


REPEAT {statements}:UNTIL {condition}

The REPEAT command causes a position in the program to be remembered on a stack; execution simply continues after that point. When an UNTIL statement is encountered its argument is evaluated. A false condition causes execution to be resumed at the stacked position of the REPEAT command. A true condition, simply causes the interpreter to continue after the UNTIL statement, after popping the REPEAT point from the stack.

The way that REPEAT UNTIL loops are handled, means that the UNTIL statements don't have to match up with the REPEAT statements. This happens at one point in our program: there are two different UNTIL statements that end the scanning loop. These are executed according to which branch of the IF is taken.

The FOR...NEXT loop is implemented in a similar way. The syntax is:


FOR v=a TO b:{statements}:NEXT v

The purpose of the FOR loop is to execute the intervening code with "v" set to the all the values from "a" up to and including "b", starting at "a" and incrementing by one each time round the loop.

BBC BASIC provides facilities for defining functions and procedures (functions that don't return values). These are not useful in a one line program, as new definitions must begin on separate lines, and the interpreter skips over lines containing definitions.

The Graphics System and VDU commands

Graphics are produced using the VDU system. This accepts bytes one at a time and puts them on a queue. This queue is interpreted by the VDU system as a sequence of instructions which have some graphical effect, or change some state which controls how the graphics are displayed. Most values cause the equivalent ASCII character to be printed. Values below 32 each have their own meaning, and are called VDU commands. Some of the meanings correspond to ASCII control codes.

All sorts of other clever things are possible with the VDU commands. Here are some example VDU commands, many of which are used in the program:

0
does nothing
6
enable screen output
7
bell: makes a noise
8
back space: moves cursor left one character
9
horizontal tab: moves cursor right one character
10
line feed: moves cursor down one character
11
vertical tab: moves cursor up one character
12
clear text screen
16
clear graphics window
17,c
set text colour to palette colour c
22,m
set screen mode
25,t,x,y;
plot using "action code" t, at graphics location [x,y].
26
restores windows to defaults
28,x1,y1,x2,y2
sets the text window
30
home cursor. moves cursor to (0,0), the top left of the screen.
31,x,y
moves the text cursor to (x,y).
32-126
draw corresponding ASCII character
127
delete character, goes back one character and draws a space
128-255
draw character

In BASIC these commands can be accessed using the VDU statement. The VDU statement evaluates its arguments, putting each onto the VDU queue. Some commands, for example VDU 25, take 16 bit parameters. These are put onto the VDU queue as two separate bytes; the least significant one first. In the BASIC VDU statement an expression can be followed by a semi-colon to indicate it is a two byte parameter. For example the command:


VDU25,4,300;100;

will put codes 25, 4, 46, 1, 100, 0 on the VDU queue, which will then be interpreted as a command to move the graphics cursor to [300,100].

There are many other BASIC statements that perform many of the more useful operations more conveniently, for example:


MODE m

does the same thing as:


VDU 22,m

COLOUR, CLG and GCOL all just put the relevant codes onto the VDU stream. Printing strings and numbers is handled using the PRINT statement.

Reading from the screen can be done using the POINT(x,y) built-in. As this returns a value, it cannot simply put a command on the VDU queue and so works in a different way.

There is a fair amount of graphics state stored in what are called VDU variables. These control the way graphics and text are printed on the screen. There are controls for both graphics and text, which rarely interact except that they both affect the same screen. So there are separate cursors, one for graphics and one for the text screen, as well as different text and graphics colours.

The resolution and number of colours the screen has depends on the current graphics mode, (which is set by the appropriate VDU command). Text characters are drawn 8 pixels high and 8 pixels across. The screen is divided into a grid of character squares (which are square if the pixels are). The text cursor occupies one of these squares and advances when a character is printed.

Board state

Our program, Rheolism, uses graphics MODE 9. This has the advantage of having 16 colours, enough for all the bricks, and of having square characters. Unfortunately this is a graphics mode that is only available on later 32-bit Acorn machines, not on the original BBC Micro.

MODE 9 is a 16 colour, 320 by 256 pixel graphics mode. The pixels are roughly square, and therefore so too are the characters, which are all 8 by 8 pixels. The text operations view the screen as being composed of a grid of 40 by 32 of these characters. The graphics operations see the screen as being 1280 by 1024 units, the units in this case being a quarter of the edge length of a pixel.

We store the current position of the brick in the text cursor, being careful only to make relative motions. We draw and undraw by printing spaces after setting the appropriate background text colour. Reading from the screen we use a graphics and not a text operation.

The text screen is indexed from the top left of the screen. The VDU variables which store the horizontal and vertical positions of the text cursor can be read using the (one byte) pseudo-variables POS and VPOS. The cursor position can be changed either absolutely using VDU 31,x,y (or PRINT TAB(x,y);), or relatively using VDU 8,9,10,11 (see list of VDU commands).

When we read from the board we use graphics coordinates. These are indexed from the bottom left of the screen. In the MODE we are using, there are 4 units per pixel in each direction so a character square is 32 units across and 32 units high. So the bottom left of the screen is at text position (0,31) or graphics position [0,0], and extends to text position (39,0), whose top right pixel is at [1276,1020].

Using square brackets to denote a graphics position and parentheses for text positions, the text square at (x,y) extends from [32*x,992-32*y] at the bottom left to [32*x+31,1023-32*y] at the top right.

The game board is 10 columns wide and 25 rows deep. It is placed centrally, and extends down from the top of the screen to row 24. The bottom left of the board is at (15,24); the top right is at (24,0).

We always draw the graphics so that each character square is entirely one colour. Any black square is considered empty and, after initialization, the screen outside the rectangular board is always white.

Structure of the Code

To aid understanding, I repeat the code here with added spaces and indentation. The spaces help to show how the program is tokenized, and the indentation suggests a block structure to the program. The program as shown will not execute as a BASIC program as the conditional actions are split onto separate lines.

All variables used in the program are lower case, so any upper case character is part of a tokenized keyword. Code fragments that occur later in the text have parentheses as well as spaces added to clarify where the tokens begin and end, and help illustrate precedence of the operations.

The colons which separate statements are omitted here to aid readability. They are not explained again, but simply occur in the code exactly where they are needed by the interpreter.


0 d=d
  IF d
    VDU d                         | the subroutine
    a=POINT(32*POS,31-VPOS<<5)    |
    RETURN                        |
  ELSE
  MODE 9    | init code
  GCOL -9   |
  CLG       |
  OFF       |
  d=9       |
  REPEAT              - start brick loop
    VDU 30                        | scanning code
    REPEAT                        | | scanning loop
      GOSUB FALSE                 | |
      IF POS=28                   | |
        VDU POS,15,VPOS,24;11,26  | |
      IF 0 ELSE                   | |
      IF a=0                      | |
        PRINT                     | |
    UNTIL 0                       | + end scanning loop
      ELSE                        | |
    UNTIL VPOS=25                 | + end scanning loop
    v=ABS RND MOD 7   | brick init
    i=0               |
    VDU 4895;3        |
    REPEAT                                                | key loop
      m = 9 - INKEY 6 MOD 3                               |
      FOR r=TRUE TO 1                                     | | phase loop
        t = r AND SGN t                                   | |
        IF t=r                 - motion condition         | |
          COLOUR v-15          | motion                   | |
          VDU r EOR m          | code                     | |
          i+= m=7 AND 9-6*r    |                          | |
        IF 0 ELSE                                         | |
        FOR n=0 TO 11                                     | | | flower
          d= n/3 OR 2 EOR d                               | | | loop
          GOSUB FALSE                                     | | |
          IF 1<<(n+i) MOD 12 AND 975 AND &C2590EC / 8^v   | | + action
            VDU 2080 * ABS r;   | action                  | | |  condition
            t+=a                | code                    | | |
          IF 0 ELSE                                       | | |
      NEXT,                                               | | |
      VDU 20                                              |
    UNTIL t*LOGm                                          |
  UNTIL VPOS=3        - end brick loop

Variables

These are the variables used in the program.

d
direction of motion
a
colour of tested square
v
brick number (0-6)
i
brick orientation (in multiples of 30 degrees)
m
action command
r
phase in phase loop
t
motion control variable
n
flower loop index

Some extra state is stored in the VDU variables, and on the screen.

The board state is stored as described above. The cursor position stores the position of the brick during the key loop, and the position of the scan during the scanning loop.

The Subroutine

A subroutine is a block of code identified by a line number. There being only one line, there can be at most one subroutine. Rheolism has exactly one subroutine.

As we will see below, the subroutine does two things. First, it moves the cursor in the direction d. Second, it reads the colour of the now current square.

The subroutine is invoked by:


GOSUB FALSE

The GOSUB causes a jump to a subroutine (GOto SUBroutine). The keyword FALSE is a built-in constant that evaluates to 0, which is also the line number the subroutine starts on. We don't write:


GOSUB 0

not to obfuscate but because the 0 here tokenizes to 4 bytes, whereas FALSE is a single byte token. There only being one line in the program, it has also to be the line for the subroutine, which is chosen to be 0.

The start of the line:


    0d=d:IFd

introduces the subroutine, at only line 0, and also causes the program to start executing after the first ELSE.

The first statement:


d=d

is an assignment to the variable d of the value of the variable d. This is not an obvious start, you may think, when trying to write compact code. Yet, if the statement is missing, the program will not work.

When the interpreter finds an assignment to a variable name that hasn't been seen before, it creates that variable. If an as yet unknown variable is used in an expression then the interpreter will report an error. The statement is there to avoid this error. It so happens that the interpreter initializes the variable with the value 0 on creation. When the right hand side of this assignment is evaluated, the variable will now exist, and have the value 0. The effect then, of the above statement, is to create the variable, d, with value 0 on first execution. Subsequent executions preserve the value of the variable d.

The following IF statement helps to explain the previous statement:


IF d

The intention is that d will be non-zero when we use the line as a subroutine. This condition is used to test whether we are inside the subroutine or not.

So if we are in a subroutine, d will be non-zero, and the following code is executed:


VDUd:a=POINT(32*POS,31-VPOS<<5):RETURN

The final statement, RETURN, marks the end of the subroutine, and the following ELSE introduces the remainder of the code, which is executed once on the first execution of the initial IF statement.

The subroutine takes the variable d as an input, and uses the variable a as an output. The routine does two things. First it executes the VDU command stored in d with:


VDU d

This updates the current position of the cursor according to the VDU command stored in the direction variable d. The VDU command stored in the variable d will usually step the cursor left (8), right (9), down (10) or up (11). Second, it reads a pixel colour from within the character at the now current text cursor position. This is done by the command:


a=POINT(32*POS,31-VPOS<<5)

To do this we have to translate text coordinates into graphics coordinates. The expression:


32*POS

scales the horizontal text position, provided by the tokenized pseudo variable, POS, by 32, giving the left hand x position of the graphics coordinate. The y text coordinate is addressed from the top left of the screen, whereas the graphics coordinate is addressed from the bottom left of the screen. So the expression:


(31-VPOS)<<5

first calculates 31-VPOS, which flips the y coordinate, and then applies the binary shift operator << to scale the result by 32 (2 to the 5th power). The precedence works favourably in this case so there is no need for space wasting parentheses.

It would have been as compact to write:


POS<<5

for the x position and perhaps this would have been clearer.

These x and y coordinates are fed as parameters to the POINT( call, which returns the pixel colour. (I say POINT(, rather than POINT, as the open parenthesis is part of the token.) This value is assigned to the variable a. It does not matter which pixel within the square is examined as they are all the same colour.

The next statement:


RETURN

simply returns from the subroutine to the statement after the GOSUB. The variable d is unchanged. The variable a now contains the colour value at the text cursor. The value will be zero only for a black square, which is our representation and display of an empty square on the board.

Initialization Code

After skipping the subroutine, the first part of the code is the initialization (or init) code, and is executed only once:


MODE9:GCOL-9:CLG:OFF:d=9

The first statement:


MODE 9

sets the screen mode and most of the graphics state to sensible default values.

The next statement is a graphics operation:


GCOL-9

The GCOL command sets the graphics colour. The command takes a one byte value. This byte is formed from the bottom 8 bits of the signed 32-bit integer value -9, that is, 247. For MODE 9, the graphics colour is chosen based on the bottom 4 bits, colour 7, which is by default white in this MODE. There is a foreground graphics colour and a background graphics colour. In this case bit 7 of the argument is set, which indicates that we are setting the background colour.

The next statement:


CLG

is a single token, short for CLear Graphics, which clears the graphics window, in this case the whole screen, to its background colour. So this paints the screen white, the graphics colour we have just set.

It may seem more natural to use:


COLOUR-9:CLS

which has the same effect, but using text operations. This however would not work as we rely on the text background colour being black when we enter the scanning code.

The next statement:


OFF

and the colon that follows it is a two byte indulgence. This simply turns the text cursor off, which otherwise would flicker and flash away on the screen, looking untidy.

The final statement of the initialization:


d=9

sets the direction d to its usual value, 9 (right). We have already seen that the value of d is also used to indicate that we are calling a subroutine when we use the GOSUB command. We always use a positive value of d and so this is not a problem.

The Brick Loop

The next piece of code is the brick loop.

This loop is responsible for:

The brick loop is a REPEAT UNTIL loop, beginning with the statement:


REPEAT

This is matched by the final statement:


UNTIL VPOS=3

These statements and everything in between form the brick loop:


REPEATVDU30:REPEATGOSUBFALSE:IFPOS=28VDUPOS,15,VPOS,24;11,26:IF0ELSEIFa=
0PRINT:UNTIL0ELSEUNTILVPOS=25:v=ABSRNDMOD7:i=0:VDU4895;3:REPEATm=9-INKEY
6MOD3:FORr=TRUETO1:t=rANDSGNt:IFt=rCOLOURv-15:VDUrEORm:i+=m=7AND9-6*r:IF
0ELSEFORn=0TO11:d=n/3OR2EORd:GOSUBFALSE:IF1<<(n+i)MOD12AND975AND&C2590EC
/8^vVDU2080*ABSr;:t+=a:IF0ELSENEXT,:VDU20:UNTILt*LOGm:UNTILVPOS=3

When this code is first executed, the initialization is not yet complete. We have yet to empty the board, which starts off full. The observation that saves us some code is that the clearing of the board can be performed by the code that removes completed lines. We therefore start the loop by scanning for completed lines. The code occurs in the order:

scanning code
removes completed lines
brick init
initializes the brick
key loop
controls the play of the brick

The Scanning Code

Before we start play, we have to empty the playing board. This is equivalent to starting with a full board and removing all the completed lines from it. So the loop starts with the code for detecting and removing complete lines:


VDU30:REPEATGOSUBFALSE:IFPOS=28VDUPOS,15,VPOS,24;11,26:IF0ELSEIFa=0PRINT
:UNTIL0ELSEUNTILVPOS=25

We use the current cursor position to mark where we are in the scan. This can mark a vertical and a horizontal position. We arrange the code so that we only need one loop.

The code scans across one line at a time. When it notices an empty square it continues on the next line. If it encounters a complete line it removes it and starts again. Scanning finishes when the cursor reaches row 25.

The VDU 30 command is the instruction to move the text cursor to its home position at the top left of the screen.

The scanning loop is a REPEAT loop. This loop is terminated in two places: first by:


UNTIL0

which always loops, and second by:


UNTIL VPOS=25

which ends the loop just as we start scanning row 25, the row just below the bottom of the board.

The first statement inside the loop:


GOSUBFALSE

is the first call to the subroutine. This steps the cursor to the right and checks the colour of the square. The direction variable d always has its usual value 9 (right) at this point in the program.

The subroutine will first advance the text cursor by one square to the right and then return the colour of that square in the variable a.

The next statement is an IF statement:


IF POS=28

Recall that POS is the pseudo variable which contains the horizontal position of the text cursor. The condition tests to see if the text cursor has reached column 28. If we've reached this column, we assume that the line on the current row is completed, and now need to remove it. We explain the code for removing a line below. Usually we are not at column 28 and the condition will be false. Let's examine this case first.

When the condition is 0 (false), the IF statement continues after the next ELSE. In this case this is followed by another IF statement:


IF a=0

Since the variable a contains the return value from the subroutine, this tests to see if we have detected an empty square at the current cursor position. If we have then we know there is a gap on the current line. When we find a gap we must move onto the next line. In that case, we execute the following two statements:


PRINT:UNTIL0

The PRINT statement, when used like this without any arguments, moves the text cursor to the start of the next text line. The start of the next line, is at the left hand edge (column 0), one line down. This both increments the row we are on and moves the horizontal position back to the left of the board.

The next statement:


UNTIL 0

simply causes execution to continue at the start of the REPEAT loop. We haven't checked we have moved onto row 25 yet, but this will happen after harmlessly checking the first character of the row is occupied.

So when we find a gap the scanning for a line without gaps continues at the beginning of the next line.

The next token:


ELSE

marks where to continue from if the condition:


a=0

is false, that is if the current square is occupied. The statement that follows:


UNTIL VPOS=25

is also an UNTIL statement. This also ends the matching REPEAT statement which begins the scanning loop. Familiarity with the interpreter tells us there is no rule to stop us having different UNTIL statements matching the same REPEAT statement.

The condition:


VPOS=25

uses the pseudo variable VPOS to test when scanning reaches row 25, just below the bottom of the game board. This UNTIL condition terminates the loop as desired, at the end of scanning, and this is the last piece of scanning code.

Now let's look at how lines are removed. This is done entirely by a single VDU statement which issues three commands. The line removal is done by scrolling the board above the completed line down one square. The first command defines a text window around this region, the second causes the scrolling by attempting to move the cursor up off the screen. The third restores the text window defaults.

Every time we go around the scanning loop, the text cursor is moved on one square to the right, provided there is no gap detected there. In a complete line with no gaps, the cursor will eventually reach column 28. When this happens the condition we saw earlier:


POS=28

will be true. The code executed in that case is:


VDU POS,15,VPOS,24;11,26

This VDU statement executes three VDU commands. The first of them is 28, the current value of the pseudo variable POS. We can be sure POS is 28 as the condition just evaluated verifies this. We use the token POS and not the literal 28, as this saves a whole byte.

The VDU 28 command sets the shape of the current text window. By default, the text window is the full screen. The text window defines a region where text is displayed. When the cursor gets to the edge of the window, printing continues on the next line. Attempting to move down on the last line causes text to move up. Similarly moving up from the top line of the window, causes its contents to scroll down.

To specify the size and position of the text window, the VDU 28 command uses the next four bytes issued on the VDU queue as parameters. These parameters specify using character coordinates in turn: the left, the bottom, the right and the top. These define the extent of the text window.

In the above statement these parameters are first:


15,VPOS,

which specify the bottom left position of the window as being at column 15, and the row the cursor is currently on. Second the top right is specified by the expression:


24;

The semicolon in a VDU statement indicates that the expression is to be treated as two-byte quantity and added least significant byte first onto the VDU queue. This puts 24 and then 0 onto the VDU stream in only three bytes of code. The right position of the window is therefore at (24,0), the position of the top right square of the board.

We now have a ten-character-wide text window extending from the top of the screen down to the completed line. A side effect of this command is to move the text cursor to its home position at the top left of this window.

The next argument to the VDU statement is 11. This is the VDU command for moving the cursor up one line. As the cursor is already on the top line, this has the effect of scrolling the contents of the window down by one line. The top line is replaced by a blank line, actually black, the current text background colour. The bottom line of the window disappears.

These simple VDU commands have been used to remove the completed line. The final argument to the VDU statement, 26, is the command for restoring the window defaults to the full screen. It also moves the text cursor to the top left of the screen. This side effect is what makes the scan start again at the top of the board. This means all the incomplete lines will be rescanned, but this has no real harm, other than to slow the program down a bit.

The next three bytes:


IF 0 ELSE

are an example of the way a condition can be terminated in a one line program. Execution continues after the ELSE, joining back with the main flow of execution.

After removing a line we know the variable a is non-zero so the IF statement:


IF a=0

skips to the UNTIL statement:


UNTIL VPOS=25

The cursor at this point has been reset to the top left of the screen. So VPOS is 0, and this condition is false. The scanning loop continues where it began. The loop will eventually terminate, because the cursor only moves backwards whenever we have removed a line. Each time we remove a line, the number of completed lines is reduced. The loop will terminate exactly when we have removed all the completed lines.

Note that the scanning for gaps extended both to the left and to the right of the game board. This works because the screen was painted entirely white before we started, and the parts outside the game board remain that way. We exploit this, by starting to scan at the left edge of the screen, or in fact column 1, avoiding setting the left position explicitly. We choose to stop scanning at column 28 so that POS contains the handy value used in the VDU statement. This relies on column 28 being to the right of the board at column 24 and to the left of the edge of the screen at column 39.

Brick Initialization

The brick init code has to:

The following code does exactly that using three statements.


v=ABSRNDMOD7:i=0:VDU4895;3

The first statement is an assignment:


v=(ABS RND) MOD 7

sets the variable v to a random number representing the brick. RND generates a signed 32 bit random number. The ABS function is applied to this to create a positive random value. The MOD function computes a remainder after division. So the whole expression:


(ABS RND) MOD 7

generates a random number in the range 0 to 6. These represent the 7 different tetrominoes. This brick number is stored in the variable v.

The next statement:


i=0

simply initializes the variable i to the orientation 0. This variable is used to store the orientation of the brick. Resetting the orientation is not something that is strictly necessary, but the statement is needed to create the variable i.

The next statement is a VDU statement which puts the text cursor at the position the brick will be drawn:


VDU 4895;3

The semicolon indicates the 4895 (&131F) is treated as a 16 bit number to be output as two bytes: 31 (&1F) then 19 (&13). The whole statement puts 3 bytes on the VDU stream: 31, 19 and 3. The VDU command 31 moves the text cursor. It takes the next two bytes and treats them as x and y text coordinates. The (x,y) position set here is (19,3), the start position of the brick. This is a central position near the top of the board.

The Key Loop

The key loop controls the play of the chosen brick until it lands. The loop is responsible for the following tasks:

The loop ends when the brick can't fall any further.

This is the key loop code:


REPEATm=9-INKEY6MOD3:FORr=TRUETO1:t=rANDSGNt:IFt=rCOLOURv-15:VDUrEORm:i+
=m=7AND9-6*r:IF0ELSEFORn=0TO11:d=n/3OR2EORd:GOSUBFALSE:IF1<<(n+i)MOD12AN
D975AND&C2590EC/8^vVDU2080*ABSr;:t+=a:IF0ELSENEXT,:VDU20:UNTILt*LOGm

The loop repeatedly checks the keyboard and then attempts the action.

The action command is stored in the variable m, and can have be one of four values:

7
rotate
8
move left
9
move right
10
move down (fall)

The first statement in the loop:


m = 9 - ((INKEY 6) MOD 3)

determines the action command and stores this in the variable m. The sub-expression:


INKEY 6

uses the built-in function INKEY, to read the ASCII code of the key pressed. The parameter 6 is the number of centiseconds the built-in waits, if a key isn't pressed, before giving up and returning -1.

The recommended keys are '7', '8' and '9'. '7' to move left, '9' to move right and '8' to rotate. These are read as their ASCII values: 55, 56 and 57. Calculating the expression:


(INKEY 6) MOD 3

these become 1, 2 and 0 respectively. Many other keys will also work here, including the combinations: '4', '5', '6'; and '1', '2', '3'.

The full expression:


9 - ((INKEY 6) MOD 3)

evaluates to 8, 7 and 9. Conveniently, 8 is the VDU code to move the cursor left one column, VDU 9 moves the cursor right one column. This value is stored in the variable m.

Importantly, we have to distinguish the case when no key is pressed. Fortunately, due to the mathematically strange behaviour of MOD, (-1) MOD 3 evaluates to -1. So if no key is pressed, m ends up with the value 10 (9 - (-1)). VDU 10 moves the cursor down one row.

The Phase Loop

After reading the action, the brick must be moved according to the action, if possible. This is done by the phase loop:


FORr=TRUETO1:t=rANDSGNt:IFt=rCOLOURv-15:VDUrEORm:i+=m=7AND9-6*r:IF0ELSEF
ORn=0TO11:d=n/3OR2EORd:GOSUBFALSE:IF1<<(n+i)MOD12AND975AND&C2590EC/8^vVD
U2080*ABSr;:t+=a:IF0ELSENEXT,

The outside FOR statement:


FOR r=TRUE TO 1

is controlled by the variable r and runs through the three integer values from -1, the value of TRUE, to 1. The variable r encodes the three phases of the loop:

r=-1
undraw brick
r=0
test if brick fits in new position
r=1
draw brick

The code inside the loop has to perform each of these phases and changes behaviour according to the variable r. Much code is shared as drawing, testing and undrawing the brick all involve moving the cursor over the positions that the four squares of the brick cover.

Before we visit the squares of the brick, we must ensure we are in the right position by adjusting the position if necessary. This is stored by the current position of the cursor, and is correct for undrawing the brick. In the (phase 0) case we must first move to the new potential position. If it is rejected, because the brick doesn't fit, we need to move it back again (at phase 1). This is done by the code:


t=rANDSGNt:IFt=rCOLOURv-15:VDUrEORm:i+=m=7AND9-6*r

The variable t has two uses. One is to remember whether the test failed during the r=0 phase. It is also used to help control when the motion code is executed. It's use is fairly tricky, as it behaves differently according to the phase stored in r.

The first statement here:


t=r AND (SGN t)

sets up the secondary control variable, t, which is never negative. The first time this piece of code is run, t will be initialized to zero before executing the expression, which will evaluate to 0, and so leave t as 0. So t starts with a non-negative value. SGN t can only have values of 0 or 1 for non-negative values of t. The bitwise AND of this with any value only results in values 0 or 1. So this statement also ensures t never becomes negative.

In the r=0 case, the t will be set to 0, as a bitwise AND with 0 is always 0. In the r=1 case, we get the bitwise AND of 1 with a value of SGN t. The value of SGN t and the whole expression, will be 1 if t was positive, or 0 if t was 0. The values of t are therefore:

r=-1
t = 0 or 1
r=0
t = 0
r=1
t = 1 if t was non-zero (i.e. brick doesn't fit in new position)

The following IF statement:


IF t=r

uses the motion condition which is true only when we need to adjust the cursor position. In the (r=-1) phase the brick should never move. In this case the value of t can never equal r as r is negative, and t isn't. In the r=0 phase, the condition is always true. In the r=1 phase, the condition is sometimes true. We will see below that this matches the case when the cursor needs to be adjusted back to its old position.

The Motion Code

The motion code:


COLOURv-15:VDUrEORm:i+=m=7AND9-6*r

is executed whenever the motion condition is true. It performs the following tasks:

The position and orientation must be updated according to the action. However, the effect must be undone, if the phase is (r=-1).

The statement:


COLOUR v-15

sets the background text colour to the colour the brick will be drawn in. As the COLOUR command works by issuing the VDU command which sets the text colour, the text colour is determined by the bottom eight bits of the argument only. In this case this is determined by the expression:


v-15

The brick number stored in the variable v is in the range 0 to 6. The value of the argument is therefore in the range -15 to -9. The bottom 8 bits of this argument are in the range 240 to 247, or in hexadecimal &F1 to &F7. The top bit (bit 7), by being set indicates that we are specifying the background text colour, instead of the foreground text colour. The graphics mode we are using (MODE 9) has a palette of only 16 colours; only the bottom four bits are needed, and are used, to specify the colour.

Brick number 0 will be colour 1 in the default palette, which is red. Brick 1 is green; 2 yellow; 3 blue; 4 magenta; 5 cyan; and 6 white (colour 7). We avoid using colour 0, which is black, the colour we are using for empty space on the playing board. Colours 8 to 15 default to flashing colours and are also unused.

The colour is used in the draw phase (r=-1). This code doesn't always execute in that phase, but the colour remains set from the (r=0) phase, in which this code always executes. Setting the colour more than once is harmless.

The next statement updates the brick position:


VDU r EOR m

Remember, we only execute this code if the brick needs moving. In the test phase (r=0), we execute the VDU command in m (as (0 EOR m) = m). The action command in m encodes the left, right and down actions using the VDU command that moves the brick in that direction.

The other action, rotate, is coded as 7. VDU 7 causes a sound to be made. This is an amusing, but harmless, side effect.

If the brick won't fit in its new position, then this command will be executed again when (r=1). In this case the actions are reversed; 8 (left) and 9 (right) swap, 10 (down) becomes 11 (which moves up). The rotate command 7, becomes (1 EOR 7) = 6. VDU 6 is a command to enable screen output; screen output is on by default, and this command has no effect, luckily.

Table of arguments (VDU effects in brackets)
m    r EOR m (r=0)     r EOR m (r=1)
 7    7 (bell)          6 (display on - no effect)
 8    8 (left cursor)   9 (right cursor)
 9    9 (right cursor)  8 (left cursor)
10   10 (down cursor)  11 (up cursor)

We have one more thing to do when we update the brick position: change the orientation. This is handled by the next statement:


i += (m=7) AND (9-(6*r))

The variable i stores the orientation. It is always non-negative and a multiple of three. Each increment of three represents a rotation of 90 degrees. The expression 9-6*r is 9 in the (r=0) test phase and 3 in the (r=1) draw phase. This changes the orientation of the brick by 90 degrees and 270 degrees (90 degrees back) respectively. (Again the statement is only executed in the draw phase if the brick won't fit in its new place.)

The expression m=7 is -1 (TRUE) if the action is rotate and 0 otherwise. Since AND does a bitwise AND, we either get the value of the other operand or 0. This ensures that the increment is made to the variable i only when the action is rotate.

The end of the code which moves the brick is terminated with the familiar:


IF 0 ELSE

which joins the conditional path back into the main execution of the code.

The Flower Loop

The remainder of the key loop is the flower loop:


FORn=0TO11:d=n/3OR2EORd:GOSUBFALSE:IF1<<(n+i)MOD12AND975AND&C2590EC/8^vV
DU2080*ABSr;:t+=a:IF0ELSENEXT

The job of this loop is to visit each square the brick covers, the active squares, and perform the appropriate action.

The appropriate action corresponds to the current phase:

r=-1
Blank the current square
r=0
Test the square for occupancy.
r=1
Draw a brick colour in the square

The current position is used to store where the brick is, so we need to make sure that we return to where we started. The approach we use is to trace the same shape for each brick, and only perform the action on the active squares.

The pattern we trace out is the following rotational symmetric pattern:

 54
7632
8901
 AB

This is traced out in the order 0,1,2,3,4,5,6,7,8,9,A,B. The active squares can be indicated in only 12 bits. Rotation through a right angle can be achieved by cyclically shifting these bits round by 3 places. (This is why the orientation changes by 3 to rotate by 90 degrees.)

Each of the seven bricks is represented by a 12 bit number (see the encoding table). Fortunately 12*7 = 84, which fits comfortably within a 32-bit constant (&C2590EC).

The flower loop begins with a FOR statement that iterates over the squares in the flower pattern:


FOR n=0 TO 11

The following statement updates the variable d for each iteration around the loop:


d = ((n/3) OR 2) EOR d

In the above expression n/3 the fractional part is ignored when it is used as an operand of the OR operator.

The variable d begins with its usual value, 9. The table shows the values of the variable d for each iteration of the loop:

old d,  n,  INT(n/3), (n/3 OR 2), new d=n/3 OR 2 EOR d
 9      0   0          2          11
11      1   0          2           9
 9      2   0          2          11
11      3   1          3           8
 8      4   1          3          11
11      5   1          3           8
 8      6   2          2          10
10      7   2          2           8
 8      8   2          2          10
10      9   3          3           9
 9     10   3          3          10
10     11   3          3           9

This sequence of values in d are the VDU codes for: up, right, up, left, up, left, down, left, down, right, down, right.

When this sequence of VDU commands is executed, the cursor traces out the above flower shape, starting and ending at B.

This flower pattern has the properties we desire. Not only does the cursor end up where we started, but the final value of the variable d is 9, its usual value.

The next statement:


GOSUB FALSE

calls our subroutine that executes the:


VDU d

command which will move the cursor one step round our flower shape. It also has the side effect of reading the colour of the current square into the variable a. Remember this will be positive if it is not empty (black).

All that is left to do now in the flower loop is to take the appropriate action for all active squares. This code handles that:


IF1<<(n+i)MOD12AND975AND&C2590EC/8^vVDU2080*ABSr;:t+=a

The appropriate action in the undraw (r=-1) phase is to draw a space character under the cursor. This code is shared with the draw case as it is just a matter of setting the right background colour to either draw black or the colour of the brick. In the test phase we don't want to draw anything, but do want to remember if any of the squares are occupied.

Brick Encoding

The encoding of the brick is hidden in the action condition tests for active squares:


1<<(n+i)MOD12AND975AND&C2590EC/8^v

This is non-zero when the current square is active, i.e. covered by the brick.

We use a bit pattern to encode which squares in the pattern are active. Due to the compact flower pattern, it only takes 12 bits to code each brick.

Orientation is quite easy to handle. Due to the rotationally symmetric shape of the flower pattern, we can rotate the brick simply by moving the squares of the brick 3, 6 or 9 squares further round the pattern. This is achieved by using ((n+i) MOD 12) instead of just (n) to pick out the bit to test from the brick pattern . So the first part of the action condition:


1 << ((n+i) MOD 12)

selects the bit of the brick pattern to test.

The brick pattern is evaluated from an expression that depends on the brick number, v:


975 AND (&C2590EC/(8^v))

The bit pattern for the bricks is stored in the hexadecimal number &C2590EC. The bit pattern for brick v starts at bit 3*v. The division /8^v (one character shorter than >>3*v) shifts the bits so that the bricks pattern is at bit 0. The bitwise AND operator is used to pick out the bits given by the number 975 (&3CF hex). This expression has only 8 bits set in it. So although 12 bits are used only 8 of the bits can ever be set. This miraculously gives us enough flexibility to find a way of overlapping all eight bricks so they fit in the space.

This brick expression evaluates as follows:


brick   brick encoding
              BA9876543210
0 (I)   204 (%000011001100)
1 (S)   525 (%001000001101)
2 (J)   579 (%001001000011)
3 (T)   712 (%001011001000)
4 (O)   585 (%001001001001)
5 (Z)   75  (%000001001011)
6 (L)   777 (%001100001001)

 54
7632  7632    32   6    763    63    63     3
8901         90    901   9     90     01  890
 AB

The following table shows the way in which the mask 975 (%1111001111) picks out bits from the 28 bit constant that encodes the bricks. There are 28 squares altogether and 12 bits set in the encoding, so on average each bit is used 28/12 = 2 1/3 times.

brick
         zy    t  q on  k    fed ba   (letters in diagram below)
        %1100001001011001000011101100 (&C2590EC)
0 (I)                     %0011..1100
1 (S)                  %1000..1101
2 (J)               %1001..0011
3 (T)            %1011..1000
4 (O)         %1001..1001
5 (Z)      %0001..1011
6 (L)   %1100..1001

This mask (975) has a geometrical interpretation. It is the 4x2 block that is made up from the centre of the flower and its left and right petals. We can see how the encodings overlap. The centre of each brick (squares 0,3,6,9) is made from the previous brick rotated through 90 degrees clockwise, with only square 9 being chosen afresh. The right petal (squares 1 and 2), is hidden by the mask in the previous brick, but is also the inverted left petal (squares 7 and 8) from two bricks ago.

mask   0     1     2     3     4     5     6

7632  feba    ed   k    onk    qn    tq     t
8901         kb    nef   q     tk     no  yzq

Action Code

Having determined that we are at an active square, we need to take the action as described above. This action is implemented by:


VDU2080*ABSr;:t+=a

As the VDU statement:


VDU 2080 * ABS r;

is terminated by a semi-colon, it puts two bytes on to the VDU stream, the least significant 8 bits of the argument first, and then the next 8 bits. The sub-expression:


ABS r

evaluates to 1 in the undraw (r=-1) and draw (r=1) phases, and zero in the test phase (r=0). So the value of the argument will be 2080 or 0.

When the argument is zero, in the test case, the two VDU codes written will both be 0. VDU 0 is a command with no effect.

When the argument is 2080 (&820), the VDU commands emitted are 32 (&20) then 8 (&8). VDU 32 (ASCII space) draws a space where the cursor is, and advances the cursor. VDU 8 is the code for moving the cursor left, which puts the cursor back to where the space was just drawn. So in the undraw and draw phases a space is drawn. This fills in the current square with the current background colour. This space should be black in the undraw case and the colour of the brick otherwise. We have to check we have the right background colour set at each of these phases.

The default state is that the background colour is black. This is the case when we enter the brick loop. The background colour is set to the colour of the brick in the motion code, as discussed earlier. It may also be set again in the draw phase, but this has no effect. The default colour is restored by the statement:


VDU 20

which follows the brick loop.

The next part of the action updates the variable t:


t+=a

This adds the colour value of the current square to our control variable t. The variable t is initialized to be zero outside the flower loop in the test phase (r=0). The colour values are 0 for black, and positive for the other colours. If each tested (active) square is black, then the net effect will be to leave t unchanged. If any of the active squares are already occupied or are outside the playing board, the net result will be to increase t (by at most 7*4=28). As the variable t is initialized to 0 outside the loop, its value will be positive if any square is occupied (i.e. the brick doesn't fit), and zero otherwise.

One slightly fussy detail to check is that the cursor never moves off the screen. This matters because for example if it moved up off the screen the screen would scroll. As we know it stays within the bounds of the screen, we also know that the squares tested will be either on the board, or part of the background, which is white, and therefore treated as occupied. The flower pattern extends at most three squares up. As we only move the brick up after moving it down, and it starts at (19,3), it never extends above the top of the screen. Also, the position of the pattern is never moved more than one square down, left or right from a legal position. As a legal position means the pattern must intersect with an unoccupied square, the pattern can be at most one square away from overlapping with the board. As the width of the flower pattern is 4 squares, the cursor never moves more than 4 squares left, right, or down of this, which is well within the limits of the screen.

This conditional action code is terminated by the usual:


IF 0 ELSE

This is followed by a NEXT statement that ends both the flower loop and the phase loop:


NEXT,

This is short for:


NEXT n, r

which is equivalent to:


NEXT n: NEXT r

which ends both loops.

The next piece of code is the last statement before the end of the key loop. It is the single statement:


VDU 20

This executes the VDU 20 command which restores the colours to their default values. In particular it resets the background text colour to black, reestablishing its usual value. This is not only used by the action code in the undraw phase, but also by the scanning code.

The next statement is the UNTIL statement that ends the key loop:


UNTIL t * (LOG m)

The key loop terminates when the brick cannot move down. That happens when the occupied test failed and the action was down (m=10).

This is equivalent to the condition:


t * (LOG m)

evaluating to a non-zero number after conversion to an integer. At this point the variable t has the value 0 or 1. It only has the value 1 if brick failed its attempted motion. We have already seen this to be true after the assignment to t at the beginning of phase (r=1). Now we have to show that it doesn't change after that point. The only change to the variable t that can occur is in the action code. This will only change t if there are non-blank squares under the new position of the brick. This is never the case, as either we are drawing where we have just undrawn, or are drawing where we have just checked that there are no filled squares.

The value of the variable m is the action command - either 7, 8, 9 or 10. The LOG built-in function calculates logarithm to base 10. The value of the expression:


LOG m

is 1 when the action is down (m=10), and strictly between 0 and 1 in the other cases.

So the expression:


t * (LOG m)

will then be a value at least 0 and no more than 1. When converted to an integer, it is rounded down. This converted product will be zero, unless the unconverted product is one. The product is one if and only if each factor is one. This is true when the action is down (m=10) and the attempted motion failed (t=1).

A piece always responds to key presses that are made, but if it falls but can't move, the program moves on to the next brick. This means that the pieces aren't too "sticky", and can be moved left and right even when adjacent to a brick below. This is a nice game-play feature.

The brick loop is terminated by:


UNTIL VPOS=3

This loop exits if the brick doesn't drop during its play. This is the case if the height of the cursor, given by the built-in variable, VPOS, is the same as the bricks initial height, 3.

If the game does not stop, we return to the beginning of the brick loop, which will remove any filled lines before continuing with the play of a new brick.

After that there is no more code, and the game simply ends as does this explanation.


David Moore
$Id: explanation.html,v 1.38 2001/07/01 14:54:07 dsm Exp dsm $