I have had the chance to get back into calculator programming, and though it'd be a nice challenge to create a QR code generator.
The simple parts (drawing the finder, alignment, and timing patterns to the screen, encoding the data into a bitstream, data masking, hard-coding version information) were a fun exercise in TI-BASIC graphics and list manipulation. It was nice to freshen up on these skills.
More difficult was the actual drawing pattern for the data bits themselves. A zig-zagging left-right, down-up, 2-pixels-at-a-time pattern that had to skip over the fixed parts of the QR code. This is compounded by having to support all versions that fit on a monochrome screen, up to the 61x61 pixel Version 11, holding up to 321 bytes.
But the Reed-Solomon Error Correction... oh my. The took quite some work to get functioning properly, and even then, it is not quick. It requires heavy use of bitwise XOR between two 8-bit values. This is not something that 83+/84+ TI-BASIC does well (TI-89/92/Voyage has bitwise XOR built-in 😛 ).
There are two 256-element lists that are hard-coded, trading off RAM usage for a bit more speed. One has a LUT for 4-bit XOR (call the LUT on the low and high nibbles of the two values, add them together, you get an 8-bit XOR). The other is a conversion from Galois Field 256 alpha notation and their exponents, a sort of log/anti-log table in GF(256). The XOR LUT, and perhaps even the GF(256) LUT, could be removed, saving 2-5kB+ of RAM during execution, if there existed a much faster way of calculating bitwise XOR.
If anyone has a better idea for bitwise XOR in pure TI-BASIC (that works for all values [0,255]) that are faster than the LUT...
This up for approval into the archives as well.
Code:
The simple parts (drawing the finder, alignment, and timing patterns to the screen, encoding the data into a bitstream, data masking, hard-coding version information) were a fun exercise in TI-BASIC graphics and list manipulation. It was nice to freshen up on these skills.
More difficult was the actual drawing pattern for the data bits themselves. A zig-zagging left-right, down-up, 2-pixels-at-a-time pattern that had to skip over the fixed parts of the QR code. This is compounded by having to support all versions that fit on a monochrome screen, up to the 61x61 pixel Version 11, holding up to 321 bytes.
But the Reed-Solomon Error Correction... oh my. The took quite some work to get functioning properly, and even then, it is not quick. It requires heavy use of bitwise XOR between two 8-bit values. This is not something that 83+/84+ TI-BASIC does well (TI-89/92/Voyage has bitwise XOR built-in 😛 ).
There are two 256-element lists that are hard-coded, trading off RAM usage for a bit more speed. One has a LUT for 4-bit XOR (call the LUT on the low and high nibbles of the two values, add them together, you get an 8-bit XOR). The other is a conversion from Galois Field 256 alpha notation and their exponents, a sort of log/anti-log table in GF(256). The XOR LUT, and perhaps even the GF(256) LUT, could be removed, saving 2-5kB+ of RAM during execution, if there existed a much faster way of calculating bitwise XOR.
If anyone has a better idea for bitwise XOR in pure TI-BASIC (that works for all values [0,255]) that are faster than the LUT...
This up for approval into the archives as well.
Code:
:DCS6
"FD3F84A1B52DB4AD8521FCBF0100AAAA55550000FCCE8529B529B52E8549FCA9"
ClrHome
""Goto A
Menu("USE Str1 AS-IS?","YES",A,"NO (NEW Str1)",B
Lbl B
ClrHome
Input "Str1:",Str1
ClrHome
Lbl A
//Store the variables used for restoration later
{A,B,C,D,E,F,G,H,S,V}->|LA
Output(1,1,"BYTES: VER: "
Output(1,7,length(Str1
//Determine Verion needed based on Str1 length. Up to 321 characters (encoded bytes) in Version 11
1+sum({17,32,53,78,106,134,154,192,230,271,321}<length(Str1))->V
//If too long, truncate Str1 and note this by displaying ^^o next to the BYTES readout
If length(Str1)>321
Then
sub(Str1,1,321->Str1
11->V
Output(1,10,"^^o"
End
Output(1,15,V
//Start Encoding the bitstream, broken into bytes in |LD
//Byte-encoding is used, so the first 4 bits should be 0100
{4*16}->|LD
//For Versions 1-9, 8 bits are needed to store the length of the encoded data. for versions 10 and 11, 16 bits are used. Since the encoding method takes up 4 bits, put the high 4 bits of the length into the lower 4 bits of the first bytestream byte, and the next 4 bits into the higher 4 bits of the next byte.
If V>=10
Then
iPart(length(Str1)/256)->C
|LD(1)+iPart(C/16)->|LD(1)
16*16fPart(C/16)->|LD(2)
End
//This line extracts the lower 8 bits of the length. For V's 1-9, this does not do anything.
256fPart(length(Str1)/256)->C
//Again, upper 4 bits into the lower 4 bits of the last byte, lower 4 bits into the upper 4 bits of the next byte
|LD(dim(|LD))+iPart(C/16)->|LD(dim(|LD))
16*16fPart(C/16)->|LD(1+dim(|LD))
//At this point, the datastream looks like this:
//V 1-9: [0100 XXXX] [XXXX 0000], where XXXXXXXX is the 8-bit length of the encoded data
//10-11: [0100 YYYY] [YYYY XXXX] [XXXX 0000], where YYYYYYYYXXXXXXXX is the 16-bit length of the encoded data
Output(2,1,"DATAWORD:0 / "
Output(2,14,length(Str1
//For each character, convert it to the ASCII codeword (0x20-0x7E), and split that 8-bit code into high and low 4-bit nibbles, putting them in the datastream in order.
//SourceCoder removes the \ character, so I have replaced it here with / (between [ snd ]). This is correct in the .8xp
For(A,1,length(Str1
//Find the codeword corresponding to the character
31+inString(" !^^o#$%&'()*+,-./0123456789:;<\=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}|~",sub(Str1,A,1))->C
//If it's not found, replace it with a space
If C=31
32->C
dim(|LD)->D
//upper 4 bits of codeword go into the lower 4 bits in the last byte of the datastream
|LD(D)+iPart(C/16)->|LD(D)
//lower 4 bits of the codeword go into the upper 4 bits into the next byte of the datastream
256*fPart(C/16)->|LD(D+1
Output(2,10,A
End
//After all characters are encoded, the datastream need to be padded out to fill the length required of its version.
{19,34,55,80,108,136,156,194,232,274,324}
//Find the difference between the length of the datastream and the requried length
Ans(V)-dim(|LD)->C
If C
Then
1->D
//If there is a difference, alternate between adding bytes 236 and 17 to the end of the datastream. Being 3 bytes short would result in the augmentation of 236, 17, 236
For(A,1,C
D236+(1-D)17->|LD(1+dim(|LD))
1-D->D
End
End
//The datasteam is then broken into blocks, and each block has a number of error correction codes calculated. The version's error correction code count per block is determined
{7,10,15,20,26,18,20,24,30,18,20
Ans(V->E
Output(3,1,"BLOCKING:0 / "
Output(3,14,dim(|LD
//For each version, the number of blocks the datastream is broken into is determined. For V1-5, the datastream is treated as a single block.
{1,1,1,1,1,2,2,2,2,4,4
Ans(V->B
//The datastream is split into half or into 4 pieces. The number of datawords in each block is calculated based on the number of datawords in that version and dividing that by how many blocks it is being broken into. For Version 10, the division results in 68.5 datawords. This is a special case, where the first two blocks are 68 datawords, and the last two blocks are 69 datawords.
{19,34,55,80,108,136,156,194,232,274,324}
iPart(Ans(V)/B->D
//Example: datasteam is {1,2,3,4,5,6,7,8}, breaking that into 2 blocks:
// block 1: {1,2,3,4}, block 2: {5,6,7,8}
{0->|LB1
{0->|LB2
{0->|LB3
{0->|LB4
//Set the dimensions of the Block lists, adding 1 to B3 and B4 for the special case of V10
D->dim(|LB1
If B>=2
D->dim(|LB2
If B>=3
D+(V=10->dim(|LB3
If B>=4
D+(V=10->dim(|LB4
1->A
1->D
//Var D tracks which block is being written to. Var A counts how many bytes have been written to that block so far.
For(C,1,dim(|LD
If D=1
|LD(C->|LB1(A
If D=2
|LD(C->|LB2(A
If D=3
|LD(C->|LB3(A
If D=4
|LD(C->|LB4(A
A+1->A
//When the whole block has been written, move to the next block.
If A>(dim(|LB1)+(V=10 and D>=3)
Then
1->A
D+1->D
End
Output(3,10,C
End
//To save RAM, once the datastream |LD has been split into blocks |LB1-3, delete |LD.
0->dim(|LD
//Reed-Solomon Error Correction emplys heavy use of bit-wise XOR operations. On the 68k calulators (TI-89, TI-92/Voyage), XOR is already a bitwise operator. But this is not the case on the Z80 calcs, so XOR calculations in TI-BASIC are computationally expensive.
//To aid in this, a 4-bit XOR table is populated into |LX. For values of F=[0,15] and G=[0,15], the lookup table |LX(16F+G+1) contains the value of bitwise F OR G. Performing this lookup on both the upper and lower nibbles of two bytes is faster than extracting each bit, comparing them, and calculating the result. Even with this speedup, the Error Correction calculations take the majority of the time spent on generating the QR code.
0->dim(|LX
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0}->|LX
//The Error Correction calculations must create data that falls within the limits [1-255] (Galois Field 256, or GF(256)), so the error codes all fit within a byte each. For this, as you multiply by 2, for every number over 255, a modulo'd value is calculated (256 turns into 29, which multiplied by 2 is 58, 116, 232, then 464 is turned into 205, etc). Doing the calculation for each time this is needed would take too long, so these numbers are pre-calculated into a Log lookup table.
0->dim(|LLOG
{1,2,4,8,16,32,64,128,29,58,116,232,205,135,19,38,76,152,45,90,180,117,234,201,143,3,6,12,24,48,96,192,157,39,78,156,37,74,148,53,106,212,181,119,238,193,159,35,70,140,5,10,20,40,80,160,93,186,105,210,185,111,222,161,95,190,97,194,153,47,94,188,101,202,137,15,30,60,120,240,253,231,211,187,107,214,177,127,254,225,223,163,91,182,113,226,217,175,67,134,17,34,68,136,13,26,52,104,208,189,103,206,129,31,62,124,248,237,199,147,59,118,236,197,151,51,102,204,133,23,46,92,184,109,218,169,79,158,33,66,132,21,42,84,168,77,154,41,82,164,85,170,73,146,57,114,228,213,183,115,230,209,191,99,198,145,63,126,252,229,215,179,123,246,241,255,227,219,171,75,150,49,98,196,149,55,110,220,165,87,174,65,130,25,50,100,200,141,7,14,28,56,112,224,221,167,83,166,81,162,89,178,121,242,249,239,195,155,43,86,172,69,138,9,18,36,72,144,61,122,244,245,247,243,251,235,203,139,11,22,44,88,176,125,250,233,207,131,27,54,108,216,173,71,142,1}->|LLOG
//For each version, the number of error codes for each block is determined.
//Then, for that number of error codes, a generator polynomial with that number of coefficients is defined. These are calculatable, but are easier just to have pre-calculated.
//coeffiecient count = {7,10,15,20,26,18,20,24,30,18,20}(V)
If V=1
{0,87,229,146,149,238,102,21->|LG
If V=2
{0,251,67,46,61,118,70,64,94,32,45->|LG
If V=3
{0,8,183,61,91,202,37,51,58,58,237,140,124,5,99,105->|LG
If V=4 or V=7 or V=11
{0,17,60,79,50,61,163,26,187,202,180,221,225,83,239,156,164,212,212,188,190->|LG
If V=5
{0,173,125,158,2,103,182,118,17,145,201,111,28,165,53,161,21,245,142,13,102,48,227,153,145,218,70->|LG
If V=6 or V=10
{0,215,234,158,94,184,97,118,170,79,187,152,148,252,179,5,98,96,153->|LG
If V=8
{0,229,121,135,48,211,117,251,126,159,180,169,152,192,226,228,218,111,0,117,232,87,96,227,21->|LG
If V=9
{0,41,173,145,152,216,31,179,182,50,48,110,86,239,96,222,125,42,173,226,193,224,130,156,37,251,216,238,40,192,180->|LG
//These Generator Polynomials are used as the divisor in a long-division algorithm with each block's bytes as the coefficients of a polylomial in the numerator. The remainder of the this long-division is the error correction code.
{0->|LE1
{0->|LE2
{0->|LE3
{0->|LE4
//Prepare the Error Correction Code (ECC) lists, based on the number of B Blocks the datastream was broken into.
E->dim(|LE1
If B>=2
E->dim(|LE2
If B>=3
E->dim(|LE3
If B>=4
E->dim(|LE4
//For each Error Code list E up to the number of B Blocks:
For(E,1,B
If E=1
|LB1->|LM
If E=2
|LB2->|LM
If E=3
|LB3->|LM
If E=4
|LB4->|LM
//Store the correspongin Block into an in-work Message |LM
dim(|LM->C
Output(4,1,"ERR: / , 0 / "
Output(4,5,E
Output(4,7,B
Output(4,14,C
//For every byte A in the Message:
For(A,1,C
//Find the exponent (reverse lookup into |LLOG) of the byte
sum(not(cumSum(|LLOG=|LM(1)->D
//Store the Generator Polynomial into an in-work |LH
|LG->|LH
//For every coefficient in the in-work Generator Polynomial:
For(G,1,dim(|LH
//Add the exponents of the byte A and the coefficient of the generator polynomial (mod 255), effectivly multiplying the generator polynomial such that the leading term of the resulting polynomial (which used to be 1) is now equal to the byte.
D+|LH(G
If Ans>255
Ans-255
|LLOG(1+Ans)->|LH(G
End
//In Long division, if calculating 781/2, we have now said that two goes into seven 3 times. This 3 has been multiplied by the divisor 2 resulting in 6 (the current contents of |LH is "6"). Now we need to subtract 600 from the numerator 781, giving us a remainder to continue the long division.
//In Galois Field 256, addition and subtraction act on bits independently, which is to say addition and subtraction are the same operation: bitwise XOR. So for each element in the Message (781), subtract (XOR) the result of the multiplacation of the Generator polynomial and how many times the leading term of that polynomial fits into the leading byte of the message (600, |LH)
max({dim(|LM),dim(|LH)->dim(|LM
Ans->dim(|LH
For(D,1,dim(|LM
|LH(D)/16->F
|LM(D)/16->G
16|LX(16int(F)+int(G)+1)+|LX(256fPart(F)+16fPart(G)+1->|LM(D
End
//This result is a new message polynomial that has a leading coefficient of 0, so chop off that zero and move down to the next byte in the remainder. (781-600=181, now move on to the ten's place)
DeltaList(cumSum(|LM->|LM
Output(4,10,A
End
//Do this for each block. The remainder after stepping through every byte in the message is the ECC. Store these for later
If E=1
|LM->|LE1
If E=2
|LM->|LE2
If E=3
|LM->|LE3
If E=4
|LM->|LE4
End
//To save on RAM, delete the in-work lists and lookup tables.
DelVar |LM
DelVar |LG
DelVar |LH
DelVar |LX
DelVar |LLOG
Output(5,1,"INTERLEAVING:"
Output(6,1," DATA: 0 / "
Output(6,11,dim(|LB1
//Now we re-build the Datastream using the blocks and the ECCs. For codes with more than one block, this involves interleaving the data from each block byte-wise.
For(A,1,dim(|LB1)
|LB1(A->|LD(1+dim(|LD
If B>=2
|LB2(A->|LD(1+dim(|LD
If B>=3
|LB3(A->|LD(1+dim(|LD
If B>=4
|LB4(A->|LD(1+dim(|LD
Output(6,8,A
End
//V10 is the special case, where blocks 3 and 4 are 69 bytes long. Tack those on the end.
If V=10
Then
|LB3(dim(|LB3->|LD(1+dim(|LD
|LB4(dim(|LB4->|LD(1+dim(|LD
End
//To save RAM, delete the blocks.
DelVar |LB1
DelVar |LB2
DelVar |LB3
DelVar |LB4
Output(7,1," ERROR: 0 / "
Output(7,13,dim(|LE1
//After all the blocks' data bytes are interleaved, the same process is enacted on the Error codes, reulting in a final bitstream to draw the QR code.
For(A,1,dim(|LE1)
|LE1(A->|LD(1+dim(|LD
If B>=2
|LE2(A->|LD(1+dim(|LD
If B>=3
|LE3(A->|LD(1+dim(|LD
If B>=4
|LE4(A->|LD(1+dim(|LD
Output(7,9,A
End
//To save RAM, delete the error codes.
DelVar |LE1
DelVar |LE2
DelVar |LE3
DelVar |LE4
//The resulting datatream |LD is used to draw the QR code.
//Set up the graphscreen to be blank, ready to draw.
PlotsOff
FnOff
AxesOff
GridOff
ClrDraw
//The size of the QR code is calculated based on the version. V1 is 21 pixels, with each version being 4 pixels larger. This is 0-indexed, so Size will be 20 for V1, not 21.
4V+16->S
//Calculate the Upper-Left pixel position of the QR code. Limiting this to V11 means a 61x61 pixel code, which has only a single row of pixels spare on the top and bottom of the code on the 63x95 pixel space of a monochome calculator. The color calculators could handle much larger codes, but for simplicity, these have not been implemented. Even so, that much data is not possible to be viewed on the homescreen at once (26x10=260 characters on the homescreen of a color calc, only 16x8=128 on monochrome), and if your data is longer than 300 bytes, it's probably best not to use a QR code, or shorten your data IMO)
31-S/2->A
47-S/2->B
//The 3 characteristic finder patterns are drawn to the screen (the bulls-eyes in the 3 corners).
{0,0,0,0,0,0,0,1,1,2,2,2,2,2,3,3,3,3,3,4,4,4,4,4,5,5,6,6,6,6,6,6,6,0,1,2,3,4,5,6,0,6,0,2,3,4,6,0,2,3,4,6,0,2,3,4,6,0,6,0,1,2,3,4,5,6}
//Ans is used since it is quicker to access than a list.
For(C,1,33
Pxl-On(A+Ans(C),B+Ans(C+33)
Pxl-On(A+S-Ans(C),B+Ans(C+33)
Pxl-On(A+Ans(C),B+S-Ans(C+33)
End
//Alignment pattern(s) are drawn. These are the smaller bulls-eye(s) that are in the bottom-right of larger QR codes, and in the center and along the edges of even larger QR codes.
{~2,~1,0,1,2,~2,2,~2,0,2,~2,2,~2,~1,0,1,2,~2,~2,~2,~2,~2,~1,~1,0,0,0,1,1,2,2,2,2,2}
For(C,1,17
If V>=2
Pxl-On(25+S/2+Ans(C),41+S/2+Ans(C+17)
If V>=7
Then
Pxl-On(31+Ans(C),53+Ans(C+17)-S/2
Pxl-On(37+Ans(C)-S/2,47+Ans(C+17)
Pxl-On(31+Ans(C),47+Ans(C+17)
Pxl-On(31+Ans(C),41+Ans(C+17)+S/2
Pxl-On(25+Ans(C)+S/2,47+Ans(C+17)
End
End
//Timing patterns are drawn between the Finders, alternating black and white pixels.
For(C,1,2V+1
Pxl-On(A+2C+6,B+6
Pxl-On(A+6,B+2C+6
End
//For every QR code, there is a single pixel that is always black, and is not dependent on any condition.
Pxl-On(A+4V+9,B+8
//Weare using bitmask 0, and correction level L. The L-0 format information is drawn around the finders.
Pxl-On(A+8,B
Pxl-On(A+8,B+1
Pxl-On(A+8,B+2
Pxl-On(A+8,B+4
Pxl-On(A+8,B+5
Pxl-On(A+8,B+7
Pxl-On(A+8,B+8
Pxl-On(A+7,B+8
Pxl-On(A+2,B+8
Pxl-On(A+S,B+8
Pxl-On(A+S-1,B+8
Pxl-On(A+S-2,B+8
Pxl-On(A+S-4,B+8
Pxl-On(A+S-5,B+8
Pxl-On(A+S-6,B+8
Pxl-On(A+8,B+S-7
Pxl-On(A+8,B+S-6
Pxl-On(A+8,B+S-2
//G keeps track of which byte is currently being drawn.
//H keeps track of which bit in that byte is being drawn.
1->G
0->H
0->|LD(1+dim(|LD
//Working from the right to the left, every 2 pixels:
For(F,S,0,~2
fPart((S-F)/4)<.5->C
//starting from the botton up, then from the top down, every 4 pixels
For(D,SC,Snot(C),1-2C
//For each of the 2 pixels, from right to left (D,E contains the Y,X position of the pixel to be evaluated
For(E,F,F-1,~1
//if the pixel is not in the finder pattern or format information sections
If not((D<9 and E<9) or (D<9 and E>S-8) or (D>S-8 and E<9)
Then
//if the pixel is not in the timing pattern, or in the alignment patterns, or in version sections
If not(D=6 or E=6 or (V>=2)(D>=S-8 and D<=S-4 and E>=S-8 and E<=S-4) or (V>=7)((D<6 and E>S-11) or (D>S-11 and E<6) or (abs(D-S/2)<=2 and max(abs(E-{S/2,6,S-6})<=2)) or (max(abs(D-{6,S-6})<=2) and abs(E-S/2)<=2)))
Then
//it is a pixel that can be drawn to
//if the current bit of the current byte is 1, draw the pixel
If iPart(2fPart(|LD(G)/2^(8-H)
Pxl-On(A+D,B+E
//if the current position of the drawn pixel falls on the checkerboard pattern (datamask 0), change the pixel.
If 0=fPart((D+E)/2
Pxl-Change(A+D,B+E
//increment the bit
H+1->H
If H=8
Then
//increment the byte
0->H
G+1->G
End
End
End
End
End
//if on the left timing pattern, skip this column of pixels.
If F=8
7->F
End
//We are done drawing the data, so we can delete the datastream
DelVar |LD
//For versions 7 and higher (those with additional alignment patterns), the version is encoded as an 18-bit number into a 3x6 block near the finder patterns.
If V>=7
Then
//These 18-bit numbers can be calculated for each version, but are easier to have hard-coded.
{31892,34236,39577,42195,48118}
Ans(V-6)->F
For(D,0,5
For(E,0,2
//For each bit in that number, draw that bit in its respective location in the QR code.
F/2->F
If fPart(Ans
Then
Pxl-On(A+S-10+E,B+D
Pxl-On(A+D,B+S-10+E
iPart(Ans->F
End
End
End
End
//For Monocrome calcs, draw black bars down the outside of the screen to indicate (out of the corner of the eye, having left this program to run for half an hour) that the program is finished.
If (Xmax-Xmin)/DeltaX<100
Then
For(A,0,62
Pxl-On(A,0
Pxl-On(A,1
Pxl-On(A,2
Pxl-On(A,3
Pxl-On(A,94
Pxl-On(A,93
Pxl-On(A,92
Pxl-On(A,91
End
Else
//For Color calcs, the QR code has been drawn really small in the upper-left of the screen. Enlarge this x2, and center it properly. This is also an ou-the-corner-of-the-eye indication of program completion.
For(D,A+S,A,~1
For(E,B,B+S
If pxl-Test(D,E
Then
Pxl-Off(D,E
Pxl-On(2D+18,2E+36
Pxl-On(2D+19,2E+36
Pxl-On(2D+19,2E+37
Pxl-On(2D+18,2E+37
End
End
End
End
//Restore the used variables
|LA(1->A
|LA(2->B
|LA(3->C
|LA(4->D
|LA(5->E
|LA(6->F
|LA(7->G
|LA(8->H
|LA(9->S
|LA(10->V
//Leave No Trace
DelVar |LA
ClrHome
//DispGraph so that if this program was executed from a Shell, it won't just disappear never to be seen again. This could have been a Pause, or a StorePic 1, or something else.
DispGraph




