Version 2.2 July 2006 TI-83 Basic Tricks & Tips (The Compleat Basic Programmer) This guide is designed for TI-Basic programmers who think they've mastered Basic. Why is optimization important? Several reasons come to mind: Often, your program will run faster. Often, your program will be smaller. Frequently, you will expand your knowledge of Basic. Mostly, your audience will thank you, for allowing them to stuff more games into their 27K of RAM. Also, it's fun, in a geeky sort of way. When I first wrote this guide, back in 2000, I had never seen a concise guide to the "dos and don'ts" of TI-83 programming. There were a couple of small tutorials floating around, but nothing all-inclusive. These days, with the rise of the Web forum and the graying of the TI-83 community, it's easier to find extensive documentation on various individual aspects of Basic programming, but rarely in an easy-to-use, indexed format. With Version 2 of "The Compleat Basic Programmer," I hope to remedy this problem. This guide contains many "case studies": snippets of code and the occasional full-fledged program, all of which I believe to be bug-free. These programs are often simplified to demonstrate some approach more concisely, but I have taken great care to optimize them as I would if they were "real" programs for everyday use. I hope that you, the reader, can use the case studies to learn more about optimization than is stated explicitly in the text. A note on syntax: The TI-83 Basic code in this guide is written in an ASCIIfication that closely approximates the stuff you can import into GraphLink. However, since this guide is meant to be read by humans, I've made a few changes: for example, the first built-in list is called L1 in this guide, not L\1\, and the unary minus is called -, not \(-)\. A note on scope: I have included some non-Basic trivia in section 10, including what might be seen as a segue into the world of Z80 assembly language and a partial list of operating system glitches that could easily grow in size to rival the Basic sections of this guide. This guide is still first and foremost about how to optimize Basic programs; but I decided that as long as I was writing an all-in-one reference, I might as well include some of the non-Basic things I'd like to see collected in one place, too. Because I'm familiar only with the original TI-83, I confine this guide to that calculator only; some parts may be inaccurate with respect to the TI-83+, Silver Edition, TI-84+, and other models in the TI-83 family. (For example, section 10.2 does not mention the TI-83+ command "Asm(", and section 1.1 does not mention the TI-84+ timer functions.) Without further ado, the guide! TABLE OF CONTENTS 1. The basics 1.1. Editing and profiling 1.2. Syntactic sugar 2. Control flow: If, Then, For, While, Repeat 2.1. Modes of control flow 2.2. Replacing "If" with arithmetic 2.3. Comparison of "If:Then" and "If" 2.4. Loop hacks 2.5. Unexplained quirks of control flow 3. Advanced control flow 3.1. Lbl, Goto, and Menu( 3.2. IS>( and DS<( 3.3. Comparison of "DelVar" and "0\->\" 4. Subroutines 4.1. External subroutines 4.2. Recursion 4.3. Internal subroutines 4.4. "Gosub" with Goto and End 4.5. Making use of Ans 5. Graphics 5.1. Bitmap sprites 5.2. Bitmaps using "inString(" 5.3. Textsprites 5.4. Plotsprites 5.5. Screen scrolling with "sub(" 6. List and matrix operations 6.1. List equations 6.2. Matrix operations 6.3. Hunt the Wumpus dungeon generation 6.4. Finding poker hands 7. Input hacks 7.1. Comma-separated list 7.2. Cheating with "Input" 7.3. Illegal string characters 7.4. The empty string 8. Two-calculator communications 9. Other hacks 9.1. Integer into string 9.2. Decimal into fraction 9.3. Shuffling a deck of cards 9.4. The dbd( function 9.5. Miscellaneous control flow hacks 9.6. Miscellaneous timings 9.7. List of single-byte tokens 10. Non-Basic miscellany 10.1. Finding your ROM version 10.2. Hexadecimal assembly code 10.3. Operating system glitches 11. References 11.1. Contributors 11.2. Web sites SECTION 1. The basics. SUBSECTION 1.1. Editing and profiling. I do most of my program editing directly on the TI-83, using the built-in program editor; however, for large programs, I often find that I want an environment with faster scrolling and the ability to keep more than one screenful of code in view at once. In those cases, I turn to TI's GraphLink software, which is rather painful to edit large amounts of code with, but does let me scroll to the right part quickly, and lets me transfer the program back and forth between TI-83 and PC for testing. When a program has complicated control flow, I typically export it into a plain text file and indent it by hand in a regular text editor to keep track of matching Then-End pairs. I usually keep two copies of a work-in-progress on my calculator: one "stable" version and one "experimental" copy, whose name usually begins with A or AA to give it a shortcut in the PRGM->EXEC menu. At some point, then, I need to rename the experimental copy. This can be done in at least two ways: Create a new program with the new title. Hit 2nd-Rcl, then PRGM->EXEC and select the old program's title. Hit Enter twice; the old program's text will be pasted into the new program. (The old program can then be deleted.) This also works to "paste" program text into the middle of existing programs; the new text is always inserted, and "pushes down" the text that follows it, instead of overwriting that text. However, you may not have enough RAM to make two copies of some large programs. To rename a large program, you will need to transfer the program to GraphLink on a PC, change its name there, and transfer it back. Now, before I explain ways to make your programs smaller and more efficient, it's worth explaining how to tell whether they're smaller or more efficient! To find out how much RAM a program takes up, press 2nd-Mem and select Delete, then Prgm. You'll see a list of all the programs on your calculator, along with their sizes in RAM. Do not press "Enter" at this point --- that will delete a program! Instead, press "Clear" to get back to the home screen. A Basic program on the TI-83 takes up 6 bytes of header, plus one byte for each character in the name, plus however many bytes of code it contains. It's generally not worth obfuscating the name of the program to save memory; besides, if you name your program "A", it will conflict with the other dozen programs whose programmers had the same "clever" idea. Give your programs good, descriptive, unique names, and make up for the extra byte or two in the compactness of the code itself. I know of no royal road to finding out how fast a program is. Intuition and good use of Stop, Disp, and the On button will give you some idea of the places your code is spending most of its time. Once you have identified a bottleneck, make a copy of the offending code and start changing it to see whether you can speed it up. There is sometimes a tradeoff between speed and size. To time a piece of code, I generally just watch the "progress indicator" in the upper right corner of the display, and count how many "ticks" it takes until the code finishes; that's how the timings in this guide were recorded. If a piece of code runs in less than one tick, then obviously the thing to do is run it many times together --- either by cutting-and-pasting the code, or by putting it in a For loop, or both. See sections 3.3 and 9.6 for details on the timing procedures in this guide. SUBSECTION 1.2. Syntactic sugar. The Basic language contains a lot of "optional" elements: ones that aren't really needed to make your program work, and can even slow it down. These "optional" elements are called "syntactic sugar" by computer programmers, because they make the syntax look prettier without adding any nutritional value. In general, you should trim away all the syntactic sugar you can. Here are all the sugar-filled areas of TI-83 Basic: Arithmetic. Write "64" instead of "4^3". Algebra. Write "X-Y-Z" instead of "X-(Y+Z)". Implied multiplication. Never, ever use the * sign for multiplication! Write "AB" instead of "A*B". (Note that implied multiplication does not bind tighter than regular multiplication and division. 1/2A is .5A, and not the same as 1/(2A).) Arithmetic instead of logic. TI-83 Basic, like C and many similar programming languages, uses the values 1 and 0 for "true" and "false" respectively. The value of "X>1" is 1 if X is greater than 1, and 0 otherwise. Thus, you can write "If XY" instead of "If X and Y". (But see section 9.6 for information on the relative speed of multiplication.) Remove closing parentheses, braces, and quotation marks. Write "[[0,1][1,0" instead of "[[0,1][1,0]]". The "STO>" character \->\ automatically closes quotes, parentheses, brackets, and braces behind it. Write "expr(Str1\->\L1(I" instead of "expr(Str1)\->\L1(I)". (Note that this means it is impossible to put a "\->\" character inside a string. See section 7.3.) The colon ":" closes parentheses, brackets, and braces, but NOT quotes. (Thus it is possible to put a colon inside a string.) Use the e^( key rather than e ^, the squared-sign key rather than ^2, the superscript-3 character rather than ^3, and the 10^( key rather than its constituent parts. (These optimizations improve speed as well as size.) The small-E exponentiation character should be substituted for 10^( whenever possible. It works only with integer powers. If nothing is put before the E, the interpreter treats it as 1 times ten-to-the-whatever. Write "E2" instead of "100", "E3" instead of "1000", and so on. ("E2" is actually faster than "100" when tested inside a loop, so there's no speed penalty.) The "DelVar" command needs no colon or newline after it. Write "DelVar A0\->\B" instead of "DelVarA:0\->\B". (This can have side effects; see section 2.1. For more on DelVar, see section 3.3.) User-defined lists often don't need the "L". Write "{1\->\A" instead of "{1\->\\L\A" to store a list into LA. The value of the scalar variable A will not be affected. (For more on the interchangeability of lists and scalars, see section 7.2.) Not all string characters are created equal. Many of the lower-case characters accessible through the Catalog or the Statistics menus, [VARS][5], really take up two bytes in the calculator's RAM. Thus, while lower-case letters can look nice in Basic programs, there is a cost to using them: your program will take up more memory. (Fewer than half the available characters and functions on the TI-83 are accessible through the Catalog. Take the time to browse the [VARS] menus and see what's there.) When displaying long text messages (as in help screens), there is no difference in size between :Disp "HELLO :Disp "WORLD and :Disp "HELLO","WORLD This is a purely stylistic choice. I tend to prefer the latter. However, there is a way to save space on multi-screen paused messages; write :Disp "HELLO :Pause "WORLD instead of :Disp "HELLO","WORLD :Pause This optimization saves two bytes, but has one side effect. The optimized version also stores "WORLD" into Ans, while the unoptimized version does not affect Ans. SECTION 2. Control flow. A lot of people use Basic's control structures --- If, For, and so on --- without actually understanding how they work. If you are going to squeeze every last drop of performance out of your program, it is very important that you understand your tools. For example, it is useful to know that Basic's "For" loop stores its upper bound and increment on the stack, rather than re-evaluating them each time. Thus, the program :10\->\B:5\->\C :For(A,1,B,C :2\->\B :-1\->\C :Disp {A,B,C :End loops over the "Disp" command only twice before exiting. (See section 3.2 for the same advice about "IS>(".) This also means that to loop exactly N times, one can write :For(N,1,N :Disp N :End The upper bound is evaluated only once, before N is set to 1 for the first iteration of the loop. SUBSECTION 2.1. Modes of control flow. The TI-83 Basic interpreter has at least three distinct "modes" in which it moves around in your program's code. I will call these modes "execution," "skipping," and "jumping." The "execution" mode is the ordinary one, in which the instruction pointer moves forward over the code, one token at a time, executing whatever code it finds. Suppose it finds the command "If 0:Then". Since the condition is false, the interpreter will change modes --- it will enter "skipping" mode. In "skipping" mode, the interpreter skips down the program's listing, line by line (where a "line" is begun with a colon), until it finds a line beginning with the token it's looking for (which may be "Else", "End", or "Lbl", depending on the context). As soon as it finds that token, it returns to execution mode. It's very important to realize that if the token is in the middle of a line, the interpreter will skip right over it! Suppose the interpreter is in the middle of executing a "For" loop. When it sees the terminating "End", it will evaluate the loop condition, and, if the loop should continue, it will enter the third mode: "jumping" mode. In this mode, no tokens are actually parsed --- the instruction pointer simply jumps forward or backward in the code to a location it had stored (on a stack) when it executed the "For" command. "Jumping" mode is only used by "For", "While", and "Repeat" commands. "Goto", "IS>(", and "DS<(" all use "skipping" mode. You can figure out whether control flow is in "jumping" or "skipping" mode by seeing how the program reacts when you insert "DelVar X" (or any variable) immediately before the control structure's target. (See section 1.2.) :DelVar XLbl A :Goto B :Disp "JUMPING :Lbl B :Goto A :Lbl A :Disp "SKIPPING This program displays "SKIPPING", proving that the interpreter doesn't remember the location of the "Lbl A" in line 1, which it encountered while in execution mode. Instead of jumping right back to that label, it searches from the top of the program, looking for a line that begins with "Lbl A". The DelVar hides the "Lbl A" on line 1 from the interpreter's skipping mode. SUBSECTION 2.2. Replacing "If" with arithmetic. Increments and decrements with simple conditionals don't need the "If" command. Instead of :If A=15:B+1\->\B write :B+(A=15\->\B and the program will be two bytes shorter and run faster. This idiom leads to a common snippet, illustrated as part of the following program that moves an "X" around the home screen until the user presses "Clear": :DelVar K4\->\A:8\->\B :Repeat K=45 :Output(A,B,"_ :A-(K=25 and A>1)+(K=34 and A<8\->\A :B-(K=24 and B>1)+(K=26 and B<16\->\B :Output(A,B,"X :Repeat Ans:getKey\->\K:End :End (The "_" on line 3 indicates a space.) SUBSECTION 2.3. Comparison of "If:Then" and "If". Every programmer should know that it is possible to write :If X :Disp "XYZZY instead of the more verbose :If X:Then :Disp "XYZZY :End It should not surprise you to learn that the former program, being shorter, runs faster when the condition X is true. However, it may surprise you that the former program takes much longer to execute than the latter when X is false! "If X:Y" takes much longer than "If X:Then:Y:End" when the body is skipped; it takes a little less time when the body is not skipped. SUBSECTION 2.4. Loop hacks. Reversing the sense of a loop test can save space. For example, write :Repeat Q :getKey=105\->\Q ... :End instead of :0\->\Q :While not(Q :getKey=105\->\Q ... :End This optimization illustrates the two differences between "While" and "Repeat": the first is that their conditions have opposite semantics ("while true" versus "repeat until true"), and the second is that the "Repeat" loop always executes its body at least once. To save one byte of storage, replace :For(I,9,0,-1 :Horizontal I :End with the reversed loop :For(I,0,9 :Horizontal 9-I :End It is also possible to condense If structures with loops. For example, we save six bytes by replacing the somewhat unlikely construction :If not(B:Then :For(I,1,E2 :Horizontal I :End:End with :For(I,1,E2not(B :Horizontal I :End SUBSECTION 2.5. Unexplained quirks of control flow. There is at least one quirk of TI-83 Basic's control structures that I don't fully understand. It involves "For" loops that contain only "skipping" instructions --- by which I mean If without Then; IS>(; and DS<(. The simplest example is probably :For(A,1,1000 :If 0:0 :End This program takes 25 ticks to run to completion. But if a right parenthesis is added after the "1000", the modified program --- one byte longer, and semantically equivalent --- runs in only 8 ticks! Or, the loop can be changed to include more than just a skipping command: :For(A,1,1000 :X :If 0:0 :End The program above runs in 11 ticks. It makes no difference which side of the "If" you put the "X". This effect seems to occur no matter whether the "skip" is taken or not; "If 1" or "If 0", it's all the same. Obviously, this has implications for using For loops to time complicated code snippets! If you repeat an "If" inside a For loop, but omit the closing parenthesis on the For command, you might wind up thinking that the command takes much longer than it really does. (I ran into this problem in an earlier version of this guide, trying to analyze "If pxl-Test(0,0:0" versus "pxl-Test(0,0:If Ans:0". It turns out that both commands run about equally quickly, the latter a little more slowly; but dumped into a parenthesis-less For loop, the former takes much, much longer due to the For-loop effect alone.) SECTION 3. Advanced control flow. This section deals with the less commonly used built-in control structures of TI-83 Basic, such as Goto and IS>(. SUBSECTION 3.1. Lbl, Goto, and Menu(. As mentioned in section 2.1, the "Goto" command uses "skipping" mode to search for its target "Lbl". It always searches the program from top to bottom, so earlier "Lbl" commands will be found first. (Duplicate "Lbl" commands are useless.) The "Menu(" command also uses "skipping" mode, also searching from top to bottom. Control flowing out of a menu behaves exactly the same as control flowing out of a Goto. SUBSECTION 3.2. IS>( and DS<( The "increment and skip if greater" and "decrement and skip if lesser" commands are extremely rarely used, for several reasons. The main reason is that they're obscurely named and buried at the bottom of the program menu; but one might be excused for thinking that they must be pretty efficient, or else why devote special commands to them? One would be wrong. IS>( and DS<( are slow compared to the more verbose combination of "\->\" and "If". Almost three times as slow, in fact. So their disuse is justified --- never use IS>( or DS<( for their intended purpose if speed is what you want! However, the commands are sometimes smaller. You can save one byte by replacing :A-1\->\A with :DS<(A,0 as long as A is guaranteed to remain non-negative (meaning the jump never happens); and you can save four bytes by replacing :A+1\->\A :If 0:End with :IS>(A,A:End This optimization illustrates one of the amusing properties of the increment operators: Like "For", they evaluate their second parameter before performing the increment. Therefore, "IS>(A,A)" will always jump, and so will "DS<(A,A)". Even more oddly, the increment operators check their first parameter for existence before evaluating (and thereby bringing into existence) their second. Whereas :DelVar A0\->\B :IS>(B,A:End runs without complaint (because B exists), :DelVar A0\->\B :IS>(A,B:End produces "ERR:UNDEFINED"! (See also section 3.3.) If the effect of the jump would be to run off the end of the program (for example, if the ":End" were removed from the first program above), the result will be "ERR:SYNTAX", whether or not the jump is actually taken. SUBSECTION 3.3. Comparison of "DelVar" and "0\->\". In the first version of this guide, I recommended the use of "DelVar A" over "0\->\A" in all cases; since then, my position has picked up a few nuances. The two-byte "DelVar" command is often no smaller than "0\->\"; it is often slower; and it has pitfalls for the unwary. Still, it has its place in optimization. The DelVar command is sometimes smaller. As mentioned in section 1.2, the "DelVar" command doesn't need a terminating newline or colon, so we can replace :0\->\A:0\->\B:0\->\C with :DelVar ADelVar B0\->\C The latter program is two bytes smaller than the former. The DelVar command is sometimes faster, sometimes slower. Its behavior is practically the opposite of "0\->\" --- if variable A does already exist, then "0\->\A" simply stores a value, while "DelVar A" performs garbage collection, which takes a long time. But if variable A does not exist, then "0\->\A" performs memory management, while "DelVar A" does nothing. I haven't figured out how to get exact timings for the two "interesting" cases, but the following table may be helpful: We use the program Let W := DelVar, variable exists :For(A,1,800 X := DelVar, does not exist Y := 0\->\, exists :End Z := 0\->\, does not exist with various code snippets, as indicated. We observe the following timings, in seconds, and using "H" to indicate the overhead of the For loop: X+H = 4 Z+W+Y+H = 22 Y+H = 6 Z+X+W+H = 21 Z+W+H = 19 Solving the system (and fudging for measurement error) gives us X = 1.5, Y = 3, W+Z = 16. Therefore, "DelVar" when the variable already does not exist is twice as fast as "0\->\" when the variable already exists; and at least one of the other two possibilities is at least 2.5 times worse than that, but we can't tell which. Finally, there are some pitfalls to using DelVar. In most contexts, if you try to evaluate a non-existent scalar variable on the TI-83, it will be silently created and initialized to zero. For example: :DelVar A :Disp A prints "0". However, there are some exceptions to this rule which can trip up the unwary programmer who relies on DelVar to clear variables: The "For" command expects its counter variable to exist each time the loop's "End" is reached. The "IS>(" and "DS<(" commands expect their first parameter to exist. (See section 3.2.) SUBSECTION 3.4. Comparison of "DelVar" and "ClrList". The TI-83 also provides a command called "ClrList", accessible via the Catalog. "ClrList L1" behaves like "0\->\dim(L1", except that it also detaches any formula that had been attached to L1. (As detailed in section 6.1, "0\->\dim(L1" has no effect if L1 is pinned to a formula.) As with "0\->\dim(L1", ClrList doesn't completely delete the list. It leaves behind a useless zero-element list, which takes up 9 bytes in RAM. ClrList requires a real list name; "ClrList A" is a syntax error, not a synonym for "ClrList \L\A". (See section 7.2.) ClrList takes one byte fewer than DelVar, although that advantage goes away if the colon after DelVar is removed. All in all, I find there is no reason to prefer ClrList --- use DelVar instead. SECTION 4. Subroutines. One of the most important ways modern programming differs from programming as it was in the long-distant past is that modern programmers make a lot of use of subroutines; that is, smaller programs that fit together to make up a coherent whole. For example, if I were writing a poker game, I would need to display a lot of cards to the screen. I could write down some code to display one card, and then cut-and-paste it into every situation in which I needed a card displayed; but that would be tiresome, and it would also bloat the program with a lot of duplicated code. A better solution is to write the code only once, and then package it as a subroutine using one of the following three methods. These methods all have different advantages and disadvantages, which I will try to make clear. Which method you choose will depend on your particular circumstances. Before getting into the details of implementing subroutines, let's stop to consider your options when it comes to temporary storage. Subprograms, especially external ones, are often in need of temporary variables that are "safe" (that contain no data from the main program). For reasonably safe variables, try: The italic \n\, on the independent-variable key. delta-X and -Y, and XFactor/YFactor (and other window- and zoom-menu variables). Theta is a good counter variable for loops. X and Y. These two variables are overwritten by some graphing commands, including "ClrDraw" and many operations involving Y-vars (Y1, Y2,...). Thus, the user should never expect that a program won't overwrite these variables, and you should feel free to use them --- except when you're trying to use graphing commands too! You can create a new user-defined list, such as \L\A or \L\MYPRG, and use the list elements as temporary storage. Many calculator RPGs store status information in a list, and some games keep a high-score list on the calculator this way even when the game itself isn't running. Ans. See section 4.5. SUBSECTION 4.1. External subroutines. "External subroutines" are the simplest kind of subroutine: separate programs that are called from within the main program. To call another program, use its name; to return from a called program, execute a "Return" command or simply "fall off the bottom" of the program. Here's an example, using code from sections 2.2 and 5.2 to let the user manipulate a smiley-face sprite on the graphics screen: prgmMANIP :10\->\A:Ans\->\B :Repeat K=45 :A-2(K=25)+2(K=34\->\A :B-2(K=24)+2(K=26\->\B :prgmZSMILEY :Repeat Ans:getKey\->\K:End :prgmZSMILEY :End prgmZSMILEY :"101000000100010111\->\Str1 :1 :While Ans :Pxl-Change(A+int(.2Ans),B+5fPart(.2Ans :inString(Str1,"1",Ans+1 :End :Return The external subroutine prgmZSMILEY draws the sprite on the screen, and prgmMANIP calls that subroutine twice: once to draw the sprite and once to erase it. You can pass information to external subroutines in essentially two different ways. In that example, we passed the player's X and Y coordinates in the variables A and B, so variables are one way to pass information --- and certainly the most common! However, we can also pass information in the "Ans" pseudo-variable. This usually costs some extra space in the subroutine, but it can save some space and time in the calling program: we can write :0:prgmZHELPER instead of :0\->\A:prgmZHELPER Whole lists of parameters can be passed in "Ans", too: :{1,2,3,4,5:prgmZHELPER SUBSECTION 4.2. Recursion. The next logical step from the last section is to ask, "Well, if we can call other programs from within our main program, can we also call our main program itself?" The answer is "yes," TI-83 Basic programs can make use of recursion. It is generally a bad idea to try to use unbounded recursion in Basic, because your programs will run out of memory. For example, prgmBADIDEA :Disp "GUESS MY NUMBER :Prompt P :"LOSE! :If P=7:"WIN! :Disp "YOU "+Ans :Input "PLAY AGAIN? ", Str0 :If "Y"=sub(Str0,1,1 :prgmBADIDEA If you play this game long enough, it will crash with the message "ERR:MEMORY". It runs out of memory because each recursive call opens a new "stack frame," a hidden data structure in the calculator's RAM that costs approximately 16 bytes. If your calculator has 1600 bytes of free RAM, you won't be able to answer "Y" to prgmBADIDEA's prompt more than 100 times before seeing a memory error. This is why unbounded recursion is a bad idea. However, if you can ensure that your program never recurses more than once or twice before returning and closing the open stack frames, you will find recursion a very useful tool. See, for example, the very next part of this guide: section 4.3. As a side note to the above, it is generally good practice never to use the "Stop" command. If you can manipulate the program so that a normal termination is a "fall off the end", good for you; you saved two bytes. If not, use the "Return" command instead of "Stop", as it helps if you later want to execute the program from a shell. SUBSECTION 4.3. Internal subroutines. Consider the following reworking of prgmMANIP from section 4.1. prgmMANIPR :If \pi\=Ans:Then :"101000000100010111\->\Str1:1 :While Ans :Pxl-Change(A+int(.2Ans),B+5fPart(.2Ans :inString(Str1,"1",Ans+1 :End :Return :End :10\->\A:Ans\->\B :Repeat K=45 :A-2(K=25)+2(K=34\->\A :B-2(K=24)+2(K=26\->\B :\pi\:prgmMANIPR :Repeat Ans:getKey\->\K:End :\pi\:prgmMANIPR :End The external subroutine prgmZSMILEY has been tacked onto the front of the main program, and shielded from it by the lines :If \pi\=Ans:Then ... :End Notice that whenever prgmMANIPR recursively calls itself, it sets Ans equal to pi; however, it's unlikely that the user would set Ans to pi before running the program. (And even if he did that once by accident, the program would just draw one smiley and then stop; if he ran the program again from that point, it would work normally again. If you use internal subroutines, it's a good idea to tell your users that, in a README file, so they don't get confused this way.) Now, some caveats: If we try running :{1,2,3:prgmMANIPR the program tries to compare a list to a scalar and then branch on the resulting list, which isn't meaningful in Basic. So we see "ERR:DATA TYPE" and the program crashes. This is more problematic! We can fix this little glitch by changing the first line to :If \pi\=Ans(1:Then If "Ans" is a list, then "Ans(1)" is the first element of that list. If "Ans" is a scalar, then "Ans(1)" is simply "Ans" times the constant 1. Either way, the result is a scalar, and the expression type-checks. Again, we're relying on the assumption that the user won't be calculating with pi right before running our program. I know of no general-purpose expression that will handle not only lists and scalars, but strings and matrices too. Therefore, internal subroutines are very fragile and should be used with care. The major advantage of internal subroutines is that the whole program resides in a single file. Therefore, the user's program menu isn't cluttered, and only one file needs to be transferred in order to give the program to someone else. SUBSECTION 4.4. "Gosub" with Goto and End. Most computer Basic languages have a command named "Gosub" which acts like calling an external subroutine in TI-83 Basic, but instead of jumping to the top of some other program, jumps to a label within the main program itself. Returning from the internal subroutine (via the command "Return", naturally) returns the instruction pointer to the line following the original "Gosub". TI-83 Basic isn't that sophisticated. However, we can use what it provides to simulate "Gosub" in at least two ways, besides the simplistic "internal subroutine" method of section 4.3. First, if our subroutine is called from only a few places (which is generally the case), we can write something as simple as :Disp "MAIN PROGRAM :0\->\T:Goto S :Lbl 0 :Disp "MAIN AGAIN :1\->\T:Goto S :Lbl 1 :Disp "BACK IN MAIN :Stop :Lbl S :Disp "IN SUBROUTINE :If T:Goto 1 :Goto 0 This is simple, and since it doesn't use any control structures except Lbl and Goto, it's easy to "plug into" any existing program without lots of confusion about which "End" matches up to which "If". That method is perfectly satisfactory for all practical purposes. However, let's consider an impractical purpose: Suppose we want to call our internal subroutine from more than 1406 places in the program! (1406 is 37+37*37, the number of labels available in Basic. Labels can be composed of one or two characters, and those characters can be A-Z, 0-9, or \theta\.) In that case, we must fall back on a less-than-elegant control flow hack. :Disp "MAIN PROGRAM :For(L,-1,0:If L:Goto S:End :Disp "MAIN AGAIN :For(L,-1,0:If L:Goto S:End :Disp "BACK IN MAIN :Stop :Lbl S :Disp "IN SUBROUTINE :End :Disp "N.B. When we want to call the subroutine, we invoke a "For" loop, which executes exactly twice. (The subroutine had better not modify L!) On the first iteration, we "Goto" the subroutine, and at the end of the subroutine encounter the "End" that sends us back to the top of the loop where we began. The second iteration of the loop does nothing, and at the end of that iteration we drop out of the bottom of the loop and continue executing in the main program. (Just one iteration of the "For" loop wouldn't be enough; we'd drop out of the loop at the location marked "N.B." in the program. For the same reason, we can't simply use a "Repeat 1" loop, or "While F" where the subroutine sets F to zero, or any similar such trick --- they all fail to return to the right location in the main program.) Perhaps surprisingly, the two methods take exactly the same amount of memory --- the "labels" method actually takes more, if two-character labels are involved! And the "for-loop" method should generally be more efficient, since its return journey is taken in "jumping" mode instead of "skipping" mode. Thus, the "for-loop" method may be preferable in many cases. However, there is a major pitfall with the "for-loop" method! Once the loop has been started, there's no way to get that "End" off the interpreter's stack. Therefore, the following program has a bug. prgmSUBBUG :While 1 :Lbl C :Disp "ENTER A NUMBER :For(L,-1,0:If L:Goto S:End :Ans\->\A :Disp "ANOTHER NUMBER :For(L,-1,0:If L:Goto S:End :Ans\->\B :" NOT_ :If A=B:"_ :Disp "YOUR NUMBERS","ARE"+Ans+"EQUAL :End :Stop :Lbl S :Prompt N:N :If Ans\>=\0:End :Disp "NO NEGATIVE","NUMBERS PLEASE :Goto C The programmer intends "Goto C" to send the control flow back to the top of the main loop, and it does --- but the interpreter is still looking for an "End" to end the "For" loop that called the subroutine! Therefore, the next time the program encounters the "End" that normally matches the main loop's "While 1", it assumes that's the end of the "For" loop and continues on its way. The next command is a "Stop", and the program terminates. The sensible solution is to avoid using the "for-loop" method with complicated control flows. But a solution to this particular problem is fairly simple. prgmSUBFIXED :While 1 :Lbl C :Disp "ENTER A NUMBER :For(L,-1,0:If L:Goto S:End :Ans\->\A :Disp "ANOTHER NUMBER :For(L,-1,0:If L:Goto S:End :Ans\->\B :" NOT_ :If A=B:"_ :Disp "YOUR NUMBERS","ARE"+Ans+"EQUAL :End :Stop :Lbl S :Prompt N:N :If Ans<0:0\->\L :End :Disp "NO NEGATIVE","NUMBERS PLEASE :Goto C The fixed subroutine increments the loop control variable L so that if the input number was negative, the subroutine's "End" causes control flow to fall out the bottom into the "No negative numbers please" message. At that point, the "For" loop's address is cleared off the interpreter's stack, and the program behaves as it's supposed to. SUBSECTION 4.5. Making use of Ans. The special variable Ans can really speed up a program if used properly. Storing to Ans is done just like on the home screen; quote the value directly and it's automatically stored to Ans. Normal stores also store to Ans, so the getKey loop :Repeat K:getKey\->\K:End can be replaced with :Repeat Ans:getKey\->\K:End I find the latter more aesthetically pleasing, although both snippets have the same size and execution time. By judicious use of a list stored in "Ans" and parallel list operations, an entire program can be created that uses no permanent data space whatsoever. For example, consider the following pi-calculating program, which expects the number of iterations to be provided in "Ans": prgmPI1 :DelVar S :For(L,1,Ans :S+1/(4L-3\->\S :S-1/(4L-1\->\S :End :Disp 4S It can be rewritten without any variables by letting the first and second elements of the new "Ans" list stand for L and S, respectively, and counting down with L instead of up: prgmPI2 :{Ans,0 :While Ans(1 :{Ans(1)-1,(4Ans(1)-3)\^-1\-(4Ans(1)-1)\^-1\+Ans(2 :End :Disp 4Ans(2 The resulting program uses no variables (and hence erases none of the user's own data). Readability does suffer, however, and programs may be slightly longer or even slower with this particular optimization; there is a tradeoff between preserving the user's data and running efficiently. (prgmPI1 takes 16 ticks to do 400 iterations; prgmPI2 takes 18. prgmPI2 is also 11 bytes longer.) Conversely, to store the result of an expression R into the variable X without affecting the user's value of Ans, you can use :For(X,R,0 :End if R is greater than zero, or repeat the expression if R may take any value: :For(X,R,R-1 :End SECTION 5. Graphics. Many Basic programs make use of the graphics screen, and make use of it poorly. However, there's no reason a well-written Basic program can't do graphics as well as a program written in assembly. Efficiency starts with the right setup. Generally, any program that uses graphics will start with the lines :0\->\Xmin:1\->\\DeltaX\ :-62\->\Ymin:1\->\DeltaY\ :AxesOff:FnOff :PlotsOff (\DeltaX\ and \DeltaY\ are found in the Vars->Window... menu.) The command "GridOff" may also be desired, but I don't put it in my own programs, since I've never had any reason to turn the grid on in the first place. Once these commands have been executed, the point (X,Y) in the calculator's coordinates will correspond exactly to the pixel (-Y,X) on the screen. Therefore, the program can mix and match coordinate-relative commands such as "Pt-On" and "Line" with screen-relative commands such as "Pxl-On" and "Text". Whenever possible, use the "Horizontal" and "Vertical" commands instead of "Line"; they are faster and shorter. Unfortunately, while "Line(A,B,C,D,0)" erases the given line, there is no such "erase" feature for "Horizontal" or "Vertical". The most efficient way of getting dark pixels onto the screen is almost always to use "Text(". See section 5.3 for an elaboration on this theme. The next three subsections deal with "sprites," the graphics term for little bitmapped pictures that don't change shape, but move around the screen or appear and disappear in different places. Sprites are a big part of many games, in Basic and in assembly. SUBSECTION 5.1. Bitmap sprites. The following program draws a Tetris block on the screen, using the "box" feature of Pt-On. The block is represented by a bitmap --- literally, the bits of the integer B correspond to the boxes that appear on the screen. :402\->\B :ClrDraw :For(I,1,3 :For(J,1,3 :.5int(Ans :If fPart(Ans:Pt-On(20-3J,3I-20,2 :End :End In this example, 402 decimal is 110010010 binary, so the block that appears on the screen is shaped like this: 11. .1. .1. If this example isn't clear yet, try different values for B, and see how the bitmap changes. For example, B=341, or 101010101 binary, yields an "X". Bitmap sprites are versatile --- the TI-83's floating-point numbers can store up to 43 bits without losing precision, which means you can have bitmaps that are 6 pixels by 7 pixels! If you're dealing with bitmaps that big, though, you will probably find the following program useful: prgmBITCNVT :Disp "ENTER BITMAP :Input "> ",Str0 :0:For(A,1,length(Str0 :2Ans+expr(sub(Str0,A,1 :End :Disp Ans The main problem with bitmap sprites is that they're very slow to display. No matter whether you use Pt-On or Pxl-On (or Line or Text) to turn on the pixels, you still have to loop over the bitmap, dividing by 2 at each iteration, and that takes time. SUBSECTION 5.2. Bitmaps using "inString(". "Axcho" describes an interesting variation on the bitmap theme that doesn't require so many explicit loops: :"110010010\->\Str1 :ClrDraw :inString(Str1,"1",1 :While Ans :Pt-On(20+9fPart(Ans/3),-20-3int(Ans/3),2 :inString(Str1,"1",Ans+1 :End Here, the "While" loop only iterates four times, once for each of the 1 bits in the bitmap. The other five of the original bitmap code's loops are done inside the "inString" function. The problem with this method is that it must do extra calculation to figure out where to plot each point --- the "fPart" and "int" expressions are not cheap. Therefore, while the running time of the original code was dominated by the loop overhead, this code's running time gets worse and worse as more "on" pixels are added to the sprite. For 3x3 sprites, the two methods perform approximately the same when the sprite has three "on" pixels; the string-based method is better for one or two pixels and worse for more. For 5x5 sprites, the two methods perform approximately the same when the sprite has eight or nine "on" pixels. SUBSECTION 5.3. Textsprites. (The name "textsprites" is due to "Axcho".) As mentioned before, the most efficient way of getting dark pixels onto the screen is generally to use "Text(". Therefore, it makes sense to look for a way of encoding arbitrary bitmaps in "Text(" commands. Textsprites is just such a method. Consider the following program to draw a user-specified suit symbol (diamonds, hearts, clubs, or spades) on the graph screen: :Prompt S :"-:):- " :If not(S:"'\^2\S\^2\' " :If S=1:"eQ[Qe " :If S=2:"e([(e " :ClrDraw :For(A,1,length(Ans :Text(1,1+A,sub(Ans,A,1 :End Each of the characters in the "Ans" string encodes a vertical bar of five pixels. Thus, any 5-by-N sprite can be encoded in just N characters, and drawn with lightning speed, as long as there exist characters with the right pixels along their right-hand margins. The following table is a complete listing of characters on the TI-83, indexed by textsprites value: 00000 (space) \-\ \^3root\ \dot\ 00001 . , 00010 \cross\ \sqrt\ 00011 / J \DeltaX\ 00100 { < - + \^-1\ 00101 00110 a c d e \sigma x\ 00111 \box\ 01000 ^ \pi\ 01001 S 1 z \<=\ \n\ gcd( 01010 : * = \!=\ 01011 01100 v \degrees\ 01101 s 01110 ( C G 0 \theta\ w 01111 A 6 \10^(\ n p \p-hat\ r u \L\ min( fpart( 10000 T ? \ln(\ \^T\ 10001 ) ] } > I 3 \Sigma x\ 10010 \^2\ \^3\ 10011 Z 2 7 10100 \^xroot\ 10101 \xbar\ \ybar\ \>=\ 10110 10111 i invNorm( 11000 Y 11001 11010 \chi^2\ 11011 X 11100 ' V 4 " 11101 ! 5 9 11110 Q t \^r\ 11111 [ B E F H K L M N O P R U W 8 b \E\ \>Dec\ \F\ \N\ Notice that four rows are empty. (The 00101, 10110, and 11001 are filled in by c-cedilla, e-grave, and I-circumflex, respectively, on the TI-83+, but even on the TI-83+ there is reportedly no way to achieve 01011.) To obtain those columns of pixels, you would have to supplement the normal textsprites routine with a few calls to Pxl-On or Pxl-Off. Besides the unobtainable pixel columns, another disadvantage of the textsprites method is that it requires one or two (for 11010, as many as seven!) "scratch" columns to the right of the sprite, which will get overwritten with garbage and then erased with spaces when the sprite is completely drawn. One way to use textsprites in "close quarters" without having the garbage overwrite important screen data is to use StorePic: prgmTSPIC :ClrDraw :Pxl-On(1,1:Pxl-On(1,7 :Pxl-On(7,1:Pxl-On(7,7 :StorePic 1 :"-:):- " :For(A,1,length(Ans :Text(1,1+A,sub(Ans,A,1 :End :RecallPic 1 Try the above program with and without the RecallPic command. Without RecallPic, one of the pixels around the diamond sprite is erased by the sprite's garbage columns and never redrawn. Recalling the saved picture redraws the missing pixels correctly. SUBSECTION 5.4. Plotsprites. Another method of displaying sprites is to put the coordinates of the sprite's pixels in a couple of lists, and set up a scatterplot of them. Drawing the scatterplot will draw the sprite; and, conveniently, the TI-83 automatically redraws scatterplots when any change is made to the graph screen. Examine the following program, which uses complex arithmetic to move and spin a "spaceship" sprite on the screen. The complex number J represents the ship's offset, and the integer R represents its rotation in quarter-turns. The sprite's ten pixels are stored in the complex list L1, and pulled into the real lists L2 and L3 when it's time to update the sprite. prgmPLOTSPRT :{-2-2i,-2i,2-2i,-1-i,1-i,-1,1,-1+i,1+i,2i\->\L1 :Plot1(Scatter,L2,L3,\dot\ :47\->\Xmax:-Ans\->\Xmin :31\->\Ymax:-Ans\->\Ymin :AxesOff:ClrDraw :DelVar J0\->\R :Repeat K=45 :real(2J+i^RL1\->\L2 :imag(2J+i^RL1\->\L3 :Pt-Off(E7,0 :Repeat Ans:getKey\->\K:End :i(K=25)-i(K=34)+(K=26)-(K=24 :J+i^RAns\->\J :R+(K=21\->\R :End Plotsprites are generally very slow, but might be more convenient than the alternatives in some cases. Their other major disadvantage is that any use of plotsprites necessarily overwrites at least one stat plot and at least two lists. SUBSECTION 5.5. Screen scrolling with "sub(". (This method is due to Basicoderz. They used it to great effect in the side-scrolling bomber game WAR2.) prgmSUBSCROL :"THIS IS A TEST_ :Ans+Ans+Ans :Ans+Ans\->\Str1 :1\->\I :While 1 :I+1-80(I=80\->\I :Output(1,1,sub(Str1,1+int(.5Ans),16)+sub(Str1,1+int(Ans/3),16 :End SECTION 6. List and matrix operations. Existing lists and matrices get truncated when the program stores into an expression involving "dim("; non-existing structures get created and zeroed automatically. Therefore, explicit zeroing of most structures is unnecessary. However, note that resizing a matrix by assigning to "dim(" takes much longer than simply assigning values to an existing matrix, and garbage-collecting an existing matrix takes about as long as the "Fill(" command. The best way to resize and zero a matrix which may or may not already exist is therefore to delete and reallocate it: For iters Reps Code Ticks Bytes 800 1 DelVar [A]{A,B\->\dim([A] 19 13 800 1 {A,B\->\dim([A]:Fill(0,[A] 19 15 800* 1 [[0\->\[A]:{A,B\->\dim([A] 110* 16 (The starred* line was performed 160 times and the resulting time multiplied by 5. As you can see, resizing a matrix twice inside a loop is a terrible idea.) SUBSECTION 6.1. List equations. It is possible to assign a string to a list. This has the effect of associating that string with the list as an equation, similar to a spreadsheet formula. :"L1\^2\\->\L2 :{1,2,3\->\L1 :Disp L2 :{4,5,6\->\L1 :Disp L2 A list which has been "pinned" in this way will show up in the [STAT]->Edit... display with a bullet next to its name (which the manual calls a "formula-lock symbol"). Assigning a new value to a "pinned" list, or assigning it the empty string :"\->\L2 will "unpin" its formula. One amusing aspect of pinned lists is that changing their dimensions has no effect --- given L2 pinned as above to a three-element list, "2\->\dim(L2)" will set Ans to 2, but have absolutely no effect on the dimension or value of L2! SUBSECTION 6.2. Matrix operations. In certain cases, a 2-by-n matrix can be replaced with a list of complex numbers, where the real part represents column 1 and the imaginary part column 2. This technique might save space, or it might not, but it does let you create and delete your own data structures, rather than overwriting one of the ten named matrices [A] through [J]. The TI-83's matrix operations can be very useful, though. The row+(, *row(, and *row+( functions let you quickly compute linear combinations of matrix rows (and, in combination with the "matrix transpose" operator, columns). The randM( function lets you create a random matrix, and the functions Matr>list( and List>matr( let you convert between list and matrix representations, with a bit of difficulty. SUBSECTION 6.3. Hunt the Wumpus dungeon generation. The abilities to shuffle lists (see section 9.3) and to convert lists into matrices are at the heart of the game "Hunt the Wumpus", in which the "dungeon" is laid out with rooms at the vertices of a regular dodecahedron. In this pared-down example, we'll have only eight rooms, at the vertices of a cube. (The original 20-vertex code comes from my game WUMPUSR, available on ticalc.org.) :seq(X,X,1,8\->\D :Ans+1\->\L1 :Ans-2\->\L2 :rand(8\->\L3 :SortA(L3,LD :1\->\L1(8 :8\->\L2(1 :{4,7,6,1,8,3,2,5\->\L3 :For(X,1,8 :LD(L1(X\->\L1(X :LD(L2(X\->\L2(X :LD(L3(X\->\L3(X :End :SortA(LD,L1,L2,L3 :List>matr(L1,L2,L3,[D] This code snippet sets up the adjacency matrix [D], which is an 8x3 matrix whose entries [D](I,1), [D](I,2), and [D](I,3) are the numbers of the rooms adjacent to room I in the dungeon. Lines 1, 4, and 5 give LD a random permutation of the numbers 1 through 8: the permutation by which the room labels will be swapped around. Lines 2, 3, 6, 7, and 8 set up lists L1, L2, and L3 with the rows of the adjacency matrix, according to this diagram: 1------2 |\8--7/| 23456781 | | | | [D]^T is 81234567 (e.g., 1 borders 2,8,4) |/5--6\| 47618325 4------3 The next step is to shuffle the room labels according to LD. The next six lines accomplish the relabeling, and the final line puts L1, L2, and L3 into matrix [D] so the lists may be used for something else. The result is to produce a shuffled adjacency matrix; for example, if the shuffled LD were {4,7,6,1,8,3,2,5}, we'd have 7------5 |\3--8/| 74682153 | | | | [D]^T is 65827314 (e.g., 1 borders 7,6,2) |/6--4\| 21768435 1------2 SUBSECTION 6.4. Finding poker hands. (This method is due to Chris Senez's TI-83+ program TXHOLDEM, and used in my prgmZPKEVAL, both available on ticalc.org.) Suppose L1 contains a list of cards in a poker game, where 1+iPart(L1(I)/13 represents the I'th card's face value and 13fPart(L1(I)/13 represents its suit (as an integer between 0 and 3 inclusive). Then the following section of code prints "ONE PAIR" if L1 contains exactly one pair of cards with matching values. :DelVar [A]{5,14\->\dim([A] :For(I,1,dim(L1 :13\^-1\L1(I :1\->\[A](1+int(Ans),1+int(13fPart(Ans :End :[A] :For(I,1,4 :row+(Ans,I,5 :End :Matr>list(Ans\^T\,5,L2 :If 1=sum(L2=2)+2sum(L2>2:Disp "ONE PAIR This method can be expanded in a systematic way to check for all sorts of poker hands. The most interesting thing about this method is that it doesn't depend on the length of L1 --- in fact, L1 is not even consulted after the initialization of [A]. Therefore, a single subroutine (see section 4) can be written to evaluate all kinds of poker hands, from ordinary five-card hands to the seven- and eight-card hands of stud poker. SECTION 7. Input hacks. SUBSECTION 7.1. Comma-separated list. :Disp "ENTER A LIST :Input "> ",Str0 :expr("{"+Str0\->\L1 SUBSECTION 7.2. Cheating with "Input". As mentioned in section 1.2, it's possible to leave off the "L" when referring to a user-defined list variable. Unfortunately, this quirk of TI-83 Basic opens a loophole in some game programs that rely on "Input" or "Prompt" to get values from the user. Consider the following (somewhat contrived) program: prgmFLAWED :randInt(0,9\->\G :G\->\S :Disp "GUESS MY NUMBER :Prompt G :"YOU LOSE! :If G=S:"YOU WIN! :Disp Ans It is possible to "win" every time simply by entering a list such as {1,2,3} at the prompt, instead of a single number. (Entering a string also works, as implied by section 6.1; the string will become associated with LG.) There is no elegant way to keep the user from "cheating" like this. The only options are to write the program so that any trickery will only hurt the player (for example, by setting the input variable to a value representing "invalid" before invoking "Input" or "Prompt"); or to eschew "Input" and "Prompt" altogether. SUBSECTION 7.3. Illegal string characters. It is impossible to write a program that stores \->\ or " (the quote character) into a string. However, it is possible for the user to enter a quote character as part of a string via the "Input" command! If the user does that, nothing bad happens; the string simply contains a quote character. You can even apply "expr(" to the string and get back a string, if the quotes match up. SUBSECTION 7.4. The empty string. ***WARNING!*** This section contains programs which will clear your TI-83's memory! Do not experiment with any of this section's code unless you have recently backed up all your favorite programs to your PC! It is not possible to explicitly produce an empty list or a 0-column matrix in TI-83 Basic. However, it is possible to create an empty string. :"\->\Str0 The empty string is a strange creature. It can be assigned and displayed just like a normal string, but it has the following odd behaviors (some dependent on ROM version): Command Calc, ROM v1.08 Virtual TI --------------- ---------------- ----------- length(Str0 0 Str0\->\Str1 Behaves normally Disp Str0 " Output(1,1,Str0 " Output(8,16,Str0 " Text(1,1,Str0 " Text(1,94,Str0 " Text(1,95,Str0 ERR:DOMAIN, as usual Str0+"X ERR:INVALID DIM "X"+Str0 ERR:INVALID DIM Str0+Str0 ERR:INVALID DIM inString(Str0,"X",1 ERR:INVALID DIM inString(Str0,"X",0 ERR:DOMAIN inString(Str0,Str0,1 ERR:INVALID DIM inString(Str0,Str0,0 ERR:DOMAIN sub(Str0,1,1 ERR:INVALID DIM sub(Str0,0,1 ERR:DOMAIN sub(Str0,1,0 ERR:DOMAIN sub(Str0,0,0 ERR:DOMAIN Str0\->\L1 Behaves normally; L1 is created but empty; any access produces ERR:INVALID DIM Str0\->\Y1 Clears Y1 Equ>String(Y1,Str1 Gives the empty string expr(" Clears the RAM ERR:INVALID "":expr(Ans Clears the RAM Hangs expr(Str0 Clears the RAM Hangs Therefore, it is possible to write a TI-83 Basic program that crashes the user's calculator and erases its memory! Needless to say, this is not a good idea, even as a prank. See section 10.3 for more on RAM-erasing crashes. SECTION 8. Two-calculator communications. (The most extensive coverage I've found of TI-83 link programming is Frank Schoep's "The secret to linking two TI83's in TI BASIC", available on ticalc.org as 83BSLINK.TXT.) There's not much written about programming the TI-83's link in Basic, and that's because very few programs have actually been written to use the link port (and those that have been written are often buggy or fragile). In short, the TI-83 doesn't have much support for two-calculator programming, and the support it does have (the "GetCalc" command) is essentially broken. The TI-83 provides three link-port commands. Two of them, Get( and Send(, are only documented for use with the CBL and CBR, specialized hardware devices used in educational settings. That leaves GetCalc(. GetCalc(X), where X can be the name of any variable, sends a request across the link attempting to receive the value of the given variable. If the link isn't connected, or if the other calculator is busy, the request will fail, and X will retain its old value. What does it mean for the other calculator to be "busy"? If the progress indicator is ticking away normally, then the calculator is busy. GetCalc requests will only succeed if the other calculator is paused; at a menu; waiting for Input or Prompt; or not running a program at all. A successful GetCalc request will cause the requestee's calculator to become un-paused, if it is currently paused. Consider the following pair of programs: prgmREQR :42\->\A :While 1 :A\->\B :GetCalc(A :"SUCCESS :If A=B:"FAILURE :Disp Ans :End prgmREQEE :DelVar A :While 1 :1+A\->\A :Pause "ABC :Disp "DEF :End If prgmREQR is started running, it will display "ABC" and then pause. When prgmREQEE starts running on the second calculator, if all goes well, prgmREQR will start spewing out alternating "ABC" and "DEF" lines, while prgmREQEE spews line after line of "SUCCESS". Terminating prgmREQEE will cause prgmREQR to start emitting "FAILURE". SECTION 9. Other hacks. SUBSECTION 9.1. Integer into string. For small integers, the best way to convert an integer into a string is undoubtedly :Prompt N :sub("0123456789",N+1,1 :Disp "YOU ENTERED "+Ans+". This method should only be used with numbers under your control, since it will cause a program error (ERR:DOMAIN or ERR:INVALID DIM) if you try to feed it input such as 10 or 2/3. For somewhat larger integers, a couple of variations on the above "indexing" method exist. Obviously, you can use 'int' and 'fPart' to extract each digit and convert it as above, if you know the maximum number of digits your number may have. :Prompt N :"YOU ENTERED_ :If int(.1N:Ans+sub("123456789",int(.1N),1 :Ans+sub("0123456789",10fPart(.1N)+1,1 :Disp Ans+". (In the above program, "_" represents a space character. Notice that while the calculator recognizes the empty string as a valid string, it produces an ERR:INVALID DIM if you try to concatenate anything onto it. Therefore, we must start building our string with a non-empty "seed" --- in this case, the whole text of our message, "YOU ENTERED...") A second variation places no limit on the number of digits entered. :Prompt N :{0\->\L1 :For(I,1,1+log(N :10fPart(.1N\->\L1(I :int(.1N\->\N :End :". :For(I,1,dim(L1 :sub("0123456789",L1(I)+1,1)+Ans :End :Disp "YOU ENTERED "+Ans (Notice that this program, as written, produces an ERR:DOMAIN on the input "0". However, this is easy to patch up, either with an "If X=0:Then... Else...", or by replacing "1+log(N" with "1-2not(N)+log(N+not(N" or some such obscure expression. These methods can be adorned in various obvious ways to handle negative numbers, numbers with a fixed number of decimal places, numbers in bases other than 10, and so on. However, there is a clever way to make the calculator do the work for us, if you don't mind overwriting the user's Y-vars. :Prompt N :{0,1\->\L1 :{0,N\->\L2 :LinReg(ax+b) Y1 :Equ>String(Y1,Str1 :Disp "YOU ENTERED "+sub(Str1,1,length(Str1)-3)+". This method uses the calculator's linear regression solver to store a formula in Y1 which contains the number N; for example, if the user entered 42, the equation in Y1 would be "42X+0". Then we can pull out the substring before the "X" in that string, and we have a string representation of our original number. This method automatically takes care of decimals and negative signs. SUBSECTION 9.2. Decimal into fraction. (This method is due to Kenneth Hammond and Mikhail Lavrov.) Interestingly, executing "Disp expr("X>Frac")" displays a fraction on the home screen, but "Text(1,1,expr("X>Frac"))" displays a decimal. Therefore, the only way to display a fraction on the graph screen is to convert it to numerator and denominator first, and then display them independently. :Ans\->\X :{1,abs(X :Repeat E-9>Ans(2 :Ans(2){1,fPart(Ans(1)/Ans(2 :End :iPart({X,1}/Ans(1 :Text(0,0,Ans(1),"/",Ans(2 The last two lines are essentially equivalent to :iPart(Ans(1)\^-1\\->\D :iPart(XD\->\N This algorithm fails on many inputs, such as 61000/99, but it may be useful in some applications. (For example, you could convert the decimal to a string using this method and another method, and pick the shorter of the two results.) SUBSECTION 9.3. Shuffling a deck of cards. :seq(I,I,0,51\->\L1 :rand(52\->\L2 :SortA(L2,L1 This code is certainly the shortest and simplest way to shuffle a deck of cards, represented by a list of integers between 0 and 51 inclusive. However, most of its time is spent computing random bits that never get used. Therefore, it is significantly faster to re-use different parts of the same random real numbers, like this: :seq(I,I,0,51\->\L1 :rand(26 :augment(Ans, fPart(AnsE4\->\L2 :SortA(L2,L1 In fact, it becomes even faster (though possibly at the cost of some randomness; I have not investigated this) to perform the augmenting step twice: :seq(I,I,0,51\->\L1 :rand(13 :augment(Ans, fPart(AnsE3 :augment(Ans, fPart(AnsE6\->\L2 :SortA(L2,L1 This is the watershed point, though; adding yet another "augment" (with rand(8) or rand(9)) takes longer. But we can get even faster shuffles by taking the generation of random numbers into our own hands. (Of course, there's probably a tradeoff between speed and randomness here, as well, but I haven't noticed any problems with the following algorithm in practice, for calculator card games.) :13\->\dim(L2:rand :For(I,1,13 :fPart(97Ans\->\L2(I :End :augment(L2,fPart(L2E3 :augment(Ans,fPart(AnsE6\->\L2 This method takes 24 bytes more than the best of the "rand(13)" methods, but it runs slightly faster. I'd recommend the above method if speed is paramount, and the "rand(13)" method otherwise. Here is a table of timings for all the operations discussed in this section. The first entry in the table is for the sorting operation alone; the other entries are for setting up the list L2 alone, and don't include sorting. Operation Ticks/15 iterations Bytes ----------- ------------------- ----- SortA(L2,L1 5 7 rand(52\->\L2 21 8 rand(26, augment 14 16 rand(13, aug x2 11 24 rand(9, aug x3+dim 13 43 rand(8, aug x3+dim 13 39 rand+loop52 12 22 rand+loop26+aug 10 40 rand+loop13+aug x2 9 48 There is yet another way to speed up shuffling. Consider a video poker game, in which at most only nine cards are shown between shuffles. To shuffle the whole 52-card deck before each hand would be a waste of time. Instead, simply pick the nine cards you'll be using, and swap them to the top of the deck: :seq(I,I,0,51\->\L1 :For(I,1,9 :randInt(I,52\->\A :L1(Ans\->\B :L1(I\->\L1(A :B\->\L1(I :End SUBSECTION 9.4. The dbd( function. The TI-83 guidebook has this to say about the dbd( function: "Use the date function dbd( to calculate the number of days between two dates using the actual-day-count method. date1 and date2 can be numbers or lists of numbers within the range of the dates on the standard calendar." By "dates," the TI-83 means decimals in the form MM.DDYY or DDMM.YY, where the Ds, Ms, and Ys stand for digits. The calculator's "actual-day-count method" does indeed account for leap years. Because there are only two digits for the year, the dbd( function treats all dates as falling between January 1, 1950, and December 31, 2049. (In this range of years, every fourth year is a leap year, with no exceptions.) SUBSECTION 9.5. Miscellaneous control flow hacks. The following programs are self-contained snippets of code which can be inserted into a program without messing up the instruction pointer's "skipping" mode (see section 2.1). The "Disp" commands tell the input conditions under which they will be executed. :If A:Then :Disp "A :If B:Else :Disp "not(A) or not(B) :End :If A:Then :Disp "A :If B:DelVar XElse :Disp "A and not(B) :End :If A:If B:Disp "B or not(A) :If A:DelVar XIf 0 :Disp "not(A) In the following program, the "DelVar" hides the beginning of a While loop from the interpreter's skipping mode, but then the "IS>(" command inserts a "While" that is seen in skipping mode but never executed, since the jump is always taken. (The string "A and not(B)" is never displayed unless B becomes false while inside the inner loop.) :While A :Disp "A :DelVar XWhile B :Disp "A and B :IS>(Y,Y:While 1 :End :Disp "A and not(B) :End :Disp "not(A) or not(B) SUBSECTION 9.6. Miscellaneous timings. (For other timing tables, see sections 2.3, 3.3, 6, and 9.3.) The following tables compare different methods of achieving the same goal, with timing information given. The timings were measured in "ticks" of the calculator's progress indicator, using programs of the form prgmTEST :For(I,1, : :<...Reps times...> : :End The "Bytes" column gives the size of the tested line of code, in bytes, counting the newline. For iters Reps Code Ticks Bytes 200 15 max({A,B,C 27 8 200 15 max(A,max(B,C 22 8 200 15 max(A,B:max(Ans,max(C,D 37 13 200 15 max(A,max({B,C,D 34 11 200 15 max({A,B,C,D 32 10 200 15 max(max(A,B),max(C,D 30 12 200 15 max(A,max(B,max(C,D 29 11 200 15 max(A,max(B,max(C,max(D,max(E,F 45 17 200 15 max({A,B,C,D,E,F 44 14 200 15 int(A 11* 3 200 15 iPart(A 11* 3 Interestingly, "int(" performs at 10 and "iPart(" at 13 when A is 1 or -1, but both perform at 11 or 12 when A is pi or -pi; it even seems that "iPart(" has a slight edge in those cases. 200 15 -int(-A 15 16 5 200 15 iPart(A+.5 17 16 6 200 15 round(A,0 15 15 5 Left-hand tests performed with A=201; right-hand tests performed with A=pi. Notice that round(A,0) performs best; notice also that when A is negative, iPart(A+.5) is different from round(A,0). -int(-A) rounds A up to the nearest whole number, just as int(A) rounds A down to the nearest whole number. 200 15 \tan^-1\(Y/X 19 85 5 19 97 200 15 angle(X+Y\i\ 25 86 7 37 100* 200 15 \R>Ptheta\(X,Y 52 110 5 79 122* The left set of tests were performed with X=-300, Y=0; the right set were performed with X=1, Y=3. The upper set were performed in radian mode; the bottom set were performed in degrees mode. The two starred* tests were run with only 100 loop iterations, and the times doubled. Note that the method using inverse tangent fails when (X,Y) is not in the first or third quadrant, or when X=0; the other two methods have no such limitation. For iters Reps Code Ticks Bytes 50 15 int(9rand 26 4 50 15 randInt(0,8 24 6 50 15 int(99rand 26 5 50 15 randint(0,98 25 7 50 15 randint(0,E8 25 7 50 15 int(randE8 24 5 50 15 iPart(randE8 24 5 200 15 2\^-1\X 22 4 200 15 X/2 21 4 200 15 .5X 13 4 200 15 1/L1(1 24 7 200 15 L1(1)\^-1\ 23 7 50 15 7\xroot\e^(2 38 5 50 15 7\xroot\e\^2\ 30 6 50 15 e^(2/7 20 5 200 15 X\>=\1/2 26 6 200 15 X\>=\2\^-1\ 23 5 200 15 2X\>=\1 18 5 200 15 X\>=\.5 14 5 The following tests show that there is no reason to prefer > over \>=\ nor = over \!=\, and that comparisons to powers of 2 or other "round" numbers don't go any faster than comparisons to "random" numbers. 200 15 X\>=\256 14 6 200 15 X\>=\235 14 6 200 15 X\>=\0 14 4 200 15 X>255 14 6 200 15 X>234 14 6 200 15 X>0 14 4 200 15 X=42 14 5 200 15 X=0 14 4 200 15 not(X 11 3 200 15 X\!=\42 14 5 200 15 X\!=\0 14 4 200 15 not(not(X 13 4 200 15 XY 15 16 3 15 27 200 15 X and Y 16 16 4 16 16 The above pair of tests was performed with X=0, Y=0 (upper left); X=2, Y=2 (upper right); X=0, Y=e (lower left); and X=pi, Y=e (lower right). 200 15 X xor Y 16 4 200 15 X\!=\Y 17 4 The above pair of tests was performed with X and Y taking all four combinations of 0 and 1; neither computation showed any dependence on the values of the operands. 100 10 X=21 or X=45 9 10 100 10 sum(X={21,45 12 10 100 10 max(X={21,45 11 10 100 10 12=abs(X-33 7 9 SECTION 9.7. List of single-byte tokens. The following table lists all the tokens on the TI-83 that take only one byte of RAM. All other tokens take two bytes. Notably not on this list are \e\ (the constant 2.718...); any list, matrix, or string variables; lcm(, gcd(, randInt(, sub(, inString(, real(, expr(, SinReg, DelVar, GetCalc(, and G-T. 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ \theta\ ?!'".,:()[]{} \space\ \newline\ \->\ \(-)\ Ans \E\ \L\ \i\ \pi\ +-*/^=<> \!=\ \<=\ \>=\ and or xor not( nPr nCr getKey ZStandard rand >DMS ZTrig sin(, sin^-1( >Dec, >Frac ZBox cos(, cos^-1( BoxPlot Zoom In tan(, tan^-1( \^r\ Zoom Out sinh(, sinh^-1( \degree\ ZSquare cosh(, cosh^-1( \^-1\ ZInteger tanh(, tanh^-1( \^2\, \^3\ ZPrevious If, Then, Else \^T\ ZDecimal For, End round( ZoomStat While, Repeat pxl-Test( ZoomRcl Pause augment(, Fill( ZoomSto Lbl, Goto rowSwap( Text( Return, Stop row+(, *row( FnOn, FnOff IS>(, DS<( *row+( StorePic Input, Prompt min(, max( RecallPic Disp, Output( R>Pr( StoreGDB ClrHome R>P\theta\( RecallGDB Menu( P>Rx(, P>Ry( Line( Get( mean(, median( Horizontal Send( randM( Vertical SortA(, SortD( solve( Pt-On(, Pt-Off( DispTable seq( Pt-Change( PlotsOn, PlotsOff fnInt( Pxl-On(, Pxl-Off( Plot1 nDeriv( Pxl-Change( Plot2 fMin(, fMax( Shade( Plot3 prgm Circle( 1-Var Stats Radian, Degree Tangent( 2-Var Stats Normal, Sci, Eng DrawInv( LinReg(ax+b) Float, Fix DrawF QuadReg Horiz, Full int( CubicReg Func, Param abs( QuartReg Polar, Seq det( LinReg(a+bx) IndpntAuto identity( LnReg IndpntAsk dim( ExpReg DependAuto sum(, prod( PwrReg DependAsk iPart(, fPart( Med-Med \box\ \sqrt(\ ClrList \cross\ \cube-root(\ ClrTable \dot\ \xroot\ Histogram Trace ln(, log( xyLine ClrDraw e^(, 10^( Scatter SECTION 10. Non-Basic miscellany. ***WARNING!*** This entire section contains programs and experiments which will clear your TI-83's memory! Do not experiment with any of this section's code unless you have recently backed up all your favorite programs to your PC! SUBSECTION 10.1. Finding your ROM version. Pressing Alpha-S from the Mode menu (while not editing a program) will prepare the calculator to enter its "self-test" mode. You'll see the text "Enter Self Test? 1.O8OOO" appear on the screen --- possibly with a different number in place of "1.O8OOO". That number is the ROM version of your calculator. Pressing Enter from the "self-test" screen will clear your calculator's RAM. Pressing any other key (except 2nd or Alpha, which do nothing) will return you safely to the home screen. SUBSECTION 10.2. Hexadecimal assembly code. It is possible to write Z80 assembly code on the calculator directly, in hexadecimal, if you know what you're doing and are willing to risk a crash. The command "Send(9prgmXXX" (yes, with the number 9 between "Send(" and the name of the program) will attempt to interpret prgmXXX as a sequence of hexadecimal digit pairs (i.e., bytes) possibly separated by newlines. The bytes are directly interpreted as Z80 machine code, so you must know what you're doing if you expect anything useful to happen (and want your RAM to stay uncorrupted). Following the hexadecimal machine code must come three things: an "End" command, four hex digits which are typically zeros, and another "End" command. The TI-83 checks for the presence of these commands and will give "ERR: SYNTAX" if they're not there. For example, the following program makes text display as white- on-black until some other event (e.g., viewing the PRGM menu) changes it back. (This program is harmless.) prgmRVIDEO :Send(9prgmZRVIDEO :Disp "HELLO WORLD prgmZRVIDEO :FDCB05DEC9 :End:0000:End Notice that the machine-code program ends by returning to the place in prgmRVIDEO from which it was called, so "HELLO WORLD" is displayed in reverse video. Thus it is possible to write snippets of assembly code and call them from Basic programs. However, for guidance on how to write the actual machine code, you'll have to consult an assembly- language guide; TI-83 assembly language is much more difficult to experiment with than TI-83 Basic. SUBSECTION 10.3. Operating system glitches. ***WARNING!*** This entire section contains programs and experiments which will clear your TI-83's memory! Do not experiment with any of this section's code unless you have recently backed up all your favorite programs to your PC! (Most of this section's glitches were documented by Kenneth Hammond on United-TI's forums, and verified by myself on my own TI-83. See section 7.4 for some harmful glitches related to the empty string.) There is a glitch in at least some versions of the TI-83 ROM, which has supposedly persisted all the way into the TI-84+. Using 2nd-Rcl to recall a string (variable or Ans) containing the name of matrix [H] will actually give the string with all instances of "[H]" replaced by "lcm(". Another glitch causes the assignments of certain strings (for example, "[H]1"\->\Str0 ) to display as long sequences of gibberish (in that example, "*row(8>Dec"). However, the strings still receive the right values (as can be seen by evaluating expr(Str0) in that example). Both of these glitches are reproducible on VTI. I believe both of these glitches to be harmless, but I won't make any guarantees. Another definitely harmful glitch, which is not reproducible on VTI, can be produced on the TI-83 this way: Set an equation (e.g., Y1) to 0. Before viewing the graph, at the home screen, evaluate Equ>String(Y1,Str1) several times. Then press "Y=". Y2 will be filled with random gibberish. Attempting to down-arrow onto Y2 will cause a dramatic crash and require you to pull the batteries. (This glitch has been tested on ROM version 1.08. It reportedly has different effects on different versions of the ROM.) A note on dramatic, battery-pulling crashes: You may pull your batteries, and then find that when you replace them, pressing the On button doesn't seem to do anything. Don't panic! Your calculator is (probably) not fried; all that's happened is that its contrast level has been reset to 1 along with the rest of its factory defaults, so you can't see anything on the screen. Press 2nd-Up until the contrast level returns to normal. SECTION 11. References. This guide would not have been possible without the excellent work of a lot of TI-83 Basic programmers over the past ten years, some of whom I contacted while polishing this guide. There are also a number of Web sites archiving tutorials and forum posts that may contain tips and hacks not mentioned in this guide. SUBSECTION 11.1. Contributors. Thanks to "Axcho" and Kenneth Hammond (a.k.a. "Weregoose") for their extensive comments on a draft of this guide. http://www.ticalc.org/archives/files/authors/61/6138.html http://weregoose.unitedti.org/ Thanks to Mikhail Lavrov (a.k.a. "DarkerLine") for his comments on sections 5.3 and 7.4. http://mpl.unitedti.org/ Thanks to Chris Senez for his comments on section 6.4. http://www.ticalc.org/archives/files/authors/88/8835.html Thanks to Frank Schoep for his comments on section 8. http://www.ticalc.org/archives/files/authors/24/2436.html Thanks to Anders Tiberg for drawing my attention to the quirk described in section 2.5. http://www.ticalc.org/archives/files/authors/38/3835.html An early reference to textsprites (section 5.3) is found in the forum post http://www.unitedti.org/index.php?showtopic=25&view=findpost&p=32601 The technique was independently discovered by Kenneth Hammond, and also used in Mikhail Lavrov's game "Donut Quest" for the TI-83+, available at http://www.unitedti.org/index.php?download=46 The fraction-converting code in section 9.2 is taken from http://www.unitedti.org/index.php?showtopic=3892 SUBSECTION 11.2. Web sites. "ticalc.org", an archive of programs, tutorials, and the like. http://www.ticalc.org/ "United-TI", a site with a large archive of forum posts. http://www.unitedti.org/ "TI Freak Ware", a site with forums and a collection of tutorials. http://tifreakware.calcgames.org/tutorials/83p/b/ "Weregoose"'s Web site has an archive of code snippets. http://weregoose.unitedti.org/routines.html