This article describes the development of Rheolism, a one line Tetris like game. I outline some of the early developments and examine in detail some of the later changes with reference to actual development versions of the program.
I was looking for a one line game to write that would provide a difficult challenge. The difficulty was in finding something substantial yet simple enough to fit in one line of BBC BASIC.
I had thought of writing a Tetris clone, but had quickly dismissed it as being too difficult. Martin Hollis was not so put off when the idea was independently raised, and quickly coded a version in about 1200 bytes.
Within days, with help from Olly Betts and myself, he had shrunk the program down to just over 600 bytes. This earliest recorded version named "TINY", doesn't resemble the final version in any way.
Piece by piece the program was rewritten and restructured. Two months later we produced a version which would fit on one line.
The rest of this story looks at the developments in some detail and requires a good understanding of BBC BASIC to follow. Understanding the explanation of the final version, Rheolism, is also necessary.
The early versions stored data, such as the number of occupied squares on a row, in arrays. One of the first things that was changed was to use the more compact ! and ? operators which work directly on memory.
An array operation like:
f(b)=f(a)
becomes:
f?b=f?a
This is a whole byte shorter for each reference of the array.
The first recorded version is the "TINY" program.
After this the improvements came in evolutionary and revolutionary steps. Many steps were small gradually weeding out inefficiencies. Some changes were breakthroughs that considerably reduced the amount of code and making previous modifications to now deleted parts of the code redundant.
I never felt it was a clearly possible task. At the time I thought the amount of functionality that needed to be fitted in the program made it doubtful it was possible. However these early versions also showed that the goal was not clearly impossible either.
The changes needed were far from obvious to us. It took about two months to do this development.
In what follows, which is a close look at the program as it evolved, many of the failed ideas are not recorded, but many of the incremental improvements are.
50VDU23,TRUE;TRUE;TRUE;TRUE;TRUE 60MODE9:DIMp 99,z 32,e 32,f 32 70FORA=0TO31:f?A=0:NEXT:$p="" 80REPEAT:p=p+1:UNTILp MOD6=0 90$z="ADAH(4())DAB!L" 100GCOL0,135:CLG:VDU28,5,31,14,0:PRINTSTRING$(250," "); 110GCOL0,1 120REPEAT:VDU26,29,0;1023;:b=RND(7):x=9:l=1:y=3:r=8 130p$="" 140REPEATCOLOUR0:PRINTTAB(x,y)p$:A=z!(b*2-2) 150g=INKEY0MOD9:k=y-l*(g=TRUE):j=x-(g=3)+(g=1):Q=r 160PRINTTAB(j,k); 170o=0:e!(k-4)=0:e!k=0:FORC=p+(g=2)TOp+23:Q=2-(Q>9)EORQ:IFC>=p ?C=Q*(1 ANDA):VDU?C:A=A/2:IFC MOD6=3C=C+2:C?TRUE=TRUE:?C=8:e?VPOS=e?VPOS+1:o=o+P OINT(POS*32,-VPOS*32):IF0ELSENEXT:?C=13:IFo=0x=j:y=k:r=Q:p$=$p 180COLOURb:PRINTTAB(x,y)p$ 190WAIT:WAIT:WAIT:WAIT:WAIT 200UNTILk>y:f!(y-4)=f!(y-4)+e!(k-4):f!y=f!y+e!k:b=31:FORa=31TO0STEPTRU E:VDU28,5,b,14,0,30,-11*(f?a=10),26:f?b=f?a:b=b+(f?a<10):NEXT:UNTILy=3
Briefly the code is organized as follows:
The variables are:
Although, there are places where things can clearly be done slightly more efficiently, we still had a long way to go. The task was to reduce the program size by 60 per cent. We still had many major changes to make.
It was quite clear that so much needed improvement, and that there would not be a lot left untouched. It is interesting to see how little code is shared between the "TINY" version and the final version.
The program, originally developed on a BBC micro had been transferred to an Acorn Archimedes. Some changes were made as a result of the transfer. Line 50 was added to make the program work. Line 190 was added to slow it down. Also, the graphics mode was changed to MODE 9 from MODE 2, which is why the position of the board is on the left hand side of the screen.
The core of the program is line 170. As the loop progresses the direction Q is rotated by the statement:
Q=2-(Q>9)EORQ
This cycles Q through values (8,9,10,11). When a bit in our brick encoding is set, we move in that direction, recording the direction VDU command in ?C. Every full rotation we add a couple of bytes to the string which draw a filled block (character 255). At the same time we record in the variable o, whether the squares we move to are empty. If the brick will fit, we update x,y,r and p$, with the newly constructed values.
Neatly, we store the VDU instructions which draw a brick in a string variable. By changing the text colour, we use the same string to undraw the brick.
Line 200 contains some fairly hairy code. The role of the f array is to store the number of filled squares on each row. The e array does the same thing but for the brick. We use the condition (f?a=10) to see if the row full. Inside the loop over rows, the screen is adjusted with the VDU command using text window scrolling. The array needs to be scrolled down in line with the moving squares on the screen. This is done by the other statements in the loop.
A subtle thing going on here is that e is constructed at a position where we are attempting to draw a new brick. We need to know that this new position has the same orientation as the old one for this code to work.
There is some fairly bold use of ! to zero and add multiple elements of the e array.
60MODE9:DIMp 99,z 32 70$p="" 80REPEAT:p=p+1:UNTILp MOD6=0 90$z="ADAH(4())DAB!L" 100GCOL0,135:CLG:VDU28,5,31,14,0:PRINTSTRING$(250," "); 110GCOL0,1 120REPEAT:VDU26,29,0;1023;:b=RND(7):x=9:l=1:y=3:r=8 130p$="" 140REPEATCOLOUR128:PRINTTAB(x,y)p$:A=z!(b*2-2) 150g=INKEY0MOD9:k=y-l*(g=TRUE):j=x-(g=3)+(g=1):Q=r 160PRINTTAB(j,k); 170o=0:FORC=p+(g=2)TOp+23:Q=LNQ+.8EORQ:IFC>=p ?C=Q*(1ANDA):VDU?C:A=A/2 :IFC MOD6=3C=C+2:C?TRUE=32:?C=8:o=o+POINT(POS*32,-VPOS*32):IF0ELSENEXT:? C=13:IFo=0x=j:y=k:r=Q:p$=$p 180COLOURb+128:PRINTTAB(x,y)p$ 190WAIT:WAIT:WAIT:WAIT:WAIT 200UNTILk>y:FORa=24TO0STEPTRUE:b=0:FORe=5TO14:b-=POINT(e*32,-a*32)>0:N EXT:VDU28,5,a,14;30,-11*(b=10):a-=b=10:NEXT:UNTILy=3
The changes made here are:
The f array, and the e array used to help maintain it, are eliminated. The f array stores how bricks there are on a row. The extra loop we have to put in (at line 200) to test for completed rows, is shorter than all the code to maintain the e and f arrays.
The VDU 23 command to initialize character 255 to a filled block is gone. This was inserted when moving from using a BBC to an Archimedes. Not all Archimedes character fonts have this as a filled square. Fortunately, we can draw a space and set the background text colour instead (using COLOUR128+b).
The other change is to update the rotate Q statement to:
Q=LNQ+.8EORQ
which happens to have the same effect.
60MODE9:DIMp 99,z 32 70$p="" 80REPEAT:p=p+1:UNTILp MOD6=0 90$z="ADAH(4())DAB!L" 100GCOL0,135:CLG:VDU28,5,31,14,0:PRINTSTRING$(250," "); 110GCOL0,1 120REPEAT:VDU26,29,0;1023;:b=RND(7):x=9:l=1:y=3:r=8 130p$="" 140REPEATPRINTTAB(x,y)p$:A=z!(b*2-2) 150g=INKEY0MOD9:k=y-l*(g=TRUE):j=x-(g=3)+(g=1):Q=r 160PRINTTAB(j,k);:q$="" 170o=0:FORC=g=2TO15:Q=LNQ+.8EORQ:IFNOTSGNC a=Q*(1ANDA):VDUa:q$+=CHR$a: A=A/2:IFC MOD4=3q$+=" "+CHR$8:o=o+POINT(POS*32,-VPOS*32):IF0ELSENEXT:IFo =0x=j:y=k:r=Q:p$=q$ 180COLOURb+128:PRINTTAB(x,y)p$ 190WAIT:WAIT:WAIT:WAIT:WAIT 200COLOUR128:UNTILk>y:FORa=24TO0STEPTRUE:b=0:FORe=5TO14:b-=POINT(e*32, -a*32)>0:NEXT:VDU28,5,a,14;30,-11*(b=10):a-=b=10:NEXT:UNTILy=3
Changes in this version are:
The use of q$ instead of $p is a minor change of data structure. It has quite a wide effect as the set of operations available to work on strings, are quite different from those needed to perform similar operations in memory.
The earlier version worked by reserving 6 bytes in memory where the four directions and the two bytes to draw the brick could go. These were written as zero if not used. Here the loop index C is no longer also the index into the string.
The variable C no longer indexes into p. This leads to a more compact condition. Now we can use (C MOD 4) instead of (C MOD 6) to do the test. This change has made the initialization of p on lines 60, 70 and 80 redundant.
I should explain the g=2 in the FOR statement. This is to cause an extra rotation of Q before the real work of the loop begins. This happens only when then action is rotate, and g=2 evaluates to -1 (TRUE) instead of 0 (FALSE).
There are some Archimedes features being used here. The += syntax isn't supported on earlier Acorn computers. Ideally, we would have liked to have a version that worked on the BBC.
Even more ideally, we should not have to "cheat" by taking out spaces that are there to allow the tokenizer to do its job. Even more ideally than that we would like to be able to type in the program. The BASIC editor supplied with the Archimedes allowed you to type in lines more than 255 characters long, which was the limit of the input buffer.
60MODE9:DIMz 32 70$z="ADAH(4())DAB!L" 80GCOL135:CLG 90REPEAT 110FORa=8TO32:b=0:FORe=5TO14:b-=POINT(e*32,a*32)>0:NEXT:VDU28,5,31-a,1 4;30,-11*(b=10):a+=b=10:NEXT 120VDU26 130b=RND(7):x=9:l=1:y=3:r=8 140p$="" 150REPEATPRINTTAB(x,y)p$:A=z!(b*2-2) 160g=INKEY0MOD9:k=y-l*(g=TRUE):j=x-(g=3)+(g=1):Q=r 170PRINTTAB(j,k);:q$="" 180o=0:FORC=g=2TO15:Q=LNQ+.8EORQ:IFNOTSGNC a=Q*(1ANDA):VDUa:q$+=CHR$a: A=A/2:IFC MOD4=3q$+=" "+CHR$8:o+=POINT(POS*32,1023-VPOS*32):IF0ELSENEXT: IFo=0x=j:y=k:r=Q:p$=q$:IF0ELSECOLOURb+128:PRINTTAB(x,y)p$:WAIT:WAIT:WAIT :COLOUR128:UNTILk>y:UNTILy=3
Changes:
The previously redundant initialization of p is now removed.
The statement that sets the graphics origin:
VDU 29,0;1023;
is removed, instead more work is done when reading from the screen, so
POINT(POS*32,1023-VPOS*32)
replaces
POINT(POS*32,-VPOS*32)
This adds 8 bytes, as the POINT( call occurs twice. However this replaces 10 characters of VDU arguments. I think there is already a hope here that the POINT() code may one day be shared.
The most significant improvement is that the code to remove lines on line 110 has been moved from the end to the beginning of the brick loop. Since the board state is now entirely what is on the screen, the board can be emptied by simply having this code execute before the first brick falls.
This makes the previous code to clear the screen:
VDU28,5,31,14,0:PRINTSTRING$(250," ");
has been removed. Why this code prints 250 spaces instead of just using VDU12 or CLS, I don't know. In general not as much effort has been put into the initialization code. This is because most of the attention is being paid to the heart of the code on line 180. This is where the major changes need to be made. This focus is vindicated here as fiddling around with code that is to be deleted would have mostly been wasted.
20MODE9:GCOL135:CLG 30REPEAT 40FORa=8TO32:b=0:FORe=5TO14:b+=SGNPOINT(e*32,a*32):NEXT:VDU28,5,31-a, 14;30,-11*(b=10):a+=b=10:NEXT 50VDU26 60b=RND(7):x=9:l=1:y=3:r=8 70p$="" 80REPEATA=b!(b+TOP-18) 90g=INKEY6MOD9:k=y-l*(g=TRUE):j=x-(g=3)+(g=1):Q=r 100q$=CHR$31+CHR$j+CHR$k:PRINTp$q$; 110o=0:FORC=g=2TO15:Q=LOGQ+2EORQ:IFNOTSGNC a=Q*(1ANDA):VDUa:q$+=CHR$a: A=A/2:IFC MOD4=3q$+=" "+CHR$8:o+=POINT(POS*32,1023-VPOS*32):IF0ELSENEXT: IFo=0x=j:y=k:r=Q:p$=q$:IF0ELSECOLOURb+128:PRINTp$:COLOUR128:UNTILk>y:UNT ILy=3 120REMADAH(4())DAB!L
Changes:
The brick encoding has been put in a comment at the end of the program. This saves on all the code initializing z. The old code to read the coding was:
A=z!(2*b-2)
The operator ! takes the value of the variable on the left and adds its right hand operand, and then reads a 32 bit word from memory. Although this reads four bytes, we only use two.
The new code does the same thing, except the coding is stored at TOP-16. TOP is a pseudo variable that gives the address of the end of the program. As TOP is not a proper variable, it cannot be used on the left of "!", so the new code reads:
A=b!(b+TOP-18)
This is a shorter than the equivalent:
A=!(TOP-16+b*2-2)
Storing the brick in the program text smacks of desperation. We would definitely prefer to avoid it. The realization here is that in order to be able to make the program short enough, the coding either has to be done like this to avoid lots of initialization, or we have to find a way of coding the brick in a single constant.
A constant can be 32 bits. The saving of storing the bricks in 7 bytes instead of 14 would not be huge so is not worth a great deal of effort. Storing it in a constant the can go directly in an expression is well worth it, particularly as it avoids this ugliness.
The best change here is in the dealing with the strings. The three occurrences of:
TAB(x,y)
have been replaced by:
q$=CHR$31+CHR$j+CHR$k
The other associated changes also result in a saving.
The resulting code is all quite cunning. Putting the VDU commands to position the cursor into the brick string is the motivation. [We get an advantage from extension here.] We use the variable q$ to store the position command as well as using it to draw the brick. We also extend assignment that initializes q$ to set it up with the right VDU commands. The use of q$ rather than a separate variable may seem, and perhaps is, natural. We have also noticed that we can use q$ before it is fully constructed to perform the task of positioning the cursor unconditionally at the beginning of the test loop. Previously, the value of q$ was only used if the test succeeded.
We have also reduced the test on line 40, by observing
b+=SGNx
is shorter (as SGN takes one byte) than
b-=x>0
and has an equivalent effect if we know x is positive. There is also a reduction in the statement:
Q=LOGQ+2EORQ
on line 110.
There are many of these small changes in expression that give small savings throughout the development. [expression] Neither of these reductions made it into the final program. I would like to make the point that this work was therefore wasted. However, we were actually learning about how to make these expressions shorter. The clue to this is that on line 40 the expression
-11*(b=10)
can be written three bytes shorter as:
11AND10=b
or even shorter as:
11ANDLOGb
This stands out like a sore thumb to me now, which suggests that these techniques for compressing expressions were also being learnt.
The WAIT statements, which introduced a delay, have been replaced by a change to the use of INKEY on line 90. We now use INKEY6 instead of INKEY0. This changes the behaviour of the program slightly, as the delay in INKEY is only until a key is pressed. The brick only falls if no other key is pressed since it last fell. The delay introduced now has a maximum rather than just changing the action which occurs. The WAIT statements were introduced when the program was transferred to the Archimedes from the BBC. Running on the BBC, the program was not sufficiently fast to justify an extra delay.
10REMo=o+POINT(POS*32,1023-VPOS*32):IFo<0RETURNELSE 20MODE9:GCOL135:CLG 30REPEAT 40FORa=8TO31:b=1:FORe=5TO14:b=SGNPOINT(e*32,a*32)ANDb:NEXT:VDU28,5,31 -a,14;30,11*b:a-=b:NEXT 50VDU26 60b=RND(7):x=9:l=1:y=3:r=8:p$="" 70REPEATA=b!(b+TOP-17) 80g=INKEY6MOD9:k=y-l*(g=TRUE):j=x-(g=3)+(g=1):Q=r 90q$=CHR$31+CHR$j+CHR$k:PRINTp$q$; 100o=0:FORC=g=2TO15:Q=LOGQ+2EORQ:IFNOTSGNC a=Q*(1ANDA):VDUa:q$+=CHR$a: A=A>>1:q$+=" "+CHR$8:o+=POINT(POS*32,1023-VPOS*32):IF0ELSENEXT:IFo=0x=j: y=k:r=Q:p$=q$:IF0ELSECOLOURb+128:PRINTp$:COLOUR128:UNTILk>y:UNTILy=3 120REMADAH(4())DABE
Changes:
The line removal code (on line 40) has been shortened. The condition has been coded more compactly and now can be tested with b. The softness here was that the expression "b=10" occurred twice. [softness]
One of the savings here is that the adjustment of the loop variable:
a-=b
is 3 bytes shorter. Now, I don't remember being this hard of thinking, but this statement is rather unnecessary. What is lacking here is understanding. The purpose of the statement is to recheck the row if the board has been scrolled down. This is only necessary because we are scanning from the bottom to the top of the screen. Scanning top to bottom avoids this and can be achieved by replacing:
FORa=8TO31:...:a-=b:NEXT
with:
FORa=31TO8STEPTRUE:...:a-=b:NEXT
The decoding code is shorter as a result of changing the coding slightly. Each nybble codes a motion. By selecting which of the four directions to move in, it is thus possible to end up at any of the 8 surrounding squares. Previously a square was drawn after decoding each nybble, now a square is drawn after decoding each bit. This tests and draws squares multiple times, but this is harmless as long as the motion during a nybble never moves outside the brick.
The main saving comes from avoiding conditioning on the bit position. The coding has also been shortened one byte by using the newline character at the end of the line as part of the coding.
It's not obvious why:
A=A/2
has been replaced by the longer code:
A=A>>1
The earlier code fails to work if the byte above the end of the program has the top bit set. When this happens, brick 7 makes A a negative number the rules for implicit conversion to an integer mess up the testing of the bottom bit.
This version no longer exists. Probably because it was identical to some other version or considered similar enough not to be worth keeping.
10REMo=o+POINT(POS*32,1023-VPOS*32):IFo<0RETURNELSE 20MODE9:GCOL135:CLG 30REPEAT 40FORa=8TO31:b=1:FORe=5TO14:b=SGNPOINT(e*32,a*32)ANDb:NEXT:VDU28,5,31 -a,14;30,11*b:a-=b:NEXT 50VDU26 60b=RND(7):x=9:l=1:y=3:r=8:p$="" 70REPEATA=b!(b+TOP-17) 80g=INKEY6MOD9:k=y-l*(g=TRUE):j=x-(g=3)+(g=1):Q=r 90q$=CHR$31+CHR$j+CHR$k:PRINTp$q$; 100o=0:FORC=g=2TO15:Q=Q EOR3-SINQ:IFC>TRUE a=Q*(1ANDA):VDUa:A=A>>1:q$+ =CHR$a+" "+CHR$8:o+=POINT(POS*32,1023-VPOS*32):IF0ELSENEXT:IFo=0x=j:y=k: r=Q:p$=q$:IF0ELSECOLOURb+128:PRINTp$:COLOUR128:UNTILk>y:UNTILy=3 110REMADAH(4())DABE
Changes:
Bizarrely the expression to rotate Q (line 100) has changed to be one byte longer, although only by a space, which may get removed. I think that is a result of too much concentration on that particular expression.
The condition NOTSGNC has changed to one of equivalent length (and function). It appears the point of this change is to add clarity. It seems a bit surprising that you might write C>TRUE for the sake of clarity.
But no, looking more closely, it seems the intention is to remove the spaces. This is an indication of how hard the changes are becoming, that we are considering more desperate methods. The clue is in the change of the expression. It now ends with a token TRUE, instead of the variable C. The space after the TRUE can be removed, and the interpreter will think the expression ends, as it does at TRUE. Without that the interpreter will read the a from the next statement as part of a variable Ca.
NOTSGNCa=Q*(1ANDA)
would be interpreted as a single expression involving the variable Ca.
There is a comment which contains an idea to use a subroutine. The idea of being able to have a subroutine in a one line program was new to us, and we hadn't completely sussed how to do it at this point.
10REMo=o+POINT(POS*32,1023-VPOS*32):IFo<0RETURNELSE 20MODE9:OFF:GCOL135:CLG 30REPEAT 40FORa=8TO31:b=1:FORe=5TO14:b=SGNPOINT(e*32,a*32)ANDb:NEXT:VDU28,5,31 -a,14;30,11*b:a-=b:NEXT 50b=RND(7):x=9:y=3:r=8:p$="" 60REPEATA=b!(b+TOP-17) 70g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1:Q=r 80q$=CHR$31+CHR$j+CHR$k:PRINTCHR$26p$q$; 90o=0:FORC=g=1TO15:Q=Q EOR3-SINQ:IFC>TRUE a=Q*(1ANDA):VDUa:A=A>>1:q$+ =CHR$a+" "+CHR$8:o+=POINT(POS*32,31-VPOS<<5):IF0ELSENEXT:IFo=0x=j:y=k:r= Q:p$=q$:IF0ELSECOLOURb+128:PRINTp$:COLOUR128:UNTILk>y:UNTILy=3 100REMADAH(4())DABE
Changes:
The major change here is that the action encoding has changed. We take the ASCII value of the key read MOD 5 instead of MOD 9. This makes some expressions shorter. In particular:
j=x+ABSg-1
is 7 bytes shorter than
j=x-(g=3)+(g=1)
OK, so it now means that there are keys for moving the brick two and three spaces to the right. But it's a seven byte saving!
Here we have an arbitrary choice of encoding and we make a choice that coincides with something more convenient to expressions that use it. We achieve a reduction by trading choice for coincidence.
10z=z:IFz PRINTCHR$26p$q$;:A=b!(b+TOP-17):o=0:FORC=g=1TO15:Q=Q EOR3-S INQ:IFC>TRUE a=Q*(1ANDA):VDUa:A=A>>1:q$+=CHR$a+" "+CHR$8:o+=SGNPOINT(POS *32,31-VPOS<<5):IF0ELSEIFz NEXT:RETURN:ELSE 20MODE9:OFF:GCOL135:CLG 30REPEAT 40FORa=8TO31:b=1:FORe=5TO14:b=SGNPOINT(e*32,a*32)ANDb:NEXT:VDU28,5,31 -a,14;30,11*b:a-=b:NEXT 50b=RND(7):x=9:y=3:r=8:p$="" 60REPEATA=b!(b+TOP-17) 70g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1:Q=r 80q$=CHR$31+CHR$j+CHR$k:PRINTCHR$26p$q$; 90o=0:FORC=g=1TO15:Q=Q EOR3-SINQ:IFC>TRUE a=Q*(1ANDA):VDUa:A=A>>1:q$+ =CHR$a+" "+CHR$8:o+=POINT(POS*32,31-VPOS<<5):IF0ELSENEXT:IFo=0x=j:y=k:r= Q:p$=q$:IF0ELSECOLOURb+128:PRINTp$:COLOUR128:UNTILk>y:UNTILy=3 100REMADAH(4())DABE
Changes:
The only change here is an experimental subroutine mechanism. The subroutine is not used, but is there to demonstrate the possibility. The subroutine has a condition in it, and to cope with that the construct:
IF0ELSE
is replaced by:
IF0ELSEIFz
This trick of including conditions inside subroutines, was not used in the final version.
Version 9 is identical to version 6.
10REMo=o+POINT(POS*32,1023-VPOS*32):IFo<0RETURNELSE 20MODE9:GCOL135:CLG:REMOFF 30REPEAT 40FORa=8TO31:b=1:FORe=5TO14:b=SGNPOINT(e*32,a*32)ANDb:NEXT:VDU28,5,31 -a,14;30,11*b:a-=b:NEXT 50b=Z%:Z%+=1:x=9:y=3:r=8:b$="":t$=CHR$31+CHR$9+CHR$3: 60REPEAT 70g=INKEY6MOD5:q$=b$:k=y-(g=TRUE):j=x+ABSg-1:Q=r 80A=b?(TOP-10)*16+4-2*(g=1):PRINTCHR$26p$q$; 90o=0:REPEATQ=((Q ANDA)DIV2EORA)AND3EORQ:VDUQ:A=A/4:q$+=CHR$Q+" "+CHR $8:o+=POINT(POS*32,31-VPOS<<5):UNTILA<1:IFo=0x=j:y=k:r=Q:t$=t$+CHR$ASC:I F0ELSECOLOURb+128:PRINTp$:COLOUR128:UNTILk>y:UNTILy=3 100REMADAH(4())DABE 110^P^^w^OCOUNT^[CLEAR
Changes:
The code on line 50 using Z% is test code.
The code here isn't finished it looks like a beginning of an experiment with encoding the bricks in 7 bytes.
The encoding is to store the direction to move in in two bits, and move in four directions to draw the bricks. That's not quite enough, because you need to back up on yourself to draw the T tetromino. There are 4 bonus bits added to the encoding. 2 are just to give an extra movement. The other two are to provide an extra rotate when the action is rotate (g=1).
One simple change that has crept in here is the y-argument to POINT() is one byte shorter. A different pixel within the square is tested.
10MODE9:GCOL135:CLG:REMOFF 20REPEAT 30VDU26:REPEATVDU13,10:q$="":A=2^28:Q=9: 40o=0:REPEATQ=((Q ANDA)DIV2EORA)AND3EORQ:VDUQ:A=A/4:q$+=CHR$Q+" "+CHR $8:o+=SGNPOINT(POS*32,31-VPOS<<5):UNTILA<1 50IFo=15VDU28,5,VPOS,14;30,11,26:IF0ELSEUNTILVPOS=25 70b=RND(7)-10:x=9:y=3:r=8:p$="" 80REPEAT 90g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1:Q=r 100q$=CHR$31+CHR$j+CHR$k:PRINTCHR$26p$q$;:A=b?TOP*16+4-2*(g=1) 110o=0:REPEATQ=((Q ANDA)DIV2EORA)AND3EORQ:VDUQ:A=A/4:q$+=CHR$Q+CHR$32+ CHR$8:o+=SGNPOINT(POS*32,31-VPOS<<5):UNTILA<1:IFo=0x=j:y=k:r=Q:p$=q$:IF0 ELSECOLOURb+138:PRINTp$:COLOUR128:UNTILk>y:UNTILy=3 140^P^^w^OCOUNT^[CLEAR
Changes:
This is an attempt to share scanning and test code. The obvious intention is to put the common code inside a subroutine. The cunning thing here is that the encoding has enough flexibility to encode the motion right across a row as well as tracing out the path of all the bricks.
The brick number now has 10 subtracted from it to avoid the brackets in the code:
A=b?TOP*16+4-2*(g=1)
0x=x:IFx o=0:REPEATQ=((Q ANDA)DIV2EORA)AND3EORQ:VDUQ:A=A/4:q$+=CHR$Q +" "+CHR$8:o+=SGNPOINT(POS*32,31-VPOS<<5):UNTILA<1:RETURN ELSEMODE9:GCOL 135:CLG:REMOFF 10REPEAT:x=9:y=3 20VDU26:REPEATVDU13,10:q$="":A=2^28:Q=9: 30GOSUBFALSE 40IFo=15VDU28,5,VPOS,14;30,11,26:IF0ELSEUNTILVPOS=25 50b=RND(7)-10:r=8:p$="" 60REPEAT 70g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1:Q=r 80q$=CHR$31+CHR$j+CHR$k:PRINTCHR$26p$q$;:A=b?TOP*16+4-2*(g=1) 90GOSUBFALSE:IFo=0x=j:y=k:r=Q:p$=q$:IF0ELSECOLOURb+138:PRINTp$:COLOUR 128:UNTILk>y:UNTILy=3 120^P^^w^OCOUNT^[CLEAR
Changes:
Here we see that the common code has been put in a subroutine. The variable x is used to mark whether the program has started. For this to work its initialization is moved earlier.
0y=y:IFy o=0:REPEATQ=((Q ANDA)DIV2EORA)AND3EORQ:VDUQ:A=A/4:q$+=CHR$Q +" "+CHR$8:o+=SGNPOINT(POS*32,31-VPOS<<5):UNTILA<1:RETURN ELSEMODE9:GCOL 135:CLG:REMOFF 10REPEATx=9:y=3 20VDU26:REPEATPRINT:q$="":A=2^28:Q=9: 30GOSUBFALSE 40IFo=15VDU28,5,VPOS,14;30,11,26:IF0ELSEUNTILVPOS=25 50b=RND(7)-10:r=8:p$="" 60REPEAT 70g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1:Q=r 80q$=CHR$31+CHR$j+CHR$k:PRINTp$q$;:A=b?TOP*16+4-2*(g=1) 90GOSUBFALSE:IFo=0x=j:y=k:r=Q:p$=q$:IF0ELSECOLOURb+138:PRINTp$:COLOUR 128:UNTILk>y:UNTILy=3 120^P^^w^OCOUNT^[CLEAR
Changes:
The changes here are to prepare for eliminating x. The hope is that x can be stored in p$ or something similar.
0s$="SGNPOINT(32*POS,31-VPOS<<5)":MODE9:GCOL135:CLG:REMOFF 10REPEATx=9:y=3 12VDU30:REPEATVDU9,-2573*NOT-EVALs$;:IFPOS=15VDU28,5,VPOS,14;30,11,26 :IF0ELSEUNTILVPOS=25 50b=RND(7)-10:r=8:p$="" 60REPEAT 70g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1:Q=r 80q$=CHR$31+CHR$j+CHR$k:PRINTp$q$;:A=b?TOP*16+4-2*(g=1) 90o=0:REPEATQ=((Q ANDA)DIV2EORA)AND3EORQ:VDUQ:A=A/4:q$+=CHR$Q+" "+CHR $8:o+=EVALs$:UNTILA<1:IFo=0x=j:y=k:r=Q:p$=q$:IF0ELSECOLOURb+138:PRINTp$: COLOUR128:UNTILk>y:UNTILy=3 120^P^^w^OCOUNT^[CLEAR
Changes:
We have replaced the subroutine with a much smaller amount of shared code that can be stored in a string.
We have also backed away from a failed experiment. The experiment was to code the scanning of a row in the same way as a brick. Here we share much less code but to a greater advantage.
We are using a string to store the shared code. BASIC has a feature to allow evaluation of an expression using the EVAL keyword.
0x=x:IFx p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CLG:REM OFF 10REPEATx=9:y=3 12VDU30:REPEATGOSUBFALSE:VDU2573ANDp<1;9:IFPOS=15VDU28,5,VPOS,14;2846 ;26:IF0ELSEUNTILVPOS=25 50b=RND(7)-10:r=8:p$="" 60REPEAT 70g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1:Q=r 80q$=CHR$31+CHR$j+CHR$k:PRINTp$q$;:A=b?TOP*16+4-2*(g=1) 90o=0:REPEATQ=((Q ANDA)DIV2EORA)AND3EORQ:VDUQ:A=A/4:q$+=CHR$Q+CHR$32+ CHR$8:GOSUBFALSE:o+=p:UNTILA<1:IFo=0x=j:y=k:r=Q:p$=q$:IF0ELSECOLOURb+138 :PRINTp$:COLOUR128:UNTILk>y:UNTILy=3 120^P^^w^OCOUNT^[CLEAR
Changes:
Now we undo another failed experiment. This time it is the use of a string to store the common code. We revert to the old subroutine mechanism here. Storing the code as a string is much longer than as part of the program text because it is not tokenized. So for example "VPOS" takes up four bytes instead of one.
0x=x:IFx p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CLG:REM OFF 10REPEATx=9:y=3 20VDU30:REPEATVDU9:GOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF0ELSE IFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 30b=RND(7)-10:r=8:p$="" 40REPEAT 50g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1:Q=r 60q$=CHR$31+CHR$j+CHR$k:PRINTp$q$;:A=b?TOP*16+4OR2ANDg=1 70o=0:REPEATQ=((Q ANDA)DIV2EORA)AND3EORQ:A=A/4:q$+=CHR$Q+CHR$32+CHR$8 :VDUQ:GOSUBFALSE:o+=p:UNTILA<1:IFo=0x=j:y=k:r=Q:p$=q$:IF0ELSECOLOURb+138 :PRINTp$:COLOUR128:UNTILk>y:UNTILy=3 110^P^^w^OCOUNT^[CLEAR
Changes:
The conditional code:
VDU2573ANDp<1
which conditionally prints a carriage return and cursor down, has been replaced by the shorter code which PRINTs conditionally. I think we had in mind here that the sense of the condition could be inverted to save two bytes.
The whole purpose of writing:
UNTIL0ELSE
instead of:
IF0ELSE
is to make this improvement. I think perhaps we were scared off by the implausibility of the code that results when you make this change:
IFp UNTIL0ELSEPRINT:UNTILVPOS=25
This looks wrong because it is not clear how the code will ever terminate when the screen starts completely white and every test will make p non-zero. But of course it does work, it's just a little confusing.
0x=x:IFx p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CLG:REM OFF 10REPEATx=9:y=3 20VDU30:REPEATVDU9:GOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF0ELSE IFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 30b=RND(7)-1:r=&C2590EC>>b*3AND975:p$="" 40REPEAT 50g=INKEY6MOD5:k=y-(g=TRUE):j=x+ABSg-1 60q$=CHR$31+CHR$j+CHR$k:PRINTp$q$;:Q=r:IFg=1Q=r>>>9ORr<<3:IF0ELSE 70o=0:d=9:FORf=0TO11:d=f DIV3OR2EORd:e=1ANDQ>>f:q$+=CHR$d+CHR$(9+23*e )+CHR$8:VDUd:GOSUBFALSE:o+=e*p:NEXT:IFo=0x=j:y=k:r=Q:p$=q$:IF0ELSECOLOUR b+129:PRINTp$:COLOUR128:UNTILk>y:UNTILy=3
Changes:
To reach our final goal, of a one line Tetris like program, we sought the holy grail of our quest: a four byte brick encoding.
As there are 7 bricks there are 12 * 7 = 84 bits to store. That won't fit in a single constant that is limited to 32 bits. We only have 4 4/7 bits per brick. If we make the last few bits of each brick, be the same as the first few bits of the next, we can overlap the bit patterns.
A better target is 12 bits for the first brick, and 32-12 = 20 for each of the 6 subsequent bricks. That leaves us 3 1/3 bits for each brick, or practically speaking, three. So overlapping the representations by 9 bits would work compactly. To do this we specify the brick in 28 bits. That's 4 bits per brick.
The actual expression used uses an extra mask to pick out only 8 of the twelve bits for use. Four of the bits are shared with only one other brick, the other four bits are shared by up to 3 other bricks.
There are two extra constraints here which we manage to apply. We want the pieces to rotate about a sensible place. For example the O tetromino, which is a 2x2 square, should rotate about its centre. The L tetromino should rotate about its inside corner. The second constraint is that we want the I tetromino to be red. Red is colour 1, so this needs to be brick 0. (We calculate the colour number from the brick number by adding 1.) We found only two solutions to encoding the bricks in this way; both are bit reversals of the other. Fortunately the I tetromino has to be piece 0 or 6, allowing us to make it red.
This code shows on line 30 our holy grail. The brick data can now be written as a single constant instead of the ugly peeking around in the program. The program can be seen when LISTed.
On line 60, we can see the rotation of the brick pattern is done by rotating the bits in r. The >>> operator performs a logical as opposed to >> which performs an arithmetic shift.
On line 70, the rather complex update of Q has been replaced by a more compact update of d. The display of the brick is now conditional but the path of the cursor follows the flower pattern.
The variables to store the cursor position have not been eliminated yet, but this is the intention.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CL G:REMOFF 10d=9:REPEAT 20VDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF0ELSEIFp=0 PRINT:UNTIL0ELSEUNTILVPOS=25 30b=RND(7)-1:r=&C2590EC>>b*3AND975:PRINTTAB(9,3);:p$="" 40REPEAT 50g=INKEY6MOD5:m=-10*(g=TRUE)-8*(g=0)-9*(g=2) 60q$="":PRINTp$;:COLOUR129+b:VDUm:Q=r:IFg=1Q=r>>>9ORr<<3:IF0ELSE 70o=0:FORf=0TO11:d=f/3OR2EORd:e=1ANDQ>>f:q$+=CHR$d+CHR$(9+23*e)+CHR$8 :GOSUBFALSE:o+=e*p:NEXT:m=m*SGNo:VDUSGNm EORm:IFo=0r=Q:p$=q$:IF0ELSEPRIN Tp$;:COLOUR128:UNTILg=TRUE ANDo:UNTILVPOS=3
Changes:
At last, we've manage to get rid of the position variables. This is done by storing the brick position in the text cursor. The flower pattern has been designed to finish where it starts, to make this possible.
The only complication is that in order to test the new position the cursor must be moved. If the brick won't fit in the new position it must be moved back. This was previously done by using the position variables, but now we have to do it explicitly. This motion code logically goes in the ELSE branch of the IFo=0 condition, but we can't use ELSE that easily in a one line program. And instead is rather more complicated:
m=m*SGNo:VDUSGNm EORm
I think there is a hope that the conditional assignment can be avoided.
We are at last beginning to see some of the code in the core of the brick loop that ends in the final version.
The subroutine has also been extended to include a VDU statement. This saves one more byte than it might at first seem. The space in:
IFd VDUd:p=...
can be removed after tokenization without changing the meaning. Previously the space was necessary; removing it from the following code:
IFd p=...
changes the sense: the variable d now reads as dp.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CL G:REMOFF 10d=9:REPEAT 20VDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF0ELSEIFp=0 PRINT:UNTIL0ELSEUNTILVPOS=25 30b=RND(7)-1:r=&C2590EC>>b*3AND975:PRINTTAB(9,3);:p$="" 40REPEAT 50g=9-INKEY6MOD3 60q$="":PRINTp$;:COLOUR129+b:VDUg:Q=r:IFg=7Q=r>>>9ORr<<3:IF0ELSE 70o=0:FORf=0TO11:d=f/3OR2EORd:e=1ANDQ>>f:q$+=CHR$d+CHR$(9+23*e)+CHR$8 :GOSUBFALSE:o+=e*p:NEXT:g=g*SGNo:VDUSGNg EORg:IFo=0r=Q:p$=q$:IF0ELSEPRIN Tp$;:COLOUR128:UNTILg=10ANDo:UNTILVPOS=3
Changes:
On line 50, we have replaced the action calculation by the much more compact:
9-INKEY6MOD3
eliminating the separate calculation of this into m. This is one of the most serendipitous parts of the program: we can use the four values that this expression gives in a most convenient way.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CL G:REMOFF 10d=9:REPEAT 20VDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF0ELSEIFp=0 PRINT:UNTIL0ELSEUNTILVPOS=25 30b=RND(7)-1:r=&C2590EC>>b*3AND975:PRINTTAB(9,3);:p$="" 40REPEAT 50g=9-INKEY6MOD3 60q$="":PRINTp$;:COLOUR129+b:VDUg:Q=g=7AND3:Q=r>>>Q*3ORr<<Q 70o=0:FORf=0TO11:d=f/3OR2EORd:e=1ANDQ>>f:q$+=CHR$d+CHR$(9+23*e)+CHR$8 :GOSUBFALSE:o+=e*p:NEXT:IFo=0r=Q:p$=q$:VDUg:IF0ELSEVDU1EORg:PRINTp$;:COL OUR128:UNTILg>9ANDo:UNTIL0
Changes:
The conditional rotation of the brick on line 60 has been turned into a couple of expressions replacing the previous IF .. IF0ELSE block. Unfortunately the code is now longer.
The conditional return motion is now being performed by undoing the motion unconditionally on line 70:
VDU1EORg
and earlier conditionally doing the motion again after the IFo=0 test. This is quite cunning as it avoids re-testing the condition in the opposite sense to achieve the effect.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CL G:REMOFF 10d=9:REPEAT 20VDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF0ELSEIFp=0 PRINT:UNTIL0ELSEUNTILVPOS=25 30b=ABSRND MOD7:r=&C2590EC>>b*3AND975:PRINTTAB(9,3);:p$="" 40REPEAT 50g=9-INKEY6MOD3 60q$="":PRINTp$;:COLOUR129+b:VDUg:Q=g=7AND3:Q=r>>>Q*3ORr<<Q 70o=0:FORf=0TO11:d=f/3OR2EORd:e=1ANDQ>>f:q$+=CHR$d+RIGHT$(CHR$32+CHR$ 8,DEGe):GOSUBFALSE:o+=e*p:NEXT:IFo=0r=Q:p$=q$:VDUg:IF0ELSEVDU1EORg:PRINT p$CHR$20;:UNTILg>9ANDo:UNTIL0
Changes:
This versions has a couple of minor expression changes. One of them is the highly elegant:
CHR$d+RIGHT$(CHR$32+CHR$8,DEGe)
which is a whole byte shorter than the previous expression. This uses DEGe to provide a value of either 0 when e is 0 and at least 2, when e is 1. The RIGHT$() built-in then returns either the empty string or all its first argument as appropriate.
The other expression on line 30 is now two bytes shorter (with the space is removed):
ABSRND MOD7
However, this is at the expensive of correctness. The ABS function will return a negative number when its argument is -2147483648 (&80000000), the smallest representable integer in 32 bit two's complement. The problem being that 214783648, isn't representable as an integer. The MOD operator also returns a negative number in this case. The rest of the code can't cope. [see bugs]
The COLOUR statement to reset the background colour has been replaced by the shorter:
CHR$20
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CL G:REMOFF 10d=9:REPEAT 20VDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF0ELSEIFp=0 PRINT:UNTIL0ELSEUNTILVPOS=25 30b=ABSRND MOD7:r=&C2590EC>>b*3AND975:PRINTTAB(9,3);:p$="" 40REPEAT 50g=9-INKEY6MOD3 60q$="":PRINTp$;:COLOURb-15:VDUg:Q=g=7AND3:Q=r>>>Q*3ORr<<Q 70o=0:FORf=0TO11:d=f/3OR2EORd:e=1ANDQ>>f:q$+=CHR$d+RIGHT$(CHR$32+CHR$ 8,DEGe):GOSUBFALSE:o+=e*p:NEXT:IFo=0r=Q:p$=q$:VDUg:IF0ELSEVDU1EORg:PRINT p$CHR$20;:UNTILg>9ANDo:UNTIL0
Changes:
The single change here is the shorter argument to COLOUR. It's now
b-15
instead of:
129+b
This makes use of the fact that most bits in the argument are ignored.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL135:CL G:REMOFF 10d=9:REPEAT 20VDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF0ELSEIFp=0 PRINT:UNTIL0ELSEUNTILVPOS=25 30b=ABSRND MOD7:r=&C2590EC>>b*3AND975:PRINTTAB(9,3);:p$="" 40REPEAT 50g=9-INKEY6MOD3 60q$="":PRINTp$;:COLOURb-15:VDUg:Q=g=7AND3:Q=r>>>Q*3ORr<<Q 70o=0:FORf=0TO11:d=f/3OR2EORd:e=1ANDQ>>f:q$+=CHR$d+RIGHT$(CHR$32+CHR$ 8,DEGe):GOSUBFALSE:o-=e*p:NEXT:IFEXPo r=Q:p$=q$:VDUg:IF0ELSEVDU1EORg:PRI NTp$CHR$20;:UNTILg>9ANDo:UNTIL0
Changes:
We're on the hunt for shorter expressions. Things have got very tight now, and its harder to see where savings can be made.
We're even considering putting the VDU commands in as PRINT""; strings where this is shorter. This is ugly though, and it will make the program unlistable.
The one byte improvement here is the subtraction from o instead of the addition. Now the test to see if o=0 becomes:
EXPo
which will be between 0 and 1, rounding down to 0, if o is negative. Lovely.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURNELSEMODE9:GCOL-9:CLG 1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF 0ELSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 2b=ABSRND MOD7:r=&C2590EC>>b*3AND975:PRINTTAB(9,3); 3REPEATg=9-INKEY6MOD3:S=r 4FORl=TRUE TO1:FORf=0TO11:d=f/3OR2EORd:GOSUBFALSE 5IFr>>f AND1VDU2080*ABSl;:o=o-p:IF0ELSENEXT 70IFl<0COLOURb-15:VDUg:Q=g=7AND3:r=r>>>Q*3ORr<<Q:o=0:IF0ELSE 80IFo ANDl=0r=S:VDU1EORg:IF0ELSENEXT 90VDU20:UNTILg>9ANDo:UNTIL0
Changes:
The major advance here is the removal of the strings. The strings stored the VDU commands to draw the bricks. The test/draw code is now executed in three phases, to avoid storing the sequence of actions we take.
The code that updated the brick state to the new one:
r=Q:p$=q$:VDUg
is replaced by code that conditionally undoes the state on line 80. This saves the doing and then undoing of the action, making that previous improvement irrelevant.
One thing to notice here is that we reset the variable o to zero only at the end of the undraw phase. The result of the test has to persist from the end of the test throughout the draw phase. Fortunately, the brick is always drawn over blank space, so there will be no change to the variable o in the draw phase.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL-9:CLG 1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF 0ELSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 2b=ABSRND MOD7:k=0:PRINTTAB(9,3); 3REPEATg=9-INKEY6MOD3 4FORl=TRUE TO1:FORf=0TO11:d=f/3OR2EORd:GOSUBFALSE 6IF(&C2590EC>>b*3AND975)>>(f+9*k)MOD12AND1VDU2080*ABSl;:o=o-p:IF0ELS ENEXT 70IFl<0COLOURb-15:VDUg:k-=g=7:o=0 71IF0ELSEIFo ANDl=0k+=g=7:VDU1EORg:IF0ELSENEXT 90VDU20:UNTILg>9ANDo:UNTIL0
Changes:
We are now recalculating and the original brick pattern every time round the flower loop instead of storing it. Instead of storing the orientation we increment and decrement a variable, and use that to index into the pattern. Here the main benefit is that we don't have to do the complicated job of rotating a bit pattern.
All this extra calculation makes the program much slower, but still quite playable on an Archimedes.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL-9:CLG :OFF 1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=16VDU28,5,VPOS,14;2846;26:IF 0ELSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 2b=ABSRND MOD7:k=0:PRINTTAB(9,3); 3REPEATg=9-INKEY6MOD3 4FORl=TRUE TO1:FORf=0TO11:d=f/3OR2EORd:GOSUBFALSE 6IF1<<(f+9*k)MOD12AND975AND&C2590EC/8^b VDU2080*ABSl;:o=o-p:IF0ELSEN EXT 70IFl<0COLOURb-15:VDUg:k-=g=7:o=0 71IF0ELSEIFo ANDl=0k+=g=7:VDU1EORg:IF0ELSENEXT 90VDU20:UNTILg>9ANDo:UNTIL0
Changes:
The added statement:
OFF
turns the graphics cursor off. This addition is a sign of confidence that this code will squash into one line.
The action condition is looking very like the final action condition that we use.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL-9:CLG :OFF 1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=28VDUPOS,5,VPOS,14;2846;26:I F0ELSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 2b=ABSRND MOD7:k=0:VDU31,9,3 3REPEATg=9-INKEY6MOD3 4FORl=TRUE TO1:o=o*ABSl 5IFl=SGNo COLOURb-15:VDUl EORg:k+=g=7ANDDEGSGNTANEXPl 6IF0ELSEFORf=0TO11:d=f/3OR2EORd:GOSUBFALSE 7IF1<<(f+k)MOD12AND975AND&C2590EC/8^b VDU2080*ABSl;:o+=p:IF0ELSENEXT , 8VDU20:UNTILg>9ANDo:UNTIL0
Changes:
The code on line 5 is the result of sharing code from lines 70 and 71 in the previous version. The technique used here is more than just sharing. We have modified two similar pieces of code to make them the same, and then shared the result.
There is a minor change on line 2. The use of VDU31 instead of PRINTTAB( is actually the same length, but there is more potential for compressing the VDU statement.
The value of k now stores the rotation in increments of 3. We use the flexibility that this only needs to be right MOD 12 to come up with the concoction:
k+=g=7ANDDEGSGNTANEXPl
If the action is rotate (g=7), this rather marvellous expression, adds 57 to k if l is 0 and -57 if l is 1.
The statement:
VDUl EORg
serves to both do and undo the motion of the brick.
These two statements together perform the motion code, that both does and undoes the motion of the brick to its new position and orientation.
One byte has been saved in the clearing of a row. The POS=16 has become POS=28, so that POS can replace 28 as the first argument of the VDU command.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN:ELSEMODE9:GCOL-9:CLG :OFF 1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=28VDUPOS,5,VPOS,14;11,26:IF0 ELSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 2b=ABSRND MOD7:k=0:VDU31,9,3 3REPEATg=9-INKEY6MOD3 4FORl=TRUE TO1:o=o*ABSl 5IFl=SGNo COLOURb-15:VDUl EORg:k+=g=7AND9-6*l 6IF0ELSEFORf=0TO11:d=f/3OR2EORd:GOSUBFALSE 7IF1<<(f+k)MOD12AND975AND&C2590EC/8^b VDU2080*ABSl;:o+=p:IF0ELSENEXT , 8VDU20:UNTILg>9ANDo:UNTIL0
Changes:
On line 1, the "2846;" has been replaced by "11," in the VDU statement. The original expression was equivalent to "30,11". VDU 30 homes the cursor to the top left of the window. This is unnecessary however, as the cursor is homed as a side effect of setting the text window.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN ELSEMODE9:GCOL-9:CLG :OFF 1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=28VDUPOS,15,VPOS,24;11,26:IF 0ELSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 2b=ABSRND MOD7:k=0:VDU4895;3 3REPEATg=9-INKEY6MOD3 4FORl=TRUE TO1:o=l ANDSGNo 5IFo=l COLOURb-15:VDUl EORg:k+=g=7AND9-6*l 6IF0ELSEFORf=0TO11:d=f/3OR2EORd:GOSUBFALSE 7IF1<<(f+k)MOD12AND975AND&C2590EC/8^b VDU2080*ABSl;:o+=p:IF0ELSENEXT , 8VDU20:UNTILo*LOGg:UNTILVPOS=3
Changes:
Now all the major changes have been made to the code. The lines have to be joined and spaces removed.
Changes on line 1 and 2 position the board in the centre of the screen. It now spans columns 15 to 24. The statement:
VDU4895;3
on line 2 is a shortened form of:
VDU31,19,3
which initializes the position of the brick on the screen.
The use of the variable o has evolved a bit more. It's use has become quite complicated involving interactions between the different phases and reliance on properties of the interaction between the board state and the brick drawing.
0d=d:IFd VDUd:p=POINT(32*POS,31-VPOS<<5):RETURN ELSEMODE9:GCOL-9:CLG :OFF 1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=28VDUPOS,15,VPOS,24;11,26:IF 0ELSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 2b=ABSRND MOD7:k=0:VDU4895;3 3REPEATg=9-INKEY6MOD3 4FORl=TRUE TO1:o=l ANDSGNo 5IFo=l COLOUR-b-9:VDUl EORg:k+=g=7AND9-6*l 6IF0ELSEFORf=0TO11:d=f/3OR2EORd:GOSUBFALSE 7IF1<<(f+k)MOD12AND975AND&3709A43/8^b VDU2080*ABSl;:o+=p:IF0ELSENEXT , 8VDU20:UNTILo*LOGg:UNTILVPOS=3
Changes:
The change here is in the numbering of the bricks. The brick pattern constant has been reversed, so now brick 6 is the I tetromino. The COLOUR argument on line 5 has to change to ensure that the I tetromino is still red.
This is an experiment to see if this change could make the code shorter. The resulting changes are not used however.
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
Changes:
Our one line dream has been achieved. We even have space to keep the OFF statement; a cosmetic nicety.
There is no room to write comments, so our names are encoded in the filename and variables. The variable names are made out of letters of our first names. The filename is made from letters of our surnames.
More effort did go in to reduce the size more to allow more features, or make the code cleaner. However no further reduction was found until some time later.
0d=d:IFd VDUd:p=POINT(64*POS,1E3-VPOS*32):RETURN ELSEMODE2:GCOL0,-9: CLG 1d=9:REPEATVDU30:REPEATGOSUBFALSE:IFPOS=15VDU28,5,VPOS,14;11,26:IF0E LSEIFp=0PRINT:UNTIL0ELSEUNTILVPOS=25 2b=ABSRND MOD7:k=0:VDU31,9,3 3REPEATg=9-INKEY6MOD3 4FORl=TRUE TO1:o=l ANDSGNo 5IFo=l COLOURb-15:VDUl EORg:k=k+(g=7AND9-6*l) 6IF0ELSEFORf=0TO11:d=f/3OR2EORd:GOSUBFALSE 7IF2^((f+k)MOD12)AND975AND&C2590EC/8^b VDU2080*ABSl;:o=o+p:IF0ELSENE XT, 8VDU20:UNTILo*LOGg:UNTIL0
Changes:
This is an attempt to write a version of the program that will run on the BBC. Having cut down the program to just fit within one line, a purer aim would be to have a program that works on an original BBC micro and not just on the later machines.
The BBC BASIC II had no bit shift operators << nor >>, no +=, nor -=. There was no SYS statement, and no one argument version of GCOL. There is also no OFF statement, so this has to be omitted. There was no MODE9, so we make do with MODE2, where the characters are twice as wide, but still has enough colours.
As a result we have to introduce extra brackets in places, and there are a few bytes added all over the place. There are a few changes to do with the size of the screen being different.
POINT(32*POS,31-VPOS<<5)
becomes:
POINT(64*POS,1E3-VPOS*32)
1E3 being short for 1000. The VDU POS trick no longer works as the screen is now only 20 columns wide, so there is no column 28.
This code doesn't fit on one line, even with the last statement being changed as it is, so that the program doesn't detect the end of the game.
We gave up on a colour version on the program for the BBC. If we use MODE 1 instead, the program would probably fit.
0MODE9:OFF:GCOL-9:CLG:REPEATs=s+VPOS:PRINTCHR$30s: 1REPEATSYS6,135TOi,p,d:PRINTTAB(p=0)CHR$9;: 2IF POS=22 VDU3100;VPOS,21;6667;:UNTIL0 ELSE UNTILVPOS=25: 3v=ABSRND MOD7:VDU31:COLOUR3: 4REPEATm=9-INKEY(INKEYTRUE OR 6)MOD3: 5FORr=TRUE TO1:t=r ANDSGNt: 6IFt=r COLOURv-15:VDUr EORm:i+=m=7AND9-6*r:IF0ELSE 7FORn=0TO11:d=n DIV3OR2EORd:VDUd: 8IF1<<(n+i)MOD12AND&C2590EC DIV8^v AND975 t+=POINT(p*POS,31-VPOS<<5): 9IFr VDUp,8:IF0ELSE 10NEXT,:VDU20:UNTILt*LOGm:UNTILVPOS=3:Z
Changes:
This is Olly Betts' latest version of the program. It's so much compressed that he's added several new features.
The score feature is quite simple. It accumulates a score based on how far each brick drops (VPOS), and simply prints the total so far:
s=s+VPOS:PRINTCHR$30s
The code is merged with the homing of the cursor. A slightly tricky thing is happening here. The code doesn't check the top line for being full. However, the only time this can happen is during initialization. In this case the top row is cleared when the second row is cleared. The second row gets cleared again after acquiring the contents of the uncleared top row.
Instead of using the subroutine, this code uses:
SYS6,135TOi,p,d
This is a BASIC V feature that calls a system routine. Acorn hackers will know the routine being called is OS_Byte 135, which reads a character from under the cursor. The SYS command returns multiple values from registers. The statement is used here to initialize i and d as well as return the character in p.
It so happens that i gets the value 135, the number of the OS_Byte call passed. The variable d receives the value 9, the number of the current screen mode. This uses a lovely pair of coincidences. For this to work 135 must be a multiple of 3. It's also a great help that the call to read the character also happens to read the screen mode. The fact that the screen mode is 9 is really fortunate.
The code to clear the lines has changed slightly, although the basic algorithm is still the same. A really neat way of conditionally moving onto the next line has been found:
PRINTTAB(p=0)CHR$9;
This code uses the TAB() function which is for formatting code in columns. TAB(n) advances to column n by printing spaces. The BASIC interpreter keeps track of how many characters have been PRINTed onto the VDU stream since the last newline. If we are already past the column n, a new line and n spaces are PRINTed.
When used with a negative argument, no spaces are PRINTed. So when p=0 is TRUE (-1), the PRINTed string, CHR$9, steps the cursor one character to the right. When we meet a gap, the expression evaluates to zero, and we print TAB(0), which will take us to the beginning of the next line.
This code is a little sensitive to what the character set is. If there is a character which is an entirely filled block, then p will have a value of that character in the cases where we expect 0.
There is no optimization involving waiting for POS=28. A simple two-byte VDU argument is just as short.
The brick initialization is shorter. There is no need to initialize i, as this has been done by the SYS call. The start position of the brick is initialized by the compact:
VDU31:COLOUR3
The first statement puts the VDU 31 command on the VDU queue. The second statement puts the VDU command 17 and its argument 3, onto the same queue. In this situation, the bytes issued by the second statement are treated as parameters of the VDU 31 command. The net effect is to position the cursor at (17,3). This isn't quite in the centre of the screen, but it's close! The position of the board has been moved to compensate.
The code also demonstrates an excellent drop feature. This is coded in the key reading statement:
m=9-INKEY(INKEYTRUE OR6)MOD3
The argument to INKEY, previously "6" has been replaced by (INKEYTRUE OR 6). When INKEY takes a negative argument, the argument corresponds to a key number. -1 (TRUE) is the key code for a shift key. If either shift key is pressed the function will return TRUE (-1), zero otherwise.
When no shift key is pressed, the argument to the first INKEY becomes 6, the value used before the change. If a shift key is pressed, the argument becomes -1. In this case, the first INKEY will also return -1, resulting in a down action. If we're unlucky, the shift key is released between the two INKEY calls, and the resulting action will be 9 (right).
With the subroutine abolished, the POINT() code has been inlined. Here it has been made slightly shorter by using the variable p, which has a handy value of 32 (space) after the scanning loop.
The value of p is used again to draw a space. The improvement here uses a new construct.
The code on line 9 reads:
IFr VDUp,8
this replaces the clumsier:
VDU 2080*ABSr;
The observation here is that we can have IFs within IFs:
IF {cond a} {stmts X}:IF {cond B} {stmts Y}:IF0ELSE
If "cond a" is true, "stmts X" are executed, if "cond B" is also true then "stmts Y" are executed. After the IF0ELSE the code is executed unconditionally.
The final statement in the program:
Z
is not really a statement at all. It is detected as an error when executed, and an error message is printed. Normally the error would be "Mistake at line XXX", where XXX is the current line. However, BBC BASIC uses the value 0 to signify that statements are being executed directly on the command line, so the error printed is simply "Mistake", which is what must have occurred for the game to finish.
The idea of using this goes back to the 7-byte brick encoding. The idea was that instead of using a REM token, which introduces a comment, we simply execute the encoding, which causes the "Mistake" message to be printed. A way to avoid this message and the extra space would be simply writing "ELSE" instead of ":REM". Having the error message there however is wittier.
David Moore
$Id: history.html,v 1.16 2001/07/01 14:46:06 dsm Exp dsm $