Showing posts with label code. Show all posts
Showing posts with label code. Show all posts

Friday, June 17, 2022

Day 5/100 Sequence

 5/100 Sequence

Seq for short


Yes, I know that Linux has a utility called 'seq' that can be used to generate line numbers. The problem is that it doesn't accept file input, so that making up numbered lines is quite a hassle. I believe that I used awk, the last time I did it. I think it's overkill.


So, I went ahead and did it. Of course, it's supposed to be a short 1 hour project. Turns out I was tired or something, since it became a 2 hour project. It's the end of the day, and I was tired!


The hardest part turns out to be the one I thought would be the easiest: GetOpt(). It's certainly unique in the parameter settings.


Usage: seq [OPTION]... LAST
  or:  seq [OPTION]... FIRST LAST
  or:  seq [OPTION]... FIRST INCREMENT LAST


Not pretty. I used more variables than what I usually tolerate. At least it works fine. Then the usual testing that FIRST may be less than LAST, and that the sequence should work the other way. By the way, it's possible to set the INCREMENT wrong and have a runaway program!


The core of the program turns out to be short enough at only 15 lines. That includes stdin feature for an extra 6 lines of code with 2 of them curly braces.


Well, it's getting long on the tooth for the week. I should do something fun for the next project.


#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LINE 256
int First=1;
int Inc=1;
int Last=10;
int Curr=0;
int Dir=1;
int Debug=0;
char Liner[MAX_LINE];
char STR[256];
int  WDT=4;
void ShowMsg() {
puts("Usage: seq [OPTION]... LAST");
puts("  or:  seq [OPTION]... FIRST LAST");
puts("  or:  seq [OPTION]... FIRST INCREMENT LAST");
puts("Print numbers from FIRST to LAST, in steps of INCREMENT.");
puts("");
puts("  -sSTRING use STRING to separate numbers (default: \n)");
puts("  -wWIDTH  equalize width by padding with leading zeroes");
puts("  --help     display this help and exit");
puts("  --debug    set debug mode");
puts("");
puts("If FIRST or INCREMENT is omitted, it defaults to 1.  That is, an");
puts("omitted INCREMENT defaults to 1 even when LAST is smaller than FIRST.");
puts("The sequence of numbers ends when the sum of the current number and");
puts("INCREMENT would become greater than LAST.");
puts("INCREMENT is usually positive if FIRST is smaller than LAST, and");
puts("INCREMENT is usually negative if FIRST is greater than LAST.");
puts("If LAST is 0, stdin is read for lines until exhausted.");
}

int GetOpt(int argc, char *argv[]) {
int i;
int l=0,l1=0,l2=0,l3=0;
  for (i=1;i<argc;i++) {
    if (!strncmp("-s",argv[i],2))
      { strcpy(STR,argv[i]+2); continue; }
    if (!strncmp("-w",argv[i],2))
      { WDT=atoi(argv[i]+2); continue; }
    if (!strncmp("--help",argv[i],6))
      { ShowMsg(); return 1; }
    if (!strncmp("--debug",argv[i],7))
      { Debug=1; continue; }
    if (l==0) { l1=atoi(argv[i]); l++; continue; }
    if (l==1) { l2=atoi(argv[i]); l++; continue; }
    if (l==2) { l3=atoi(argv[i]); l++; continue; }
  }

  if (Debug) printf("l=%d\n",l);
  switch (l) {
    case 1: Last=l1; break;
    case 2: First=l1; Last=l2; break;
    case 3: First=l1; Inc=l2; Last=l3; break;
    default: return 1;
  }
  Curr=First; Dir=(First<Last)?1:-1;

  if (Debug) {
    printf("First=%d\n",First);
    printf("Inc=%d\n", Inc);
    printf("Last=%d\n", Last);
    printf("Dir=%d\n",Dir);
    printf("STR =%s=\n",STR);
    printf("WDT=%d\n",WDT);
  }
  return 0;
}


int Seq() {
  char *buf;
  if (Last==0) {
    while (buf=fgets(Liner,MAX_LINE,stdin)) {
      printf("%d %s",Curr,buf);
      Curr+=Inc;
    }
  }
  for (;Last!=0;Curr+=Inc) {
    if (Dir>0 && Curr>Last) break;
    if (Dir<0 && Curr<Last) break;
    printf("%s%d",(Curr==First)?"":STR,Curr);
  }
  putchar('\n'); return 0;
}
int main (int argc, char *argv[] ) {
  int e=0;
  strcpy(STR,"\n");
  if   (argc<2) { ShowMsg(); return 0; }
  e=GetOpt(argc, argv);
  if (!e) e=Seq();
  return e;
}


At the end of the day, I use the program a lot. Specifically, line numbering the programs I'm writing since I still don't know how to enable line numbering using nano!



Tuesday, June 14, 2022

Caesar Cipher and other stuff

Day 002: Caesar Cipher

How to implement Caesar cipher and other stuff.


There is an old programming exercise to implement a simple string transformation. Of course, for beginners, it's too hard to understand, so the programming task is dumbed down. The problem is, the usual solution are implemented using ASCII chart calculation. I don't know about you, but I think a simple character array implementation is much more suitable to beginners. 


And if you have been using Unix/Linux for a while, I'm sure you wonder just how difficult it is to implement some of the programs. Sure, the source code is out there, but how about a simple one for learning purposes? Therefore, I decided to just implement a simple cipher program. As a bonus, I decided to have a user configurable keys, so that the cipher isn't limited to A=3 as in traditional Caesar cipher.


The program will take 2 string parameters, called Set1 and Set2. This will determine the plain text, and the key. To use it as Caesar cipher, do it like this:


caesar abcdefghijklmnopqrstuvwxyz cdefghijklmnopqrstuvwxyzab


That's all there is to it. Speaking of which, there is another encoding technique that is quite standard, called ROT13. ROT13 is basically Caesar cipher shifted 13 places. You use it like this:


caesar abcdefghijklmnopqrstuvwxyz nopqrstuvwxyzabcdefghijklm


As you can see, by having both the plain text and the key be provided to the program, you can easily change the cipher without too much fuss!


Another way to use this program is to use it either to lower case, or to upper case. Like so:


caesar ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz

caesar abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ


Pretty neat! You can even reverse the alphabet for some unique writings:


caesar abcdefghijklmnopqrstuvwxyz zyxwvutsrqponmlkjihgfedcba


And if you mix the alphabet just so, you may even come up with some alien sounding words:


caesar abcdefghijklmnopqrstuvwxyz alienssoundingoratoryz


If you notice, the last example has a shortened key. That is intentional. You don't have to provide a direct match just to have fun! "Hello Sailor!" becomes "Hniio Sauiot!", it's just enough weirdness without being to alien to recognize.


And if you have been following the capabilities so far, I suggest that you take it to the next step and study a standard program called "tr", which is what this is all about.


#include <stdio.h>
#include <stdlib.h>
#include <string.h> 

int tr(char Set1[],char Set2[]) {
  int i,j,c; char *p;
  j=strlen(Set2);
  while ((c=getchar())!=EOF) {
    i=(p=strchr(Set1,c))-Set1;
    i=(i<j)?i:j-1;
    if (p) putchar(Set2[i]);
    else   putchar(c);
  }
  return 0;
} 

int main (int argc, char *argv[] ) {
  if   (argc<3) puts("Usage: tr SET1 SET2\n");
  else tr(argv[1],argv[2]);
  return 0;
}


One last thing: You can use this to delete characters from the text. Suppose you want to eliminate vowels from the text. You can do it this way: 


caesar aeiou ''

caesar aeiou '_' <test.txt | sed "s/_ */_/g" | sed "s/_//g"


The quotes specify empty string, and this will delete the characters no problem. Quite an interesting capability. Couple it with some judicious regex, and you have a nice vowel remover, indeed!


Friday, December 20, 2013

Petit Computer Journal #30


Programming Turtle Graphic

It somewhat annoys me when people are teaching turtle graphic as the basis of learning computer programming. It's not that turtle graphic is bad, as I do believe that the thought process does indeed teach programming. It is that the turtle is presented as a module. A black box, if you will.

I think it's more appropriate, especially for computer programming students to learn how simple and easy it can be to write their own turtle module. Therefore, I propose that instead of turtle graphic module, it should be a turtle graphic routines. Instead of a mysterious black box, it should be a transparent wrapper.

Done as a function call, especially interested students can take a peek into the function and see exactly how easy it can be to write a useful program such as turtle graphic.

In fact, I believe that it can be done in one hour. Turtle Graphic algorithm as an "Hour of Code" challenge? Bring it on!

First, what is involved in a turtle graphic program? Well, there's turtle and there's graphic. What are the commands necessary? Forward, Left, Right, and Pen. Each of those commands are followed by a number. We'll call that Number variable. Well, well. There are only 4 commands, are they? Would you believe that they are doable in about one hour? Why not?

If you are familiar with Object Oriented methodology, those are called "methods". What about variables? There's the position of the turtle, X0 and Y0. There's the destination of the turtle, X1 and Y1. There's Angle and Distance. And there's Pen Color. Is that 8 variables total? Are you saying coding these 8 variables into 4 commands is a great challenge? I don't think so!

Of course, we're skipping a lot of things. Namely, the LOGO language. LOGO is a programming language and if you are learning LOGO, you are learning computer programming language. The first turtle graphic and LOGO make effective combination. Most people incorrectly assume that turtle graphic must involve LOGO somewhat. Yet, Turtle graphic is just the graphical component of it. As for the language, we'll use Petit Computer Smile Basic.

That's all there is to it. So, let's set the timer and begin. 1 hour is all that takes. For simplicity, I will assume that commands are given in Strings and consist of 3 characters: A letter command, followed by 2 digit number. If the string has more than 3 characters, then the program will extract the first three characters only. If the string is less than 3 characters, it will be ignored. Also, Color 0 means Pen Up, which means moving the turtle without drawing the line. And perhaps, we should expand the language so we can do GPAINT command for colorful final image. With that in mind, let's begin!

In 15 minutes, I got the initial framework already.


  1. ACLS:CLEAR

  2. @INIT
  3. C$="":'COMMAND STRING
  4. N=0:'COMMAND PARAMETER
  5. X0=128:Y0=96:'TURTLE POSITION
  6. X1=0:Y1=0:'TURTLE DESTINATION
  7. A=0:'ANGLE
  8. D=0:'DISTANCE
  9. C=0:'PEN COLOR
  10. SPSET 1,139,0,0,0,0:SPOFS 1,X0,Y0

  11. @MAIN
  12. 'CLASSIC RECTANGLE
  13. C$="F35R90F35R90F35R90F35R90"
  14. GOSUB @TURTLE
  15. WAIT 300

  16. @END
  17. BEEP 6
  18. END

  19. @TURTLE
  20. IF LEN(C$)<3 THEN RETURN
  21. TC$=LEFT$(C$,3)
  22. N=VAL(RIGHT$(TC$,2))
  23. TC$=LEFT$(C$,1)
  24. C$=RIGHT$(C$,LEN(C$)-3)
  25. ?TC$;" ",N;"  ":WAIT 60

  26. GOTO @TURTLE


If you look at the code, there is nothing much there. It's simply the structure to pass on the command for the turtle. We can use IF-THEN ladder structure, or we can use ON-GOTO command structure. Depending on TC$, which contains the actual command. We can actually skip TC$, and specify LEFT$(C$,1) instead, but using that extra variable clarifies things for now.

I decided to do the easy thing first and implement Paint and Color command. Change C$ to "C01P00C02P00C03P00C04P00" for now. Also, we'll use ON-GOTO. This requires a numeric variable, so we'll do this


  1. TN=INSTR("FLRCP",TC$)
  2. ?TC$,TN,N:WAIT 60

  3. ON TN GOTO @TTF,@TTL,@TTR,@TTC,@TTP
  4. GOTO @TURTLE
  5. @TTF
  6. GOTO @TURTLE
  7. @TTL
  8. GOTO @TURTLE
  9. @TTR
  10. GOTO @TURTLE
  11. @TTC
  12. C=N:GCOLOR C
  13. GOTO @TURTLE
  14. @TTP
  15. IF N==0 THEN GPAINT X0,Y0,C ELSE GPAINT X0,Y0,N
  16. GOTO @TURTLE


Wow, that was easy! Of course, the hard part is coming up. Specifically, we're going to talk a bit about polar coordinates. We need it to draw the turtle. But first, we're going to just set the angle. That's not too difficult. All we have to do is adjust the angle variable just like we did with color. About the only thing that we have to do is to convert the angle to radians.

A bit of caution here. Mostly, we can just to "L180" to do a turn-about movement. However, since we decided to limit the numeric portion to just 2 digits, the most we can do is quarter turn. If we want to have turn-about the turtle, then we have to do it twice, with "L90L90" command.

Another thing. We want to rotate the turtle to the appropriate angle. So far, all we did was display the sprite. Now we want to set the angle and have the turtle facing in the right direction. So, there are two steps. First, we want to convert degrees to radians, and second, we want to orient the sprite accordingly.

There is a complication here. That is, computers have 0,0 coordinate on the top-left corner of the screen, unlike math with their 0,0 coordinate on the bottom-left of the screen. We'll have to fudge the math a bit, and the best way to do it is to just experiment. So, we're going to set up a new subroutine @ROTATE which will rotate the sprites according to angles. That is, given the value of A in degree, set the direction accordingly. All we have to do then, is decide which values goes which direction. I think 0 degree should point up. With that in mind, do this real quick:


  1. @TEST
  2. FOR A=0 to 360 STEP 360/12
  3. GOSUB @ROTATE:WAIT 60
  4. NEXT

  5. @ROTATE
  6. A=A%360
  7. SPANGLE 1,A,6
  8. RETURN



We discovered a weakness in our program in that the rotation is not centered on the sprite. We fix it by adding this line on the @INIT subroutine.

SPHOME 1,8,8

Then we simply add these lines to the LEFT and RIGHT portion of the code:


  1. @TTL
  2. A=A-N:'TURN LEFT
  3. GOSUB @ROTATE

  4. @TTR
  5. A=A+N:'TURN RIGHT
  6. GOSUB @ROTATE



And there you go! Just one more function and that's all there is to it! Who says turtle graphic programming is hard? Haha, joking aside, we have been delaying the inevitable. Not only we have to convert degree to radians, but we also have to make sure that the turtle is facing the right way. Fortunately, there are functions RAD(),SIN() and COS() that makes it simple. There is something else that we have to worry about, but we'll defer that until later. For now, let's draw!


  1. @TEST
  2. FOR A=0 to 360 STEP 360/12
  3. SPOFS 1,128,96:GOSUB @ROTATE
  4. X1=50*SIN(RAD(A)):'RSINTHETA
  5. Y1=50*COS(RAD(A)):'RCOSTHETA
  6. SPOFS 1,128+X1,96+Y1,50:'50 IS THE DISTANCE
  7. WAIT 60
  8. NEXT


If you run this code, you will see that the whole thing breaks up. You see that the sprite and the movement do not match. We want to see the sprites to move in a clock-wise pattern. The problem now is how to fix it? Remember that the coordinate is centered at top-left? That's the problem! To the math savvy among you, it may interest you to come up with the solution yourself. Otherwise, here's the fix!

Y1=-50*COS(RAD(A)):'RCOSTHETA

Yes, it's that easy! Simply turn R into negative R, and that's all there is to it. Really. Now, all we have to do is do the @TTF subroutine and we'll be done!


  1. @TTF
  2. D=N
  3. X1=D*SIN(RAD(A))
  4. Y1=(-1*D)*COS(RAD(A))
  5. SPOFS 1,X0+X1,Y0+Y1,D
  6. @TTF1
  7. SPREAD(1),TTFX,TTFY:'GOSUB @WRAP
  8. IF C!=0 THEN GPSET TTFX,TTFY,C
  9. IF SPCHK(1)!=0 GOTO @TTF1
  10. X0=X0+X1:Y0=Y0+Y1


And that's it! You're probably wondering why we're doing interval D on SPOFS. The reason for it is so that we can retrieve sprite coordinates TTFX,TTFY along the way, and plot the points with color C. That's a pretty neat trick. Otherwise, we'd have to mess around with line drawing algorithm. Not that coming up with line drawing algorithm is bad, just that it's an unnecessary complexity if all that we want is a simple subroutine.

Oh, one more thing: Suppose we want to do a wrap around? What should we do? Well, I don't like to mess with the current code, but basically, what we want to do is adjust the sprite location and update the target accordingly. So, we need to check whether TTFX or TTFY is below zero and if so, shift everything by the screenful. Trivial, had we did our own line drawing algorithm. Not so much if we're dependent upon internal routines, just like we did.

What exactly do we need to implement coordinate wrapping, anyway? Well, we need the start and stop coordinates, which are X0,y0 and X1,Y1. We also need a coordinate that specifies the current location going there. And we want to be able to reset the coordinates from the sprites.

So, we can do a bunch of calculation, but basically, we want to handle these 2 lines:

SPOFS 1,NEWX,NEWY
SPOFS 1,ADJX,ADJY,TIMELEFT

So, we need 5 variables to be set. To the math savvy among you, I suggest that you work it out yourself.

Time left is easy. We have already have D. For the new D, all we have to do is calculate the distance of the current position with the target coordinate. TIMELEFT=SQR((X1-TTFX)^2+(Y1-TTFY)^2) The old distance formula standby. NEWX and NEWY is simply OLDX=OLDX+256 and OLDY=OLDy+192. Basically, screen width and height. Same thing with ADJX and ADJY.

There's nothing to it, is there? We need to add WRAP=1 and replace the commented 'GOSUB @WRAP with IF WRAP THEN GOSUB @WRAP.

Before we do a bunch of complicated math, though, I'll point out to you something very interesting. The fact that there is only one GPSET makes it easy. If we don't care about drawing the point where the turtle is, and just worry about wrap, we can easily modify the line to this:


  1. IF C!=0 THEN GPSET ((TTFX+256)%256),((TTFY+192)%192),C
  2. IF SPCHK(1)!=0 GOTO @TTF1
  3. X0=((X0+X1+256)%256):Y0=((Y0+Y1+192)%192)
  4. SPOFS 1,XO,YO


That's something to think about. Do we want to write some complicated routines or do we just want to get the job done? With that in mind, instead of doing a bunch of mathematics, we can simply create a secondary sprite and put it into the current point set whenever the original turtle is out of bound! Simple, yet effective!


  1. @WRAP
  2. SPOFS 2,((TTFX+256)%256),((TTFY+192)%192)
  3. SPANGLE 2,A,0
  4. RETURN


And that's all there is to it! We can easily implement this within one hour. With wrap function, we're pushing it into the hour, depending on how fast you can type with the stylus. Regardless, we're only typing less than 100 lines program, with testing subroutine. What's the test? Well, remember that we're limiting the data to only 2 digits? That helps when we're doing wrapping test cases. But how about when we want to do large drawings? The test subroutine is used throughout the development process in order to try out subroutines outside the main program.

I commented out the test subroutine in the final program, but it's a good example on how you can use it in your program, so I'm leaving it there. The total time that it takes to do this program is:

15 min Initial Framework
10 min C and P command
10 min Rotation
15 min TTF drawing
10 min Wrap option

Total Development time? 1 hour, including screen wrap function. Now that's what I call "Hour of Code"!





  1. REM TURTLE GRAPHIC
  2. REM BY HARRY HARDJONO
  3. REM DEC 2013

  4. ACLS:CLEAR

  5. @INIT
  6. WRAP=1
  7. C$="":'COMMAND STRING
  8. N=0:'COMMAND PARAMETER
  9. X0=128:Y0=96:'TURTLE POSITION
  10. X1=0:Y1=0:'TURTLE DESTINATION
  11. A=0:'ANGLE
  12. D=0:'DISTANCE
  13. C=0:'PEN COLOR
  14. SPSET 1,139,0,0,0,0:SPOFS 1,X0,Y0
  15. SPHOME 1,8,8
  16. SPSET 2,139,0,0,0,0:SPOFS 1,-255,-255
  17. SPHOME 2,8,8

  18. @MAIN
  19. 'GOSUB @TEST
  20. 'CLASSIC RECTANGLE
  21. FOR I=1 TO 8
  22. C$="F35R90F35R90F35R90F35R90"
  23. GOSUB @TURTLE
  24. NEXT

  25. @END
  26. BEEP 6:WAIT 600
  27. END

  28. @TURTLE
  29. IF LEN(C$)<3 THEN RETURN
  30. TC$=LEFT$(C$,3)
  31. N=VAL(RIGHT$(TC$,2))
  32. TC$=LEFT$(C$,1)
  33. C$=RIGHT$(C$,LEN(C$)-3)
  34. TN=INSTR("FLRCP",TC$)
  35. ON TN GOTO @TTF,@TTL,@TTR,@TTC,@TTP
  36. GOTO @TURTLE
  37. @TTF
  38. D=N
  39. X1=D*SIN(RAD(A))
  40. Y1=(-1*D)*COS(RAD(A))
  41. SPOFS 1,X0+X1,Y0+Y1,D
  42. @TTF1
  43. SPREAD(1),TTFX,TTFY:IF WRAP THEN GOSUB @WRAP
  44. IF C!=0 AND WRAP==1 THEN GPSET ((TTFX+256)%256),((TTFY+192)%102),C
  45. IF C!=0 AND WRAP==0 THEN GPSET TTFX,TTFY,C
  46. IF SPCHK(1)!=0 GOTO @TTF1
  47. IF WRAP==1 THEN X0=((X0+X1+256)%256:Y0=((Y0+Y1+192)%192):SPOFS1,X0,Y0
  48. IF WRAP==0 THEN X0=X0+X1:Y0=Y0+Y1
  49. GOTO @TURTLE

  50. @TTL
  51. A=A-N:'TURN LEFT
  52. GOSUB @ROTATE
  53. GOTO @TURTLE
  54. @TTR
  55. A=A+N:'TURN RIGHT
  56. GOSUB @ROTATE
  57. GOTO @TURTLE
  58. @TTC
  59. C=N:GCOLOR C
  60. GOTO @TURTLE
  61. @TTP
  62. IF N==0 THEN GPAINT X0,Y0,C ELSE GPAINT X0,Y0,N
  63. GOTO @TURTLE

  64. @TEST
  65. WRAP=1:'0 OR 1
  66. FOR SP=0 TO 150 STEP 3
  67. V=SP:LOCATE 0,0:?SP
  68. @LINER
  69. IF V>99 THEN C$="F99":GOSUB @TURTLE:V=V-99:GOTO @LINER
  70. L1$=RIGHT$(("000"+STR$(V)),2)
  71. C$="F"+L1$+"R70C"+L1$:GOSUB @TURTLE
  72. NEXT
  73. RETURN

  74. @ROTATE
  75. IF A<0 THEN A=A+360:GOTO @ROTATE
  76. A=A%360:SPANGLE 1,A,6
  77. RETURN

  78. @WRAP
  79. SPOFS 2,((TTFX+256)%256),((TTFY+192)%192)
  80. SPANGLE 2,A,0
  81. RETURN



Friday, October 18, 2013

Petit Computer Journal #21


A Simple Timer

This here is a simplified version of the timer program. I put it here just to clarify that there are other ways to do this. The previous version was used to show LOOP construct, but this version is an optimized version of it. Notice that duplicate codes are collapsed into one, thus simplifying the whole structure.



  1. 'TIMER PROGRAM BY HARRY HARDJONO
  2. SPSET 1,156,0,0,0,0
  3. SPOFS 1,-50,-50
  4. SPANIM 1,4,15
  5. SPSCALE 1,200

  6. @INIT
  7. CLS
  8. SPOFS 1,0,96,0
  9. SPREAD(1),X,Y
  10. ?"HOW MANY SECONDS?"
  11. INPUT T
  12. IF 0>=T GOTO @END
  13. CLS
  14. ?"A STARTS TIMER"
  15. ?"B SETS NEW TIME"
  16. ?"X EXIT"
  17. ?"Y PAUSE"

  18. @LOOP
  19. VSYNC 1:B=BTRIG()
  20. IF (SPCHK(1) AND 1) THEN SPOFS 1,X,Y,0 
  21. IF (B AND 16) THEN SPOFS 1,224,96,FLOOR((224-X)*T/224)*60:GOTO @TICK
  22. IF (B AND 32) GOTO @INIT
  23. IF (B AND 64) GOTO @END
  24. GOTO @LOOP
  25. @TICK
  26. IF (BUTTON(0) AND 128) GOTO @LOOP
  27. LOCATE 0,8:SPREAD(1),X,Y:?"TIME LEFT: ";FLOOR((224-X)*T/224);"   "
  28. IF (SPCHK(1) AND 1) THEN BEEP 28:WAIT 60:GOTO @TICK
  29. ?"TIME'S UP!":SPOFS 1,0,96,0:X=0:BEEP 50:WAIT 120
  30. GOTO @LOOP

  31. @END
  32. ?"GOODBYE!"
  33. SPCLR 1:WAIT 300