/HEAD>

The First Book of Tiny BASIC Programs

Copyright (C) 1981 by Tom Pittman

scanned & OCRed by Lee Hart, Richard Peters, Chuck Yakym, others
Last edit Herb Johnson Jan 16 2020

CONTENTS

 

 

Chapter 1 -- INTRODUCTION

Tiny Basic was originally conceived by the dragons at People's Computer Company as a simple language to teach kids programming. It turned out to have applications far exceeding this modest object. So much so that I doubt that it was ever used much for the original purpose. The biggest use of Tiny Basic turned out to be its ability to make small computers programmable in something higher than binary absolute.

Yet, for all its lofty intentions, the language remains arcane, and meaningful programs are hard to write in it. Certain kinds of programs turned out to be much easier than was ever thought; small control applications. These programs are satisfied with 16-bit integer arithmetic and limited variable space. Thus variants of Tiny BASIC turned up in several 1-chip CPUs for precisely this purpose. I have included in this book a small program to illustrate this kind of application.

The main purpose of this book, however, is to make available to users of Tiny BASIC a broader selection of programs that are useful in the home and perhaps even (in the case of Tiny Calc) in a small business. The purpose of these programs is not just to entertain and do a job (for programs written in machine language will do that much better), but rather to demonstrate programming techniques so that after you have enjoyed the programs for a while, you can adapt the ideas to your own programs, and thus make your computer truly personal.

When I first conceived of doing a book of Tiny BASIC programs, I came up with maybe a dozen ideas. I figured I would spend a couple of weeks getting each one up. I should know better. Six weeks later the first program was still only half done, so I lowered my sights a little. That is why this is "The First Book of Tiny BASIC Programs". Some of those other ideas are still good, and it may be worth doing another book (perhaps next year? It seems to take about that long to get it all together). I guess it depends on how well this book is received. Many of the earlier users of Tiny BASIC have moved up to bigger and better BASICs or other computers.

1.1 -- For the Record

America is rapidly becoming a consumer-oriented society. To the extent that this protects us from unscrupulous vendors who knowingly sell defective products for a greater profit, I favor the trend. Unfortunately the laws to support this protection do not adequately distinguish between fraudulent negligence and unavoidable mistakes, probably because so many of us use the increasingly sophisticated technology to conceal the differences.

One important law in the United States controls exactly what may and may not be promised under the general term "warranty". Alas! the state of the art in computer software does not support the level of confidence required to make such promises as are required at a price you would be willing to pay for something as esoteric as a Tiny BASIC program. So (listen, all you lawyers), THERE IS NO WARRANTY, EXPRESS OR IMPLIED associated with any program in this book. The purpose of this book is expressly educational. If, after studying it or the programs in it, you know a little more about programming in Tiny BASIC, or about writing big programs such as "Adventure" or "Calc", the book has served its purpose. If you succeed in using Tiny Calc in your business, I'm pleased as peaches, but that is not the purpose of the program.

Now obviously, I do not stay in business by selling a bunch of junk. If you find any problems in any program I sell, I want to hear about it. I intend to correct all errors I know about, and if you are nice enough to tell me about them, I'll surely be nice enough to tell you what the fixes are.

Please notice also that some of the programs in this book bear that little circle (C) that means "Copyright". I spent a lot of time writing and checking out these programs, and I hope I can get some money for that effort. You may make copies for your own use. Especially this means that you can make a copy on tape or some such, so that you do not have to type the whole thing in again. What you are not allowed to do is make a copy for a friend. Now some of these programs are veerrry long, and there is not much incentive for both you and your friend to individually type them into your own computers. So, I will not sue anybody who gives a machine-readable copy of one of these programs to another person who has already bought his own copy of this book. I do not know the exact legal status of doing that, but I consider it to be "fair use".

1.2 -- Configurator

Each program in this book comes with a description of how to use it. One of the important parts of that description is how the peculiarities of your computer affect its operation. Several of the programs use the "peek" and "poke" USR functions, so they need to know where they are. Most of the programs in this book are written for ease of reading, and as such they take much more memory than they could if crunched to minimum space. It is rumored that most people have lots of memory ("memory is cheap" they say). I don't exactly believe it, so I've put in notes on how to crunch the big programs.

When I started to write these programs the only Tiny BASIC I knew they would run on was what I was selling for the 1802, the 6502, and the 6800. Since that time, an article in Byte announced the availability of a Z8 microcomputer with Tiny BASIC in its ROM. Since I know where that Tiny came from, I can confidently say that most of these programs will run on that computer also, with the indicated modifications. Except for the random number problem (see below), this is something on the order of three lines per program.

The details on how to modify (if needed) the programs to run in different computers is collected in a section for each program with the heading "Configurator".

There are a number of Tiny BASICs around that I did not write. One of the most popular a while back was Li-Chen Wang's Palo Alto Tiny BASIC. Because the source code was published, several variations of this appreared for various computers. Another independent Tiny BASIC came out of National Semiconductor, in the ROM of their one-chip CPU. Since I was not involved in these, I am not in a position to determine whether the programs in this book will run (or can be modified to run) on them. You are on your own.

1.3 -- Program Notes

Since this is an educational book, every program has a complete source listing with program sections labelled with REMarks. Also, every program comes with a section of the documentation labelled "Program Notes". These are a running description of the program, explaining how it works (to the extent it is not obvious from the listing), and sometimes why it was written the way it was. It is intended that these notes should help you understand the program enough to modify it. On other occasions I have been accused of writing cryptic code, so if you are still lost after reading these notes; well, at least I tried.

One thing I discovered while testing the Tiny Adventure game is that big programs run too slow. I came up with a way to speed up Tiny BASIC by about an order of magnitude. The patches to do this are in the Appendix. All the fix does is make correct programs run faster. There is a problem, however. If you have in your program in memory, any lines with only one character (there are no legal Tiny BASIC statements of only one character), then this may cause a GOTO to fail, even though the line number is there. If you are using a single period for debugging (as suggested in the Experimenter's Kit), you will have to go to two periods, or else leave the fix out. The fix takes a few extra bytes, and if your Tiny is in ROM, you are out of luck.

1.4 -- Random Numbers for the Z8

The Z8 Tiny BASIC does not have a built-in random number generator like the other three CPU versions that these programs are able to run on. For games, it is important to have this element of chance, so in the three programs in this book that use the random number function RND, Z8 users will have to substitute something that does about the same thing.

The easiest way to get something approximately random is to use one of the timer counts. T0 is used for the Serial I/0 (your terminal), and is unlikely to be very large (especially with high baud rates). I recommend setting up T1 with a very small prescale, counting continuously through from 99 to 0:

You only need to set this up once at the beginning of your session (after reset). Then for each occurrence of a random number function call of the form

you need to replace it with the following calculation:

When the random number routine is called several times in a row without any input statements, the effect is likely to be less random but probably still acceptable.

 

Chapter 2 -- DATA LOGGER

There have been a flurry of articles in the recent press about using Tiny BASIC in industrial applications. Here is a simple program that demonstrates some of the control application capabilities of Tiny.

The application here is a simple time-keeping function, with the simultaneous logging of data input changes. Clock functions are not part of Tiny BASIC, and accuracy here requires external hardware. In this program I assume that a hardware clock is providing a periodic interrupt once every millisecond (1000 times per second). Other periods are easily accomodated by setting a different constant in variable A (line 540). A machine language routine is needed to respond to the interrupt and tweak Tiny BASIC. This example is coded for the 6800, but you should be able to read the comments to adapt the idea to your own CPU.

The program is very simple. In a tight loop, the variable U (which is counting the interrupts) is compared to variable T (which tracks it under software control). When the difference exceeds 1000 (or whatever the interrupt rate is), then one second has passed, and the clock time is updated. Also within that tight loop, the input value is compared to its previous state, and if it changes, the new state is printed out with the current time. Printing the state of the data may (I should say probably) take more than one second, but the timer can get as much as 32 seconds ahead of the software clock and still catch up.

I/O on the 6800 and 6502 is memory-mapped; the Z8 I/O is register-mapped, but Tiny sees the registers as memory. For the 1802, you will need to use the built-in I/O USR function at (Cold Start)+38 (i.e. hex 0126): Input takes an argument from 9 to 15, and output is from 1 to 7 (like the N field of the INP and OUT opcodes). Thus for the four different processors, only the setup differs.

2.1 -- Operation

Running this program is not as easy as the other programs in this book. First you must select the input port where the data is to be logged. The hardware clock must be set up to interrupt the CPU (fortunately, Tiny does not care if it is being interrupted, as long as the interrupt service routine operates correctly). An interrupt service routine must be set up in memory to respond to the hardware clock interrupts, and to increment memory locations 00AA-00AB (Tiny BASIC variable U; in the Z8 it is in a different location). Then finally the program can be (loaded and) run.

The first thing the program does is ask for the time of day. After this is entered, the program begins logging data. Here is a sample of the program output:

2.2 -- Configurator

I'm sorry that I cannot tell you everything you need to run this program on your computer. Every computer has different I/O hardware, and all I can do is give suggestions. The best thing to do is read the program listings.

This program is intended not so much to be useful by itself, as to give you ideas on how you can use Tiny in control applications.

2.3 -- Program Notes

There is not much to say about this program that is not obvious. The initialization is done at the end with a subroutine so that it does not take space at the front (which makes GOTOs slow down).

Since speed is not one of Tiny BASIC's strong points, the main loop of the program is designed to be fast so that the clock will keep good time. Thus the main loop is near the front (so GOTOs go faster), and the frequently used constants are preset into variables (which compute faster than constants).

The binary printout of the data byte is a little obscure. In this case there is only one byte to print, so it is moved to the left byte of variable R (by multiplying it by 256). The sign of variable R is now in the leftmost bit of the datum, so if R is negative then the bit is necessarily a one. Otherwise, it is zero. This is printed (variable B), and then the byte is moved over (left shift by adding R to itself), so that the sign tests the next bit. This is repeated eight times for the whole byte.

If printing a log line has taken more than one second, then the very next time through the time test will trip, and bump the seconds counter. If the clock got several seconds behind, then it will catch up slowly, one second per iteration. Thus it is important that the main loop be much less than one second in total transit. If you have a very slow Tiny (like 1802), you may have to handle seconds in tens.

The variable D retains the last known value of the input data. It is initialized with the one's complement of the initial port value, so that the first time it is compared, all bits are likely to be wrong, forcing it to print.

2.4 -- Interrupt Service

Let's assume that interrupts are vectored somehow to the memory location corresponding to the label INTS below. I assume also (though probably incorrectly for your clock hardware) that clock interrupts are self-clearing. That is, no extra instructions are needed to clear the interrupt after responding to it. The only function of the service routine is therefore to increment memory. Note that the 6800 Interrupt saves the entire CPU state and the Return restores it. Other CPUs may need to do that in the service routine explicitly, as in the 1802 example.

I have used the IEEE proposed standard mnemonics and syntax in the assembly language, but the hex object code is the same 6800 or 1802 code that you know and love.

One thing to watch for is the byte sex of the variable U. In the 6800 and the 1802 (as well as the Z8), the most significant byte is in the low address; in the 6502 it is reversed.

Careful programmers will want to consider what happens in the event of a conflict of access to variable U. One important result of this consideration is that only one routine ever modifies it. I could have written the program so that the interrupt incremented it, and the main routine bumped it back down. Then there would be the problem of what to do if halfway through the statement that bumped it down, an interrupt came to increment it. This is unsafe code.

The question still remains; what if an interrupt increments U while the main program is fetching it to look at it? If the interrupt occurs either before or after the two bytes have been both fetched, then this is no different than if it occurred remotely; there is no problem. It is when the interrupt occurs between the fetching of the two bytes that poses the potential problem. Two cases need to be considered.

Suppose after fetching one byte of the variable, the main program is interrupted and the low byte is incremented, but no carry occurs. Then, if the main program has already fetched that byte, the result is the same as if the interrupt occurred after both fetches. If the main program fetched the high byte first, then the effect is the same as if the interrupt had occurred before both fetches. Again there is no problem.

Now suppose that the interrupt service routine propagates a carry out of the low byte of variable U into the high byte, between the fetches of the two bytes in the main program. This means that the main program will get the upper half of the old value and the lower half of the new value. Both halves have changed. The high byte is one greater, and the low byte went from 255 to 0. So, depending on the order in which the two bytes are fetched, the number seen by the main program could be 255 too large or 255 too small. What are the consequences of this error?

If the number is too small, then the main program will fail the comparison in line 120, and go around for another try, at which point the same fault is unlikely. This is no more serious than if the interrupt came marginally too late to affect the test in this iteration. Again, no problem.

If the number is too large, then there is one chance in three (given that the interrupt occurred in this precise window, and that a carry was propagated) that the test in line 120 will pass too soon. That is, U will look like it has a value of 1023 when it actually is in transition between 767 and 768. The test will pass, and the seconds counter will be bumped approximately one quarter second too soon. Considering that every time data is logged, the resolution is only to the second, and that the internal (software) clock may lag the true time by several seconds after logging the data, I do not consider a lead of 255 milliseconds to be serious. It is important, however, to consider the problem. It is not adequate to note simply that the probability of this happening is one in a million and therefore ignore it.

Another question to consider; what happens if the count in U overflows? The answer; nothing. It will overflow every 65.5 seconds, and so also will the value in T. There is no problem with comparing them, since we are actually not comparing U and T, but the difference between them, which will normally be a small number between zero and maybe a thousand or so (perhaps several thousand, if the software clock falls behind because of logging). It is important to realize, however, that if this difference grows past 32K, it will go negative and the software clock will suddenly lose 65 seconds. You need to consider the capacity of your data port in the light of the interrupt rate of the hardware clock and the baud rate of your terminal.

2.5 -- Program Listing

2.6 -- Interrupt Service Routine

 

Chapter 3 -- The KINGDOM of EUPHORIA

Imagine that you are the king of the ancient European Kingdom of Euphoria. Each year your loyal ministers report to you on the state of your holdings, and you are responsible for the welfare of your peasants, to give them grain to eat, protect them from the ravages of war, etc. And of course, you want to increase your wealth and influence.

As a game, this program does not need much introduction. It is an ideal pastime to keep the kids busy, since the rules are simple. It is not particularly easy to get rich in this game, however, so you might find it challenging yourself.

The hardest thing about this program is figuring out how much to feed the peasants, plant, or keep. I recommend playing once or twice with no other objective than to find out what the parameters are. Then you will not be so disappointed when three quarters of your population dies off from starvation. After that you can get down to developing a serious strategy.

Because of the limitations in Tiny BASIC, if you try to hoard or deal in too much grain (over 30,000 bushels), strange things may happen. I tried to check for most hazards, but let's face it: This is Tiny BASIC.

This game has been around in various versions for a long time. This one is neither original with me, nor particularly well-designed. I only translated it into Tiny BASIC. It is, however, interesting to play for a while, and it has been known to keep kids (and some adults!) busy for hours.

3.1 -- Configurator

This program is pure Tiny BASIC (no USR calls), and should run as-is. As listed, it is 5791 bytes long. If you are hard up for space, remove lines 3600-3950, which only give instructions to the player. You can also do the normal crunching things like remove blanks, LET and THEN, shorten PRINT, take out REMarks, etc. Z8 users should note that this game does use the random number generator.

3.2 -- Program Notes

I translated this program from an "Extended BASIC" version of the program I had laying around. I've had that program stashed away for some eight years. I got it from a friend, who got it for his minicomputer, but I don't know where it came from originally. There was no Copyright notice on it or in it. (There is a proverb or something about people who live in glass houses throwing stones.)

The original program was full of arrays and PRINT USING statements, but on analysis, it became evident that the arrays were only used as scalars: except for the initialization FOR loop, every array reference had a constant subscript. Substituting ordinary (Tiny BASIC) variables was natural. As for the PRINT USING statements; well, their only function was to line up the numbers into neat columns. As you see, I abandoned that.

As with all significant Tiny BASIC programs, the first task was to see if the 26 variables will do the job. I always try to get the data organized for a program before starting to write it. In this case, I made a list of all the variables in the original program, cross-referenced by usage, then tentatively assigned Tiny BASIC variables to them. Two or three of the variables were used to hold running sums. I was running out of variables, so I replaced their usage with the actual sums. I also packed two variables into one (the current year, and the seven-year locust cycle). After all that, it turned out that one of the variables I thought I needed, I didn't, so there is a spare (variable I). Most of the data in this program are integers, but a few of the calculations involved percentages and fractions. I scaled these calculations, so that they could be done in Tiny's integer arithmetic.

3.3 -- Variables

Variable Y is packed so that the remainder mod 7 is the year the locusts will hit, and Y/7 is the current year. This is tested in the rather obscure IF expressions in lines 2340 and 3010. Basically,this compares the remainder mod 7 of Y to the remainder mod 7 of Y/7. Remainder is evaluated by the formula

So the test wants to establish

A little algebraic manipulation gives

Note that in this analysis we do not apply certain obvious algebraic rules to the division (such as collapsing it with the multiply), since (mathematically) "A/B" really means "IntegerPart(A/B)".

Please don't ask me to explain the probability calculations for the war -- I only copied the algorithm. The rest of the program is pretty obvious.

3.4 -- Program Listings

 

Chapter 4 -- FLASHCARD - A Math Drill

They always say that a computer can help educate your children. This is a noble goal, but most of us never get any of the round "tuits" that we need to actually pull this off (as in "I'll write that program when I get a round tuit"). With Flashcard, some of that excuse disappears, and you have to think of better excuses to hoard your computer to yourself.

Flashcard is a graded arithmetic drill program, designed to drill the arithmetic skills of elementary school children. It is intended to relate to the level of difficulty being taught, so that the problems presented are neither too hard nor too easy. It does however, require a certain amount of hand-eye coordination (to use the keyboard and display). Also, since it is written in Tiny BASIC, it is not particularly bullet-proof. This latter problem may be best solved by a special keypad with the 10 number keys and <return>. Or, use the program as inspiration to write your own version in assembler with all the protection.

Flashcard lets you choose from one of fourteen levels of difficulty, according to the skill level of your child. These begin with one-digit sums and continue through products and dividends of two digits. All problems are presented with two operands; no multiple sums are included. The levels are as follows:

The problems at each level of difficulty have been chosen for a good distribution in the range, to the extent that the random number generator in Tiny BASIC allows (it is known to have consistent patterns: sorry about that).

Several categories of errors are specifically tested for, and appropriate responses generated to suggest to the student the nature of the errors. In particular, errors in carry/borrow, digit transposition, and "closeness" are tested. Numbers radically out of range are also rejected.

Each problem is presented up to three times, with special praise heaped on the correct response the first time. One of a variety of responses is selected each time to help prevent the drill from becoming tiresome.

A note of disclaimer: I am not a professional educator, nor do I have elementary school age children. The levels of difficulty were suggested to me by a teacher working in these grade levels. The text of the responses was entirely my own doing, and you will have to establish the appropriateness of the various responses for your children, as well as the quality of the problems. When I tried this on some school-age kids, the problems seemed to be the right difficulty but often they misunderstood the responses. You may have to reduce the "cuteness" of the responses to correspond to the reading level of your students.

4.1 -- Operation

This program is pretty much self-explanatory. It is pure Tiny BASIC, so no information about the host computer is necessary. The first question requests a level number for the run. Problems are displayed either in the one-line form or operands one above the other, depending on a program parameter (see the Configurator section). Examples:

Ten problems are presented and a statistical summary is given, then another problem set is presented. The score on each subsequent problem set is compared to a running average to note improvement. This repeats indefinitely. The only way to stop or change level of difficulty is to error off (type in something illegal, like a period) and re-run.

4.2 -- Configurator

As this program is pure Tiny BASIC (no USR calls), there is not much to configure. It does use the random number generator, however, to generate problems and select responders.

Those using this program on a video display terminal will probably prefer to keep the problem presentation in the classical operand-over-operand form. Users with only a one-line display or a hard-copy terminal (all that paper!) may prefer the problems to be presented on a single line. This is easily accomplished by setting variable V in line 40 to equal 1.

As listed, the program is 5556 bytes. If you are short on memory, you may wish to reduce each response set down to a single short sentence (with no choices). This requires that the selector statement also be modified to a simple GOTO (or GOSUB).

Each set of messages is collected into a range of statement numbers comprising one block of 100. For example, the 3900s are the messages chosen when the student fails the third attempt. In general, the message selector is a computed GOTO (or GOSUB) which adds a random number multiplied by 20 to the base message line number. To eliminate all but the first message in case of three failures, line 3320 must be changed to remove the RND call:

A few more bytes can be saved in the cases where it is a GOSUB (rather than GOTO), by removing all the messages from the message block (e.g. remove all lines 3900-3950) and replace the GOSUB by a simple PRINT:

The lines that need to be modified for the various message sets are

4.3 -- Program Notes

The overall structure of this program is fairly simple. A problem is generated by the routine associated with the selected level of difficulty, then it is printed and the answer typed in is compared to the various expected responses and the score tallied. This is repeated ten times, the summary printed and it begins again.

The problem selection is done by a computed GOTO on variable L (the level number). Une number 200 corresponds to level 1; 400 to level 2, and so on to line 2800 for level 14. The operands of the problem, the operation, and the correct answer, as well as several supposed wrong answer syndromes, are set up in program variables, so that from line 3000 a single routine can print all problems and evaluate all responses. From line 3800 to the end of the program are only the quips f rom which are selected a response to the student's answer.

There are not many parts of the program that are difficult to follow. The few that are there are discussed in the next section.

4.4 -- Variables

4.5 -- Obscure Code

Line 220
This line is for the purpose of determining the function code in all cases except the review. Problem levels 1-5 all come here, but level 5 has already selected the function code. A similar expression occurs in line 1310.

Lines 1640 and 1840
These two lines determine what the result would have been if a carry or borrow was wrong (ie. if the problem caused it, D is the result of suppressing it; if the problem did not, D forces one). The expression inside the parentheses evaluates the sum (or difference) of the units digit only, which divided by ten gives the carry (0 or 1). Multiply by 20 and subtract from the correct result plus ten to get the incorrect result. This is most easily seen with a couple examples:
Lines 2600-2640
The selection of the operands for the general multiply problem bears a little explaining. First a provisional product is selected (weighted toward the high tens). Then a multiplier is chosen (with a slightly lower probability of multiplier being 1). This is divided into the provisional product to get the the multiplicand. The two factors are finally multiplied together to get the actual product.

Line 2810
This complicated expression selects one of four lines to go to, based on the four possible values of F: 1 goes to line 1600, 2 to line 1800, 3 to 2600, and 4 to 2400. Dividing F by 3 gives a zero if F is less than 3, one if 3 or 4. Zero effectively eliminates the right half of the expression from consideration. That half can therefore serve as a correction to the value computed by the left half, just in the case of multiply and divide.

Lines 2900-2980
This subroutine is designed to generate a non-linear distribution of random numbers (heavily weighted in the high end). This becomes the sum. A random number within this range is one of the terms, and the difference becomes the other term. The comment (line 2920) shows the distribution of sums. The subroutine is used to generate both digits of a two-digit sum with no carry, and the tens digits of the two-digit sum where carry is allowed. Differences use the same numbers, of course, but re-arrange them.

Lines 3270 and 3710-3740
If both the correct answer and the incorrect student response are greater than 9 (i.e. both have two digits), then perhaps the error is one of the two syndromes tested here. Perhaps the digits are reversed (line 3710), or perhaps one digit is correct and the other is wrong (line 3720 for left digit, 3730 for right digit).

Lines 3430-3450
Because Tiny BASIC cannot cope with fractions, the performance figure (as a fraction of problems answered correctly on the first try) is not directly representable. It is scaled here by 100 to overcome that limitation. This allows a 10% resolution and 327 correct results (at least 32 problem sets) before the scaled value overflows.

4.6 -- Program Listing

 

Chapter 5 -- TINY CALC - A Spreadsheet Calculator

Tiny BASIC is limited to 16-bit integers for its arithmetic (about 4.5 digits). This is inadequate for most financial computations, and in particular it is incapable of coping with decimal numbers (dollars and cents), percentages, and the like. Nevertheless an important function of computers in the home and business is financial computations. Tiny Calc lets you perform calculations to 12-digit precision and two decimal places, and saves its results to over eight significant digits. Thus Tiny Calc can carry out financial calculations to a result exceeding $1,000,000.00 accurate to the nearest cent.

A further requirement of financial calculations is that the same procedure must be carried out on a (possibly small) amount of data every day or week or month or year. The calculation usually involves many intermediate values, which for auditing purposes must be printed out. Human bookkeepers often do their calculations on ledger sheets with 40 or so lines of six or eight columns, so that the intermediate values show up as line items. Tiny Calc displays its results in the same format, so little or no readjustment is needed to convert bookkeeping methods. Up to 1791 items, organized in up to 199 lines (a.k.a.rows) of up to 9 columns each may be calculated and displayed by Tiny Calc.

Handwritten spreadsheets normally require only one or two calculations per item. Tiny Calc allows up to four, with facilities for extending this indefinitely.

Other features of Tiny Calc include optional rounding to whole numbers or one decimal place, optional non-printing of selected intermediate results, and the ability to "do the same thing as" some other item's calculation.

5.1 -- How It Works

A Tiny Calc spreadsheet consists of many items, organized into rows and columns, with one intermediate or final result per item. For example, the 1980 Federal income tax form 1040 has lines numbered from 1 to 66, generally with only one number on each line. Some of the lines used for intermediate calculations are offset to the left, so Tiny Calc might be set up with two columns to reflect this offset. A few lines have a), b), and c) parts, so you might wish to define three columns in all, and leave the unused items blank. If no calculation is specified for an item, nothing is printed there.

To set up Tiny Calc for a new job, you first specify the number of rows and columns to be calculated. You may also give the printout column headings and row labels. The number of rows and columns is specified by the variable L (for Last), which is the row and column number of the last item. For example, a form 1040 spreadsheet with 3 columns and 66 rows would set L=663, where the units digit (3) is the number of columns, and the digits to the left (66) are the number of rows. The highest number of rows allowed is 199. Too many rows, or zero rows, or zero columns are all errors. The last row and column limits are set in line number 3 of the Tiny Calc program:

At every occupied column position in every row, you provide a "calculation specification", and if desired, its rounding and printing qualifications. You may type these in any order, since they are entered with row and column identifications, and stored in memory together with the particular item.

Calculation specifications are coded as assignment statements (LET) in Tiny BASIC, as part of the Tiny Calc program. Each item sets up any or all of nine variables (letters A to I), which define the specifications for one item's calculation. When it comes time to calculate that item, these lines are called as a subroutine from the main part of the Tiny Calc program, which then uses the values to direct the calculation. You do not type in the actual arithmetic expressions that are used, but rather a sequence of operation-operand codes. Any variables that are not given values for a particular item are assumed to have a default value (usually zero, which stands for no operation). The order in which the variables are assigned in an item's specification is not significant, but the values given to the variables are.

The tens digit of the line number on an operation specification defines the column, and the digits to the left (hundreds, thousands) define the row. The units digit is used to separate the different specifications. Thus the following line is the 4th specification for row 12, column 3.

In general, there are two kinds of operations. One kind refers to another item for its operand value, and the other kind has the operand value encoded in the operation number. Both kinds are numbers, where the right-hand digit defines the operation. The digits to the left in the one case specify the row and column where the operand is to be found, and in the other case specify a percentage value to be applied.

Five variables A-E are used to specify the operations to calculate the value of an item. Variable A is assigned a number that specifies the first operation to be performed (or sets up the initial value). Variable B specifies the next operation; variable C the next after that, then D, and finally E is the last operation to be performed. If less than five operations are needed, the excess variables may be ignored. If more than five are needed, a subroutine call is required (see operation code 0 below).

5.2 -- Operators

There are ten Operators, numbered 0 to 9. The Operator is placed in the rightmost position of an Operation Code number. Operators 0 to 6 are used with a row and column to refer to some other item. Operators 7 to 9 are used with a percentage value.

A reference to a row and column is by number: RRC. The tens digit defines the column (1 to 9), and the digits to the left of that define the row (1 to 199). A row or column specified as zero (0) is taken to mean the same row or column as the item being calculated. If Row and column are both zero, it has special significance, and normally refers to the value of an entered constant (see below).

Percents are entered as percent and tenths: nn.n%, written as nnn. The tenths of % go in the tens digit of the Operation Code, the units of % goes in the hundreds digit of the Operation Code, etc. Thus to add 6.5%, the percentage code 650 is added to Operator 7 (see Add Percent, below), for a total Operation Code of 657. Note that percentages greater than 327.5% cannot be specified by this method, since that would require an Operation Code larger than the largest (Tiny BASIC) number, 32767.

The Operators:

1 = Add
Adds the specified item (row and column) to the current item. If this is the first operation (i.e. assigned to variable A), then the meaning is equivalent to "start with item..." If row 0 column 0 is specified, the value of the constant for this item is used. A row greater than that of the item being calculated, or a column greater than the item being calculated on the same row, is considered to be in error (since that item has not yet been calculated).

2 = Subtract
Subtracts the specified item from the current item's running value.

3 = Multiply
Multiplies the current item's running value by the the specified item.

4 = Divide
Divides the current item's running value by the specified item.

5 = Minimum
Compares the specified item to the current item's running value, and then sets the item's running value to whichever is smaller.

6 = Maximum
Compares the specified item to the current item's running value, and then sets the item's running value to whichever is larger.

7 = Add Percent
Calculates the specified percentage of the current item, and then adds it to the current item's running value.

8 = Subtract Percent
Calculates the specified percentage of the current item, and then subtracts it from the current item's running value.

9 = Multiply Percent
Replaces the current item with the specified percentage of it.

0 = Same As (Subroutine Call)
The calculation of this item follows the same procedure as the specified row and column. This should be the last item in the sequence, since any unprocessed operations (variables higher than the one assigned to this operation code) may be lost or erroneous in the context of the operations referred to. This operation need not result in the same calculated value as the referenced item, since any operations already done will have affected the initial value, and since any operations referring to row or column zero are understood to refer to the row or column of the item being calculated, not necessarily the same as the item where the specifications are.

The row and column specifications in the subroutine call operation are not as limited as for item value reference. There is no requirement that the item be lower in row or column number. In fact, it may be outside the computed range of the calculator entirely, just so long as there are specifications at the appropriate line numbers (if not, then Tiny BASIC will error off). Calculations specified for items outside the row and column boundaries of Tiny Calc specifications can only be referenced by a subroutine call. A reference to the row and column of the item being calculated (i.e. row 0 and column 0) is not meaningful. Row 0, column 0, operation 0 is therefore considered to be "no operation" (ignored).

The two main uses of operation zero are to save on entering duplicate calculations, and to enable the calculation of an item to have more operations than the five in variables A to E. Consider an invoice, where each line item is calculated by multiplying the quantity in column 2 times the price in column 3 to give the total price in column 4. Row 1, column 4 is given the whole calculation, and each successive row in the column is "the same":

In row 1 (lines 141 and 142) the row number is implied (zero); i.e. use the present row (row 1). Thus, when the same calculation is used in row 2 (line 241 has an Operation Code of row 1, column 4, operator 0 "same as"), the calculations will be the same as row 1, but using the values of the items from row 2.

5.3 -- Constants

Numbers are entered into Tiny Calc as constants. A constant may be as large as 32,767,999.99 though the final value of an item is limited to one tenth that value. The constant is entered in three parts, and any parts that are zero need not be specified. The three parts are the High part, in variable H; the Integer part, in variable I; and the Fractional part, in variable F. The High part specifies the thousands, for numbers greater than 999. The Integer part is normally 1 to 999, but can be as large as 32,767. The Fractional part specifies the hundredths (cents), but for small dollar amounts, the dollar part may also be specified in F for convenience. Thus F=1342 is the same as I=13 and F=42. Similarly, small thousands (to a maximum of 32,767) may be specified in the Integer part I. Negative constants are entered by making any of the parts negative. The following examples all define the same constant, -2,047.90:

If an item in a particular row/column position consists solely of the entered constant, then none of the Operation variables need to be specified. When variables A-E are all zero (unspecified) and a constant is specified (H, I, and/or F not zero), then the constant is taken for the item value.

If two or more operations specify a constant in the calculation of the same item, then the first operation uses the value of the specified constant (or zero, if none is specified), and all subsequent operations use the value zero. Thus a single item can be specified to be "not more than some number, and not less than zero". For example:

5.4 -- Print Format

Unless otherwise specified, all numbers are printed to two decimal places. This is easily overridden by assigning a value to variable G. If G=-1 the item is not printed. If G=0, 1, or 2, the item is printed with that many decimal places. If you specify 0 decimal places, no decimal point is printed. The latest value of G prevails, so if row 5 column 2 sets G=-1 (don't print) and then calls row 2 column 3 with Operation 0 (same as), and that item sets G=2, then the item in row 5 column 2 will be printed to two decimal places.

The value of an item is always rounded to the number of places printed (or to two places if not printed), so any reference to it from another item gets exactly the same value as printed. Rounding is by the "banker's rule": Round to the nearest unless there is a tie; ties are rounded up or down so that the last digit comes out even. Thus 12.345 is rounded down to 12.34 and 12.355 is rounded up to 12.36.

The column headers are specified with print statements in program lines 9 to 89. Each column item should take exactly ten positions (BASIC "comma tabbing" does not work here). In addition, each column header should leave space over the line labels. As many header lines may be printed as desired.

Row labels are defined as subroutines in the position of column 0 (i.e. line numbers ending in 00). If two-line row labels are desired, with the second line on the same printed line as the line items, then both lines of labels can be printed together. If a third line label is desired below the line of the line items, then the third line's PRINT should be on line numbers ending with 05 and have their own RETURN. If no label is to be printed below the line of numbers, the RETURN for the line labels should be on the line numbers ending in 05. In the example below, row 1 (lines 100-105) has only a one-line label, row 2 (lines 200-205) has a two-line label, and row 3 has a three-line label:

Notice that the part of the label printed on the line with the line items must end in a comma or semicolon. If a semicolon , then you must be careful to ensure that all line labels are exactly the same length, or the columns will not line up correctly. Ending with a comma makes it easier, since then the labels only need to be about the same length. Label lines above the number line should not end in either a comma or semicolon. Label lines below the line with the printed items should end in a semicolon, unless you want an extra blank line between lines. If the extra blank PRINT for the third line of a label is left out, the label will print on the right-hand end of the number line.

If all the lines are to have labels that extend below the item line (and none on the right end), or if double-spacing is desired, then these extra PRINTs may be omitted by setting the last row-column variable L negative. A negative value in L forces an extra PRINT at the end of each line of items, before jumping to line number xxx05.

5.5 -- Putting It All Together

To use Tiny Calc, first layout your spreadsheet on a piece of paper. Show where items will be printed, and what the row and column labels will be. Then for each item (i.e. each column of each row where there is a number), write down where that number comes from or how it is computed. By doing this ahead of time on a piece of paper, it is easier to clarify your thinking, and possibly change your mind about line numbers, etc. You should then load the Tiny Calc main program and its default line items. You need to load as many defaults as there are rows times columns specified in variable L; these must be loaded even if that part of your spreadsheet is to be blank. Then type in the specifications for your application, including any data values (as constants). Finally, type RUN, and sit back and relax or go get yourself a cup of coffee (Tiny Calc is rather slow)! It will stop after it finishes, or if it finds an error. In either case you may need to change some of the specifications if it did not do exactly what you wanted. Just retype the lines with the numbers or operations that need changing, then type RUN again.

5.6 -- Error Summary

5.7 -- Special Effects

Often it is desirable to create a running total, or otherwise make a computation that depends on an adjacent column or row. This is easy to do, by reference to its row and column number. However, if we want to use the same computation in several places without retyping the specifications every time, it would be nice to have a convenient relative notation. Its lack is a minor limitation of Tiny Calc, but we can get around it by appeal to the Tiny BASIC that underlies it.

The variable N normally holds the current row and column number, in the same format as L (but not negative). Thus N-1 refers to the previous column, and N-10 refers to the previous row. N*10+1 refers to the current row and column, for the operation ADD. Therefore, a running column total can be specified by

or equivalently,

As an example, Pascal's Triangle can be generated by setting column 1 of every row to the constant 1, and every other number except row 1 to the sum of the two numbers above and to the above left of it. Here is the complete specification for the first four rows:

5.8 -- Rounding

Tiny Calc is programmed to round results according to the so-called "banker's rule". This eliminates a tendency of numbers to creep up through repeated roundings. The rule is that when the quantity to be rounded is exactly halfway between possible answers the answer with the even last digit is chosen.

Consider a calculation (admittedly absurd): X+Y-Y+Y-Y+Y-Y. Let us say X is 10 and Y is 0.5 and we are rounding to whole numbers after each step. The result of this calculation is the original value with banker's rule rounding, but it creeps up to 13 with the usual "add 0.5 and throw away the fraction" rounding:

This example shows that banker's rule rounding (on the average) is likely to give answers more nearly correct. However, it is more complicated, and for this reason it is not always used in the real world. One important exception is in IRS forms, where the rounding method is specified as "Add 0.5". Therefore, for doing your income tax on Tiny Calc, you may want to disable the banker's rule. To do this, remove three lines from the program: 21350, 21380, and 21420. With this change, rounding will be strictly "Add 0.5 and Discard Fraction".

5.9 -- Data Work Space

All of the computed values in Tiny Calc are stored in memory by pokes (USR calls). As written, the area used is taken out of the GOSUB stack. This is a little slow, and if you have an available block of memory somewhere else, you can change it to use that without all the computation to allocate stack space. Four bytes are needed for every position, so for a 15x9 matrix the memory size needed is 15x9x4=540 bytes. Smaller arrays need less; larger need more.

To program your own data memory space, remove all the program lines from 20210-20330 and replace them with a single line

where nnnn is the decimal address of the first byte of the memory block.

5.10 -- Program Size

A typical Tiny Calc calculation needs about 30 bytes of program memory space for every position on the spreadsheet that you calculate; complicated calculations will need more. These bytes are used for the two or three lines of program statements required to specify the calculations to be performed. For example, an income tax calculation will require about 60x2x30=3600 bytes of program storage in addition to the Tiny Calc program. If you code it carefully with only one column, only half as much may be needed. This may still tax the size of your computer system, and further steps to reduce the program size may be needed.

One important way to get more space is to eliminate the unnecessary bytes of the executive program. Ten lines may be eliminated if you isolate a place in memory for the data (see above, Data Work Space). The program was written to be readable, with spaces, comments, etc. Remove all the comment lines, spaces, keywords LET and THEN, and shorten PRINT (important: do not remove spaces inside quotes in Print statements). Also remove the three lines that do banker's rule rounding (see above). This will save about 2700 bytes in all.

If you are really hard up for space, and are willing to be satisfied with only 19 rows (down from a maximum of 199), you can get 78 more bytes by taking the final zero off all the line numbers, then fixing all the GOTOs and GOSUBs to match (simply remove the final zero from every line with "GO" in it -- 71 in all). There are seven lines that need special handling, marked in the listing by two spaces after the line number instead of one. Three of these take an extra zero or two off in unobvious places (as shown below), and the other four should not be changed (take no zeros off).

With all these changes, the program should weigh in at a little under 4400 bytes (plus whatever is needed for row/column specifications). This allows enough space in an 8K memory to do a small spreadsheet (like the example following), but probably not enough to do your income taxes.

5.11 -- Configurator

Tiny Calc consists of two parts. The first part is the executive, and is necessary for Tiny Calc to work. The second part has the default row/column table definitions, and you only need to enter as much as you are going to use. If you have enough memory, you will generally put in all the defaults for as big a spreadsheet as you will ever want with the executive, then save them both together. For individual runs, the saved version is loaded and the specifications entered in over it. If memory is limited (and for most of us that is always true), you may want to make several default saves (say, a long narrow one, a square one, etc.). Row/column information outside the bounds set in variable L on line 3 are not used, but do not hurt anything. However, you must have row labels and row/column defaults for every position in the table within the bounds specified by line 3, or Tiny BASIC will stop on a missing line number. The default listing in this book only covers the first 15 rows; for more rows, you need to make up your own additional defaults.

Tiny Calc accesses its data by peek/poke USR calls. Line 98 defines the address for peek in your computer. It is printed as 276 (for a Cold Start of 256); you should replace that line with one appropriate to your system. Byte sex is determined automatically in lines 20210-20240, and is only used to calculate the actual address of the data area. If you supply an address, this calculation is unnecessary.

Four lines do the actual data fetch and storage. If variables M and P are set up correctly (line 98 above, and lines 20210 and following as printed, or your replacement), then these need not concern you. For the record, they are lines 21460-21470 and 24440-24450.

Z8 Tiny BASIC has different reference addresses, so extra modifications are required. Change the four references to "P+4" in lines 21450-21460 to "129", then set P=71 in line 98 and allocate your own data area (line 20210; remove 20220-20330 as described in the section on memory usage). If you want Tiny Calc to automatically allocate its data area out of the stack, then the following lines must also be replaced with the new code shown below:

5.12 -- A Business Example

Probably the best way to see how Tiny Calc works is to see a realistic problem done with it. Here is an imaginary sales tax budget for a small company doing mail-order business, with some taxable and some non-taxable revenues. The tax rate in this example is 6.5% and the businessman is required to make quarterly payments.

Six columns are required for this report; one each for taxable and non-taxable receipts, one for the gross, one to show the calculated tax, one to show the payments made to the government, and finally a running total of the taxes due (shown as a credit balance, which perhaps by "creative accounting" is mostly negative). Fourteen rows allow for one row for each month, a yearly total, and some space for neatness.

To run this example, first load the executive program, with the default row and column information for 14 rows and 6 columns. Then type in the following lines. Line 3 specifies the array size at 14 rows by 6 columns. Lines 9, 10, 40, and 70 define the header across the top. Notice that the column headings are carefully spaced in fields of 10 characters.

Now type in the row labels. Notice that each one ends with a comma, like the beginning of the header line (line 9). We will print no data on row 13, so its row label is extended to provide underlines for the columns.

Column 3 is the gross receipts; that is, the sum of the quantities in columns 1 and 2. This is specified for row 1 by the Add operation (units digit 1), columns 1 and 2 (tens digit), and "current row" (0 in the hundreds digit).

Column 4 calculates the tax on the taxable receipts in column 2. This is done by getting current row (0xx) column 2 (x2x), Add (xx1) in the first step (variable A), then multiplying (xx9) by 6.5% (65x) in the next step (variable B).

Row 1 column 6 is the difference of column 5 (add: 1) minus column 4 (subtract: 2). Row 2 column 6 adds to that the previous row's value. This is computed by the method mentioned under special effects (above), that is, N*10-100 with the Add code 1 (total N*10-99) for the first step, then do the same thing as row 1 column 6 (xx0 + 1xx + x6x = 160), namely, add column 5 of the current row (this time row 2) and subtract column 4.

Column 5 is for quarterly payments, which are first calculated in the fourth month (row 4). The payment is the running total due at the end of the previous month (column 6, previous row), or $50, whichever is greater. To get the running total, we could specify 361, but we want this same specification to be used also for the next two quarterly payments so we compute it as N*10 (current row/column) minus the row/column where we happen to be figuring this (450), plus the row/column where its data is (360), plus finally the function (2 for subtract, so the signs come out right). This gets the negative of the running balance. Then we compute the maximum of that value and constant 50 (in variable I, specified by row/column 00) with function 6.

Now we are ready to figure the totals at the bottom (row 14). The column total for 12 rows exceeds the capacity of Tiny Calc, so we will define a subroutine to do it, and put it in the non-existent row 15. "Column 1" of this subroutine adds the first four rows of the current column (we use zero here so that the same subroutine works for all columns), then jumps to the next part of the subroutine in "column 2". There the next four rows are added before jumping to the third part. The end of the subroutine is whenever there is nothing else to do, which occurs after the last four rows are added. The subroutine is called by a jump to row 15/column 1, so we code that for each of the first five columns of row 14. Column 6 of row 14 is just the same as the running total in row 12, and does not need the subroutine to compute it.

Finally, we need to fill in all the computations that are the same as the ones we defined. Deposits need to be figured in months 7 and 10, using the same computation as in month 4 (row 4 column 5 function 0). Columns 3 and 4 of every row from 2 to 12 are the same as row 1, and column 6 is the same as row 2.

Now the calculation part of the program is complete. It remains only to put in the data for each month and year, and admire the results. Let's assume an initial payrnent of $50 (row 1 column 5). The actual sales figures are coded as constants in columns 1 and 2 of each row. For this illustration, assume that December figures are not in yet. The other numbers are quite unrealistic, to illustrate various features. For example, July was a bad month and returns exceeded sales in the taxables. October taxable sales were identical to September (yes, I know it does not happen that way, but this is only an example), so we used a "same as" operator (0); but since now there is some operation specified, the constant value must be explicitly referenced, thus the Add Constant (001) in variable B. November taxables are split up strangely, with overlapping constants. Tiny Calc adds them.

If you have put in all the program codes and data correctly, you should get the table below printed out. To help you understand how the functions work, try varying things one at a time, and compare the results.

5.13 -- Program Notes

In overall structure, this program is a simple loop that computes each value in the array once, then quits. For each computation, a GOSUB to the line number where the parameters are sets up which calculations are to be performed, and these are processed iteratively until there are no more, then the resulting value is rounded, stored into memory, and printed. In advancing to the next item, when the last column has been finished on any row, the row-end processing is done before beginning the next item on the new row.

Some of the complications of this program are due to the limited arithmetic capabilities of Tiny BASIC, and some to the generality of Tiny Calc itself. As with the other programs in this book, the notes here concentrate on the more obscure and complicated code. The simple parts are left to the reader as an exercise.

The user's application fills the lower line numbers of Tiny Calc (because they are easier to type than big numbers, and more mnemonic). The entire executive part of Tiny Calc is in the line numbers from 20000 to 24990. Program lines numbered 20xxx are concerned with initialization. Lines 21xxx are the main service loop, processing one item, then advancing to the next, until all have been processed. The rest of the program is organized as subroutines to support this main loop.

5.14 -- Variables

5.15 -- Data Space

Tiny Calc uses four bytes of Poked memory for each computed value. This has to be memory not otherwise in use. The easiest thing to program here would require the user to set aside the memory and tell the program where it is. This is rather a nuisance, so I put the extra code in to allocate the space out of the GOSUB stack automatically. This way, if there is not enough memory for everything, a memory overflow error will occur (rather than perhaps trashing the program or data).

Some Tiny BASICs store their numbers low byte first. Given that I have to look at the stack pointer anyway, it is a small thing to tell which byte sex the machine is: do one GOSUB, then compare the old stack top with the new. A GOSUB pushes exactly two bytes, so the difference is either two (if I looked at it in the right order) or 512 (if I have the bytes swapped). Line 20240 makes this test.

Now, since this memory allocation process is quite slow, another optimization is included (line 20280); if the stack is already big enough, don't push any more. This would happen if instead of stopping with an END statement, the last statement of the main program (line 21790 and 22390) were something illegal, like "...". Then after printing the spreadsheet, Tiny BASIC would print some error number and stop' but you would know (by the line number) that it is not an error, and that next time you RUN it won't take so long to get going.

Otherwise, variable J is the number of bytes needed, and a GOSUB-to-self loop is executed, decrementing J by two until it hits zero. The new stack pointer at this point is the beginning of the data block. No RETURNs will ever be executed for these GOSUBs, so the data space is available.

5.16 -- Main Loop

When the program gets up to line 20000 it has already printed the heading. The next order of business is the label on the first row, which is done with a subroutine call to the appropriate line: N*10 will always point to the user specifications (label or item) under current consideration.

First the variables A-H and the accumulator are initialized. Variables B-E are left zero by the previous computation, so no further zeroing is needed. Then the user specifications for this item are fetched by another GOSUB. The user specifications supply the differences from the default values in these variables.

If variables A-E are not all zero, then this item has some calculations specified. If they are all zero, but H, I, and F are not all zero, then this is a data entry, and operation 001 is forced into variable A. Otherwise the calculation section is skipped and the zero in the accumulator is stored directly.

The calculations are performed in six bytes (three variables, plus sign), but only stored in four. This means that the calculated result must have its high and low byte discarded. The subroutine at 24510 does a shift by two digits to effect this. The number is then rounded according to the specifications in variable G, and stored. The packing for storing the number is relatively simple; two digits per byte (binary 0-99, or seven bits) for the low four decimal digits, and then just cut the upper half of the number into bytes (but since there is no sign, it is limited to 32767, i.e. 15 bits). The sign of the number is packed into the third byte, extending its range to 0-199.

Printing the result (if G is not negative) is a little tricky. Only ten digit positions have been allowed, so if the result is a full nine digits (somewhere between 999,999.99 and 3,276,799.99; any higher gives an error stop), then when it is printed with two decimal places there is no room for a minus sign in front. I cheated. Since this is not an advertised capability of Tiny Calc, I decided to let you get away with it by printing the minus sign in place of the decimal point. But notice, two large numbers in adjacent columns will not leave any blank space for readability. This is why Tiny Calc is specified for eight digits only.

Line 21520 notices if the number is very large. Thus, for printing, S takes on one of four values: 0 for normal positive numbers, 1 for normal negative numbers, -1 for positive 9-digit numbers, or -2 for negative 9-digit numbers. The rest of the printing is straightforward.

Line 21740 checks for end of row. Line 21780 checks for end of run. This looks a little peculiar, since N is always a positive number, but L could be negative. If it is, it would have been nice to have an "absolute value" function; multiplying it times itself has the same effect, but gets too big if there are over 181 rows (182x182=33,124, which is bigger than 32767 and thus goes negative). Dividing by ten brings the product into range.

5.17 -- Calculations

In the main calculation routine, each successive variable is transferred to the current operation (variable O), and the variable it came from is cleared. The fields are unpacked (lines 22210-22290) and the operation selected is jumped to. Variable J takes on the row/column number of the operand for this, and if either row or column is zero (for operation digit less than 7), the current row or column is substituted. Operation 0 jumps to 22010, where the selected row/column parameters are fetched by a subroutine call, replacing the current values, and the whole calculation is restarted (except that the accumulator value is carried over).

The arithmetic is not overly obscure. The sum or difference is formed from the low order variable parts (the fractions) first, then carries (or borrows) are propagated to higher variables in the accumulator. By limiting the lower variable parts to four decimal digits each, there is room in the 15 (unsigned) bits of the variable to hold the full sum and its carry; the carry is of course stripped away after it has been propagated to the next four digits. In the case of the difference, the result may have a different sign than was expected, so it must be recomplemented (one of the artifacts of sign-magnitude arithmetic) by subtracting it from zero (with borrow, which shows up as the "1" in 10000).

Choosing the maximum or minimum is a little sneaky. A normal subtract is done, then the operation is compared to the resulting sign by adding them:

Since the old value was destroyed in taking the difference, it must be restored (if selected) by adding the new value back on.

All of the percent calculations simply multiply by the proper factor.

Multiplying is a pain, but not very complicated. The multiplier and multiplicand are each considered to be a string of two-digit parts. Each part of each number is multiplied times each part of the other number, and the partial products are lined up according to their relative decimal points. Some of multiplications are skipped because they have no effect on the result. For example the low half of Z times the low half of W gives a result that is necessarily less than 0.0001. Conversely the two high variables, if both greater than zero, will give an answer of at least 100,000,000 which is clearly an overflow condition.

Also, I ran out of variables to collect the answer in, so I had to build it on the fly, re-using the temporaries; this gives the code a little less clarity (sorry 'bout that). Note that from time to time the partial sums of the partial products run the risk of overflowing themselves, and carries must be propagated into higher parts of the result. Generally, three partial products can be summed before worrying about the carry out. The variable A is used here as an abbreviation for 100 (less bytes of program, and it executes faster).

You remember how they taught you to do "long division" in grammar school? Maybe they don't teach that any more; I just missed the so-called "new math" and had to learn it the old way -- I think a lot of schools are going back to it. Anyway, that is the method used for division. It is actually the same as the method used in computer division hardware, but in binary it is much easier. Divide routines like this are like Byron's sonnet (in his words): "When that was written, only God and the author understood it. Now only God understands it."

The general principle is that the division is done two digits at a time. A trial quotient part is chosen (in variable S; the sign has been saved in the GOSUB stack -- see below), and the divisor is multiplied times that and subtracted from the remaining dividend. This is repeated while summing the quotient parts until the divisor is greater than the remaining divided. The sum of quotient parts becomes the next two digits of the quotient. Then the dividend and partial quotient is shifted left two digits and the process repeated. It quits when the decimal point of the quotient lines up.

There are some optimizations in the loop. If a whole variable is zero (a likely occurrence for small numbers), the divide step is bypassed, and the number shifted left four digits (lines 23790-23950).

If the multiply ran out of variables, the divide did so in spades. Variable A (the first operation) is known to be unused (after the first operation starts calculating). Variable O contained the operation code, now unused. Variable S normally contains the sign of the accumulator (watch this one); we calculate ahead of time whether the sign of the quotient is positive or negative, then do a GOSUB to the divide proper in such a way that when it returns we know (from where it came back to) what the sign must be. This amounts to keeping the sign in the GOSUB stack while we use the variable. The method is explained in the Experimenter's Kit, available from Itty Bitty Computers.

Fetching a previously computed value is just a matter of unpacking the number from the storage format. It needs to be shifted over two places to put it into the accumulator format.

The constant is not quite in the internal accumulator format, so it needs a little manipulation to add the parts together, extract the sign, and align the digits. Also the constant variables (H, I, and F) must be cleared so it can be used as zero a second time around.

5.18 -- Program Listing -- Defaults

5.19 -- Program Listing -- Executive

 

Chapter 6 -- TINY ADVENTURE

Some computer games have complicated rules and need a great deal of skill to be enjoyed. Others are best played with as little introduction as possible. Tiny Adventure (TA) is in this latter category. Unfortunately, because it is only a tiny adventure game, there are some peculiarities you should know about.

The original Adventure game (available for most popular computers with disks) provided the inspiration for TA, but if you ever played the original game you will notice a number of significant differences. For one thing, your orientation in TA is significant, and you will not automatically see things off to one side or behind you -- you have to Look. Also, you can only hold one thing at a time in your hands. This is quite a nuisance when you want to open a locked door in a dark room, because you cannot hold both the lantern and the keys at the same time. And, unlike the original game, TA. keeps no score; you play for the pleasure of exploring, or set your own goals. For those achievement-oriented people like myself who need goals, there are some suggestions (read on).

Most of the instructions you need are given at the beginning of the game. My comments here are to clarify some common misunderstandings. TA has a very limited vocabulary, and it may be that you asked it to do something using words TA does not know, but that have the same initials as words it does know. The result is that what TA did may not be what you expected at all. If you have any doubt, it usually does not hurt to take Inventory and to Look around.

Another common mistake (or maybe it is a failing in TA) is to Look Left then Look Right in order to get a panorama of the situation -- but it does not work that way, because when you Look in some (horizontal) direction, you become turned in that direction. Similarly, if you try to Go in some direction, you will usually get turned that direction, even if you cannot go that way.

One common complaint I've heard from several people who played this game is that it does not follow standard Euclidean geometry. That is not true. A map (on a flat piece of paper) was drawn of the area before a single line of code was written, and it is faithful to the map. What happens is that in crawling, climbing, or otherwise moving from one place to another, you got turned around, and the way out may not be behind you. Or, the divisions between places (such as rooms) may not fall on cartesian boundaries. This is true to life, and the game is consistent.

6.1 -- Help

The vocabulary in TA is small, and there is a (partial) list of the words it recognizes available to you at any point in the game. The list changes depending on circumstances (for example, if there is nothing nearby to Open, that word will not be listed). TA will print out the list if you type in a letter that is not recognized in the situation. Usually Help will cause the list to be printed out without complaining about an error.

Important: When you ask for Help, or if you typed an unrecognized letter and get a list of words to "CHOOSE FROM", you must retype the whole command. For example, if you type in GI (thinking "Go In"), TA will complain and give you the list of directions you can Go (and "In" is not one of them), then ask for a "COMMAND?". Do not now type a direction. If, after reviewing the directions you can Go, you still want to do so, type the G again, followed by the first letter of the direction. Of course, you can change your mind about going, and do something else instead.

6.2 -- Some Goals

The first time you play TA, you probably will just want to wander around and get comfortable, see what there is to see, learn where the keys and lantern are, what mistakes to avoid, etc. Then you might try to do some of the following (no fair figuring it out from the listing!):

Can you rescue the maiden and her jewels without killing the troll (leave him locked in his den)? What is the least number of turns to do this?

There are two ways into the dragon's lair, but you cannot get back out by one of them. Can you find it?

Can you discover what the "magic dragon tears" do for you? Can you undo it? Can you get more, after you use them up?

This is a hard one: If you get lost in the forest, can you get out? Hint: You need to head off in the direction of the ravine, but you must get your bearings before you get lost. Crashing through the underbrush of the forest tends to get you turned around, and you usually end up going around in circles.

Once you solve the forest problem, you might want to take the maiden on a moonlight boat ride around the island. Watch out for the riptide!

How many turns does it take you to visit every place? There are 17 places in all, counting both ends of the tunnel as one place. Usually you can tell you're in a different place if the scenery is different, or if something you Putdown is no longer visible.

The troll will under certain circumstances, wander around on his own. Can you coax him into the bedroom? Harder yet, can you lock him in the bedroom without the maiden being there to look on?

6.3 -- Configurator

There are three lines in Tiny Adventure that need to be configured for your system. All three are inset in the listing by two spaces after the line number instead of one.

Line 110 defines the "peek" USR address in your Tiny BASIC interpreter. This number is equal to the Cold Start + 20, and is shown in the listing as 276 (for the most common Cold Start address of 256). You should change this to correspond to your version of Tiny BASIC. This value is only used in lines 1660 and 1710 (see below).

Lines 1660 and 1710 depend on the "byte sex" of your machine. Both are concerned with getting the pointer to the input line buffer, in the one case to see if there is more input, in the other to stop the input line short. The listing shows both forms, with the normal form second. If you have a 6502 (which puts the least significant byte first), use the first form. The 6800 and 1802 use the second form.

The Z8 Tiny BASIC handles input slightly differently, and three lines need to be recoded:

Half the fun of playing TA is in discovering what you can and cannot do. Much of this fun would be spoiled if the choices were obvious from the program listing, so it has been deliberately written in an obscure manner. Actually, that was only part of the reason - just trying to nuke it small accounts for much of the obscurity. There are, however, comments marking program section boundaries. If the program is entered into Tiny BASIC as printed, the comments will be eliminated, saving space. Or, you can omit typing them in. You can save some more memory space (771 bytes) by eliminating the instructions that print out at the beginning.

Because TA is so large (19,703 bytes), I found that execution became excruciatingly slow, simply due to the memory scan for GOTOs, GOSUBs, and RETURNs. A simple patch to the interpreter converts it to a binary search, for about an order of magnitude speedup in execution time. The necessary changes are listed in the Appendix.

A note for Z8 Tiny BASIC users. Unlike the other programs in this book, TA has already been crunched. Alas, Z8 Tiny crunches PRINT statements differently than the standard, so you will have to go through and change every occurrence of "PR" to a simple quote ("), and every occurrence of PR alone to a pair of quotes (""). Sorry about that.

You will need to fix the RND function calls for the Z8 as well. Ten of them can be fixed as indicated in the introduction (Chapter 1, section 1.4), but three work with large values. The following lines need to be replaced:

6.4 -- Program Notes

STOP!

Do not read any further before you play, or you will spoil the fun.

I am not going to tell you in this section what the map of the terrain is, but I would be remiss in my documentation if I did not give you enough information to figure it out from the listing. For you to do so, however, would spoil the fun. But once you have played the game out and tried everything there is to try, you may want to see if you can change the terrain or add objects or other citizens.

All of the features of the environment are encoded in the program code. Variables mostly retain state information: Where you are, which way you are pointing, where each object or citizen is, the state of the doors and windows, etc. If you want to stop a game in the middle and resume later, it is necessary only to note the contents of the variables and restore them to resume. The low two digits (decimal) of each variable are used to decode the text input; but variables B, Q, V, X, and Z are exceptions to this rule.

Line numbers less than 1000 are strictly setup, and may be omitted to save a few hundred bytes (for whatever good that may do), provided that the variables are correctly initialized before the game starts.

From 1000 to 1999, the program consists of the main command loop and assorted utility routines. There are also some utilities scattered in between other sections of the program.

From line 2000 to 11500, every block of 500 line numbers represents one command code, positioned by the name of the command. Thus line 2000 corresponds to the letter "A" (for "Attack"), line 3000 is letter "C" (for "Close"), 3500 is "D" (for "Drink"), etc. up to line 11500, which is letter "T" (for "Take"). Non-existent commands jump to line 16384 (variable V), which reports the error.

From line 14000 to 30000, every block of 1000 line numbers represents one place in the environment (room or cave or region of the meadow, etc.). The places are effectively numbered from 14 to 30.

Each object or citizen (the variable whose name begins that object's name) maintains the place number in its thousands digits. Thus, variable R retains the location of the Rock, which may, for example, be in the ravine (place 14), so its value there is 14018. The hundreds digit is used only in variable Y (for "You"), where it records your orientation. The units and tens of course are the encoding of which variable it is (R is the 18th letter of the alphabet).

There are three implied places in the game, where an object can be. Place 1 is "in your hand"; place 2 is "in your knapsack"; and place 13 is "dead" (out of the game).

6.5 -- Variables

The bit masks in variables B and Q are not fully utilized. Originally they were intended to be, but the program got too large, so some of the bits were disabled. The bits are given in the table in section 6.10, which also shows the bit positions in X for possible directions. The bit positions in X for things that can be Opened or Closed is the same as Q.

The bits in X, B, and Q are tested by multiplying the variable by an appropriate power of two, then seeing if the result is negative. This depends on the fact that overflow in multiplication simply wraps around, modulo 65536, without error.

6.6 -- Main Program

Lines 1000-1290 define the main program command processor. First the current environment is described. Variable W (the thousands digit) defines how much to describe: 1 is Front only; 2 is Right, Left, and Front; 3 is all four directions; and 0 is none of the above. There are several entry points into the beginning of the loop here, corresponding to whether a full panorama is required, if bit 12 in variable B needs to be cleared, or if no description of the place is required. Lines 1120-1160 step variable W through all the values down to 0023 to describe the scene; X here is used to hold the corresponding compass direction with the place number (i.e. the entry to the particular place or direction to be described).

After the full description, each of the other citizens is given a chance to act.

If the Dragon is awake, the subroutine at 6520 is called to determine whether he will change rooms, or go back to sleep. While awake, he meanders aimlessly between rooms 23, 24, and 25. If this results in his entering or leaving the place where you are, then notice is made of the fact ("enters" or "exits" if there is light to see; "scuffling" if not). If he enters the place where the maiden is, then she will respond in some way (her hand turns cold if you are holding it; if the troll is not present and you are not far away, then you hear her scream). If the dragon stays in the room where you are, then note is made of that fact (depending on whether there is light, it "fills your view" or "you hear breathing").

If the Troll happens to be in the place where the keys are, they are taken out of the game (sent off to place 13). If you are in the same room and there is light you are notified of the loss. The keys can only be recovered by killing the troll.

The troll also wanders about, though not always aimlessly. When the maiden is in the same place (note that "in your hand" is not the same as in the room you are in), then he makes a beeline for his den; the maiden follows unless you are in the room and it is light, and sometimes even then. If the maiden is not with him, the troll will wander about aimlessly throughout the enclosed areas (places 23-30), but in no case will he go through a closed door. When the troll enters, exits, or remains in a lighted room with you, you are notified of the fact.

Then in lines 7310-7390, the maiden has an opportunity to act on her own. If you are invisible and she is in the same room, mention is made of it. If she could see you and is in the place you just left (variable U) and the troll is not with her then she has a chance of following you out (better if there is light). Notification of this is made by the "footsteps". Finally, if you and she are both in the same place and there is light (and you are not invisible), she holds out her hand.

The actions of the three other citizens of the game go on independently of what you do. The technical word used for this kind of activity is "demon"; I don't particularly like the spiritual implications of the term , but it can't be much worse than when I used a "Diablo" (Spanish for "Devil") printer to print this book. Anyway, after the three demons are processed, you get prompted for another command, and a computed GOTO executes it.

6.7 -- Places

The seventeen actual places are divided into functions. Every place has responses for the six cartesian directions, plus a general introduction and an appendix. There are three entry points for the introduction, one for the appendix, and two for each cartesian direction. Each entry point is associated with a fixed line number value in the low three digits. Since the listing clearly identifies each place (in the REM header), there is no need to repeat that information here. The following notes deal only with the parallel entry points that are the same for all places.

Line xx010
This is the name of the place. It is called as a subroutine whenever the game needs to tell you where you are.

Line xx040
This is a subroutine that returns in variable X, a bit mask of the available places to Go (see Go command for details).

Line xx070
This is a subroutine that returns in variable X, a bit mask of the available things that can be Opened or Closed. At most one of each category of thing to be opened or closed may be present in a given place. The Bedroom has the maximum of one Door, one Window, and one Chest. For details, see the Close command.

Line xx110
This is a subroutine that prints what may be seen by Looking North.

Line xx140
This is not a subroutine. It takes the necessary action to cause you to Go North, and eventually jumps to the command processor.

Lines xx210, xx240
These two entry points are concerned with Looking and Going East.

Lines xx310, xx340
These two entry points are concerned with Looking and Going South.

Lines xx410, xx440
These two entry points are concerned with Looking and Going West.

Lines xx510, xx540
These two entry points are concerned with Looking and Going Up.

Lines xx610, xx640
These two entry points are concerned with Looking and Going Down.

Line xx910
This is a subroutine that may do or print anything associated with this place. Often it is used to draw attention to some object, such as the Sword in the forest. In some places, the objects there are listed at this time; in other places you are required to Look (usually down) to see them.

There are a few places that are not completely straightforward. Most of them can be deciphered by following the circuitous GOTOs and GOSUBs, and need no further comment. The remarks below deal with the hard ones.

Forest
In both forest places, every direction but out gets you lost. This includes both a random re-orientation, and projection into the middle of place 19. Two turns of correctly heading towards the ravine are necessary to get out of the inner forest, and another to actually get to the ravine. Of course, each of the first two turns got you all turned around, so you need to check your bearings again.

At Sea
You can get into the boat and head off in any direction. But if you do not stay on course towards known land, the same thing happens as in the forest. When you are lost at sea, the only back is to make a beeline for mainland (again, two turns required, and since it is dark out there, you also get a random re-orientation).

In an early version of the program, when things were Putdown in the boat in one place, then you moved to another place, the things did not follow, but they magically re-appeared if you went back. This is unrealistic, so there is a special call to a routine (line 4695) to move all the objects in the boat with you to the new place.

Between the island and the mainland, the boat can be in any of three places: beached on the mainland, at sea between, and beached on the island. Two bits in variable B encode these three states (bits 12 and 13). The states are ordered, so that a simple add or subtract of 4096 (V/4) correctly changes state.

Tunnel
The tunnel is divided into two regions with different scenery and things you can do -- it's almost as if they were two places. Bit 12 of variable B distinguishes them. If I had it to do over, I would probably make two places out of it.

Crashing into the walls anywhere underground wakes the dragon up (see line 25440).

6.8 -- Commands

There are eleven defined commands. The other 15 letters of the alphabet are rejected. I had intended to include three or four others, but space got out of hand so they were eliminated. One you might want to consider adding on your own is Break, since much of the structure is there for it (an extra bit in Q for each window and mirror, to show that it has been broken).

Each command must determine on its own criteria, whether it can be done or not. If it is not possible, a suitable error message must be printed out. If the command can be executed, then that is done, and the command routine should jump finally to the command processor to take the next command. There are several points to jump back to in the command processor, depending on whether a new place was entered (and must be described), of when an action was taken in the same place (and print "OK"), etc. This is why the exit from a command is a GOTO, not a RETURN.

In the notes that follow for each command, you may find it helpful to follow in the program listing, and where subroutines are called (there are many of them), it may be necessary to refer to the descriptions of the subroutines (which come after the command descriptions).

Attack
In general, the Attack command determines if there are any citizens around to be attacked, ranking them in order of villainy, and assigns a probability of being able to kill them (hundreds digit of variable P; thousands digits are the name of the foe). I suppose I could have had it ask you whom to attack. Oh. well. A second probability figure is computed for the weapon. If what is in your hand is not a weapon, the command aborts. If it seems unlikely that you really want to complete the attack (i.e. kill the fair young maiden), you are asked for confirmation.

On with the battle. The probability factor for the foe is extracted into variable X, and reduced to half if you are invisible (he is less likely to win if he can't see you). Two random numbers are generated, and if yours is bigger, you win, killing the foe. If you failed, the troll (if he was your foe) has a small chance of dumping your knapsack in the melee (with interesting consequences). You also have a certain probability of dropping your weapon (if you have one). If you win, of course your foe is sent off to place 13. Note that the dragon must be put to sleep when he dies, or he will creep back into the game and wander around (as he did in an early version).

Close/Open
There is essentially one routine to deal with both opening and closing things, since they are basically the same kinds of operation, distinguished only by the sign of Q (negative for Open).

First it must be determined if there is anything to open or close. This involves a tricky little subroutine at 9310. Variable H holds a temporary value that has ones in those bit positions that can be opened or closed. If Q is positive, then it is used directly, because the command is to Close, and the ones in Q are those things that are open. If negative, then its complement is used. Note: I really want "one's complement", but that differs from the arithmetic negative in only the lowest non-zero bit, which is always bit 0 (and it does not hold useful data). Then X and H are shifted left together one bit at a time (lines 9350-9380) until the first non-zero bit comes up in X. The low bit of H is tagged (set to one), then if its bit (corresponding to the bit in X just tested) is also one, we are done. Otherwise the process is repeated until we run out of bits in X. On exit, both H and X are negative if there is something to open or close. H is odd if any bits in X were passed, that is, we tested them. X is not exactly -32768 if there are more things we did not look at.

When all is said and done, if there is more than one thing to open or close in the place, then it is necessary to ask which one. Otherwise, the command can just proceed with the one item. This is determined by looking at the remaining bits in X (the things not tested) and the low bit in H (things that were tested). If clarification is needed, and the input line is empty, more input is requested. Then another letter is read. It must be "C", "D", or "W", which selects a range of bits to be examined from X (and expecting only one bit in that range). The range is selected by remaindering (which is what you must do if you do not have a logical AND operation).

Now it is but a simple matter to test whether that bit in variable Q is one or zero, check the sign of Q (the two should be different), and perform the appropriate action (turn bit on or off). Of course, if the command was to Open, and if the selected bit is a door or the troll's chest, the keys must be in your hand to complete the command. Also, if you opened the troll's chest and you are visible, the troll is in the room, and there is light to see, the troll quickly closes the chest again.

Drink
This one is fairly simple. If the flask is in your hand and not empty, then it is emptied and you are set to be invisible, with appropriate response from the maiden (if she is in the same place and it's not dark).

Go
The Go command is not very complicated. A second letter is required to specify direction, and if the input line is empty, a prompt is issued. All 23 possible inputs are considered, and only a few are rejected. The selection is computed on the value of the input (in X) with a GOSUB into the range 5100-5330. The returned value is (in the hundreds digit) the correct cartesian direction to go. Relative directions are computed from the difference between Y (which has your current orientation) and N (that has a zero in that digit), by subtractinging an offset (100 for each 90 degrees of turn to the left), and if the value goes negative, normalizing it. If the direction is one of the four compass points, your orientation is set to match. Finally, the command jumps off to the respective entry point in the place code to do it.

Help
The only difference between Help and an error is that Help does not ask "WHAT?". The choices given are the same. First, if it is dark, several important commands are not mentioned (you cannot see to do them). Otherwise, if there is a potential foe around, Attack is mentioned. If something can be closed, Close is mentioned. If the flask is in your hand and not empty, Drink is mentioned. You always get Go, Help, and Inventory. Look, if not dark. Open is like close, but the sign on Q has to be toggled. If there is something in your hand, Keep and Putdown are mentioned (but you cannot Keep the maiden, so that is excepted). If there is nothing in your hand but your knapsack has something, or there is some object in the room you can take, then Take is mentioned.

Inventory
This one is pretty obvious.

Keep
This is also pretty much obvious.

Look
Look works something like Go. If there is no second letter waiting, prompt for it, then a computed GOTO on the input letter selects a cartesian direction to point you. For Up and Down, a GOSUB to the appropriate entry in the current place (N-496 or N+596) does it. Otherwise, you are pointed in the selected direction and the command processor displays what you see.

Put
With only four lines, what is there to say?

Take
If your hands are full, you cannot take anything. Otherwise if the input line is empty, another input is prompted. As in Look and Go, a computed GOSUB based on the input letter selects the appropriate action. If the letter names no object, that is an error. If a known object is named, then it must either be in the knapsack or in the place, or that is also an error. If the jewels are mentioned, they must not only be available, but if they are in either the bedroom or the troll's den, then they cannot be taken if the chest is closed. The keys work the same way in the bedroom only (since they are needed to unlock the troll's chest, it would not do to force them into it).

6.9 -- Subroutines

Tiny Adventure is full of little subroutines that do obvious little things. For example, line 1380 is a one-line subroutine to print a short message. Making it a subroutine saved a few bytes by eliminating its duplication in the several places it is used. The remarks to follow deal with the routines whose function or operation is less than obvious.

Line 1480
This is one of several routines to set or clear a bit in variable B. This one clears bit 12. The bit is tested by shifting it over to the sign position by multiplying by the appropriate power of 2; in this case, a 3-bit shift is done by multiplying by 8, which is 2^3. If the bit is one (the product is negative), it can be cleared by subtracting the appropriate power of 2 (for bit 12, subtract V/4=4096=2^12).

Line 1650
This routine examines the input line to see if there is any more input on it. X is returned zero if not. This is used to decide whether to prompt for more input.

Lines 1700-1840
This routine sets the input line to empty, and reads a value, which is assumed to be the value of a letter-variable. Because variable B is a bit vector, not a decimal string as the others, it must be treated separately. Similarly, X is set to zero, so that an input of "X" will read a well-defined value, namely zero. If the input is "Y", this also is converted to zero. All other letters, A-W, are stripped of the higher digits so that the value returned in X is a number between 0 and 23. Any value outside this range is rejected, which is signified by a returned value of -1. A second entry point is line 1720, which accepts any remaining (previous) input.

Line 1870
This routine calls on the input routine to get a yes or no answer. If not one of these, the message in line 1860 is printed to accept another input.

Lines 2700-2880
There are several situations where another citizen moves into or out of the same place as you, so this routine relates the adjacent room involved to your orientation to specify which relative direction that is. For example, if you are in the Dragon's lair, there is only one connecting passageway, to the south (hundreds digit 3 in variable P). Otherwise, if you are not in the cave (place 23), then if the adjacent room is 24 (the tunnel) then the direction must be east (2), since that is the direction to the tunnel from both the troll's den and the wine cellar (see line 2750). After all the options have been exhausted, P contains not only the place number of the adjacent place (that it started with), but also a direction. The difference between this and variable Y reflects the relative direction of that passageway, and is used to compute a GOTO (line 2810) to select the appropriate print statement.

Line 2900
This routine determines if the chest to be considered is the one in the bedroom, and if so, if the keys are also in the room ("yes" sets X to 1). Then if either the keys or jewels are in it, that is printed out. Finally, the fact that the chest is open is printed. If the chest is not open, then of course its contents cannot be seen, so this subroutine is not called. Line 2970 is also used to print the word "OPEN" associated with the command of that name.

Lines 3930-3990
This short routine is used in the forest and out in the open sea to get you disoriented. It also clears bit 12 in variable B, so that it will still take two turns to get out.

Lines 4030-4590
This routine is concerned with selecting the next room the troll will be in. On entry, the thousands digit of P has a probability figure to determine if any change is likely. Then, depending on where he is (in X), a computed GOTO into the range 4110-4180 selects the room parameters, where there may be a choice of directions (selected randomly), or if a door is closed the troll must have the keys to go through. A second entry point (at line 4060) uses a different selection algorithm, namely, the fastest route to his den, by computing a GOTO in the range 4520-4590.

Line 4610
This prints "CAN'T".

Line 4630
If it is dark, it prints "YOU SEE NOTHING". Otherwise it jumps to the line whose number is in variable X to print what you see.

Line 4660
This routine is called if the troll has bested you in battle. Then, if the knapsack is not empty (X=0) its contents are dumped out into the room (see following routine).

Lines 4695-4790
For each object that is in the same location specified by variable P, this routine moves them into the place where you are (variable N). It is used to dump the knapsack out onto the floor (P=2016), and to move objects in the boat from one place in the sea to the other (P is the place you came from).

Line 4840
This routine prints out that you cannot do something. Variable X contains the line number with the print statement of what it is you cannot do, and that is called as a subroutine after printing "CAN'T". Finally, it jumps back to the command processor to accept another command. The entry at line 4830 also sets bit 12 in variable B to one.

Lines 4920-4990
This routine checks for sufficient light to see. The answer is yes if P is greater than N, and no if less. In general there is enough light to see if the lantern is in your hand or in the room, or if you are not underground and not lost at sea. A second entry point at line 4910 answers yes only if the troll is also in the same room with you, and is used to select actions that need you to see the troll.

Line 5600
This routine takes a bit mask in variable X corresponding to the cartesian (compass) directions, and prints out the the corresponding words, relative to the direction you are facing. The non-relative directions are tested (by shifting the respective bit into the sign with a multiply) and printed directly. The computed GOTO in line 5690 selects one of the following four lines, depending on the direction you are facing, which shifts X 0-3 bits left (by adding X to itself). The bits have been duplicated (in line 5680) so that what was originally in bit positions 1,2,3,4 is now also in 5,6,7,8 thus:

Line 6210
This routine tests to see if the knapsack is empty. If any object is in the knapsack, then its location (the thousands digits) will be 2; subtract 2, and multiply times the rest of this stuff, and the answer is zero. If nothing is in the knapsack, then all these values will not be two, and X will be the product of seven numbers none of them zero, so it should not come out zero. Now, actually, this is not a perfect test. If you have four things in the forest (place 18), then you will multiply 16 times itself four times, which is 65536, which Tiny BASIC finds indistinguishable from zero. Similarly, five things in the troll's den (place 26), and at least one other thing in any even-numbered place, will do the same thing (five multiples of eight, times one multiple of two, gives a multiple of 65536). However, if this failure occurs, you will be told something like "you have kept" instead of "you have nothing kept". It is not that serious a problem, but if you wanted, you could fix it by re-coding it to work like the room test (lines 6240-6280).

Line 6240
This routine tests for the presence of an object in the same room with you, and if so, variable X is returned zero. If there is nothing there with you, X is returned non-zero (the location of the maiden). Thus a single IF condition on X can test objects other than the maiden (X=0) or objects including the maiden (X*(N-X-1)=0).

Line 6440
This routine tests the contents of the knapsack, then prints either that list, or the word "nothing".

Lines 8520-8590
This prints the tunnel scene. If it is dark, of course "you see nothing." If variable X is zero, then there is nothing to see in that direction, so you see "a dank wall". Otherwise the direction is selected by the bit in X (Up, Down, and neither=Off).

Line 8950
There are many many ways to look where there is nothing to see but a plain wall. I tried to liven it up with some (otherwise useless) descriptive adjectives. The line number of the print statement for the adjective is in variable X, and it is called as a subroutine at the appropriate place in the phrase.

Lines 9770-9890
This is a very useful routine for finding out what object (if any) is in your hand. H is returned with the object number (the ordinal value of its alphabet letter name) in the thousands digits, or if your hand is empty, with zero there. There is no convenient way to get this information but by testing each object to see if it is in your hand.

Line 10100
This is actually eight subroutines that are normally called with a computed GOSUB, indexed on the name of the object (that is, line number 10100 plus 20 times the letter value). Only the selected object is moved. Variable P contains the destination (room number, or knapsack or hand).

Lines 12490-12965
This is a set of sixteen subroutines that are normally called with a computed GOSUB on the letter value of the object whose name is to be printed. Each subroutine has two entry points, one for the simple word (at line number 12500 plus 20 times letter value) and an entry (line number less 5) that prints a more descriptive phrase. Zero is allowed as a letter value in this set, and refers to empty hands (used in Attack as a weapon name).

Lines 13540-13730
This is three routines, with indexed entry points corresponding to the three other citizens of the land. The selected routine is called with a computed GOSUB (line number 13500 plus 10 times letter value) from Attack, when the selected party is slain in battle. Note (line 13540) that when the dragon is killed he is also put to sleep. When the troll is killed, if he has the keys (line 13720), they fall on the floor.

Lines 13770-13990
This subroutine prints all the objects in the place specified by variable P. There are several entry points: Line 13770 assumes the place where you are is desired, and a full description of the objects is to be printed; line 13810 asks for a short description (used when P is the knapsack, etc.). Each object is tested to see if its place is the same as P, and if so, it is printed.

Some objects, notably the keys and jewels, have restrictions. These are in the chest if they are in the troll's den or the bedroom, so for a room description they are not printed (this is distinguished by X=0 if not the room description). If this is to be a list of objects that can be Taken, then the respective chest must be open. Line 13860 is perhaps the most obscure of these tests. If P is the bedroom, then P/29000 is 1 and the product is (1+1)*64*Q, which tests the open bit for the bedroom chest; otherwise the product is 64*Q to test the troll's chest.

6.10 -- Bit Vector Table

6.11 -- Program Listing

Chapter 7 -- IEEE DISASSEMBLER

Everybody has a disassembler for their own computer. It is one of the first programs you get, so that you can start hacking on acquired code. This disassembler is different. First, it is written in Tiny BASIC, so you can easily modify it. More important, it handles three machines, not just the one you have. And most significant, it uses the new proposed standard IEEE mnemonics and syntax.

The IEEE Computer Society has been involved in standards activities for microprocessors since 1977. One of the early projects was a uniform representation of similar functions by the same mnemonic in microprocessor instruction sets (project P694). This work climaxed in a draft standard published in Computer magazine in December 1980. Responses to that publication resulted in some changes to the proposed standard, which have been included in this disassembler. The changes are mostly syntactic, not having to do with mnemonics.

7.1 -- Operation

Running the disassembler is self-explanatory. Entries are made in a special form of hexadecimal, where the letter "O" is used instead of the digit zero (like in the hex dump at the back of the Tiny BASIC User Manual). It will first ask for the address of the cold start of your interpreter, which you type in this funny-hex. Each funny-hex number must also end with the letter "X", because Tiny BASIC keeps reading digits across spaces, commas, and carriage returns.

Then you are given a choice of CPU type: 1802, 6502, or 6800 (sorry, no Z8).

The code to be disassembled is assumed to be already in memory (getting it there is your problem), but possibly at a different location that it is intended to execute. The "offset" input will change line addresses and relative jump addresses by the offset value so that the disassembly listing reflects the true execution addresses. A positive offset means that the location in your memory is greater than the intended (and listed) address. For example, if you load into memory locations starting from hex "4OOOX" a program intended to execute at hex "O1OOX", then the offset is the difference, "3FOOX". For programs loaded into their correct locations, just type in zero "OX".

Now you will be asked for a sequence of starting and ending addresses. If the starting address you give is less than the ending address, then the program will go back to ask you for "Which CPU?" Otherwise the program between the starting and ending addresses (and possibly one or two bytes farther, if in a 3-byte instruction) will be disassembled. Note that these addresses are the logical address values, not the physical place in memory where the program is. If the offset is not zero, it is added to the starting address to get the actual location of the code. After completing one section of disassembly, the disassembler asks for another starting and ending address. Important: Because addresses in the upper 32K of memory are negative, you cannot disassemble in one segment a program that crosses address "8OOOX" -- two starting and ending address sequences are required, the first ending at "7FFFX" and the second starting at "8OOOX" (or just after, if an instruction crosses the boundary).

The form of the listing is a hexadecimal (logical) address, then the opcode byte in hex, followed by zero, one, or two bytes of operand, then the mnemonic in the next column, and any operands in the next column after that.

7.2 -- Operand Syntax

The IEEE standard makes two consistent syntactical requirements. First, the location or addressing mode of the operands is to be specified in the operand field, rather than in the mnemonic. Second, when there is more than one operand they are ordered in a uniform Source-Destination sequence (insofar as that is meaningful).

7.3 -- Addressing Modes

The proposed standard calls for a unique prefix character to identify every common addressing mode in a microprocessor. The addressing modes used in this disassembler are as follows:

Of course, as some of the examples show, certain of the addressing modes may be combined.

7.4 -- Mnemonics

The IEEE mnemonics were carefully chosen to reflect common usage and perspicuity. For example, the first letter of each mnemonic is normally the first letter of the action verb that describes the function of the opcode.

The 1802 presented a little bit of a dilemma, since the address register loading instructions (in RCA's notation "GLO", "GHI", "PLO", and "PHI") have no exact correspondence in the standard. I tend to view the machine in a hierarchical fashion, something like this:

The other way to view it would put the address registers up at the same level as the accumulator and carry. The point of view has a profound effect on the mnemonics chosen here, since memory-to-register transfers require the standard mnemonics "LD" and "ST", while register-to-register transfers require the standard mnemonic " MOV". The "MOV" mnemonic is a horizontal operator, and requires the specification of both the source and destination. The problem here is that there is not an adequate name for the 1802 accumulator. RCA uses "D" in its documentation, but that is too easily confused with the hex number 13. The IEEE register name ".D" can't be used, as it is the IEEE standard the name of 1802 address register R13. I opted for my favorite model (vertical) so I could use the "LD" and "ST" mnemonics, which require no specification of the accumulator (there is only one, so it is implied). The justification is that the register file can be thought of as a 32-byte scratchpad RAM with some special functions. Indeed, many programs I write for the 1802 treat it exactly that way.

The register loading and storing operations are distinguished from the memory references by a qualifier tacked onto the mnemonic (as per the standard recommendation) to say whether the high or low byte is accessed. This is not a necessary distinction, since there would be no ambiguity of reference: Register references use register addressing in the operand field, while memory accesses use register indirect or implied addressing.

The table on the next two pages gives the standard mnemonics (as augmented for the 1802) and the usual manufacturer-defined mnemonics for reference. Note that in many cases there are several manufacturer-defined mnemonics corresponding to one IEEE mnemonic. The reverse is also true, but the disassembler must choose one. For example, the IEEE mnemonic SETC (set carry) corresponds to a 2-byte instruction in the 1802 (FFOO), but it is usually represented as SUB #0. Many of the blanks in the table, however, represent instructions not available in the respective processor.

A number of IEEE mnemonics have not been included in this table because they are not printed out by the disassembler. Several of these are branch condition names in the standard but not represented here, though in fact they do have opcodes in the respective machines. For example, the standard mnemonic BEQ is the same as BZ for all three CPUs; BL (Branch on unsigned Low) is BNC in the 1802 and 6502, and BC in the 6800.

The last 19 entries in the table are mnemonics I made up to cover deficiencies in the standard. They are constructed according to the guidelines specified in the standard, and are believed to be consistent with it. I do not mean to suggest here, however, that they are in fact "standard" mnemonics, as are the first 62 entries.

7.5 -- Configurator

Any standard version of Tiny BASIC should have no trouble running this program. The address of the Cold Start is requested only to compute the peek address in line 160. This address is used only in line 3060. Though I cannot see much value in it, Z8 users could run this program by changing either of these two lines.

The disassembler is quite large (10426 bytes as listed), but if you are interested in only one CPU, the code for the other two is easily removed. Of course, the usual byte-saving techniques can be used also. The line numbers involved are (note there is some overlap):

7.6 -- Opcode Table

7.7 -- Program Notes

Originally I intended to make this an interpreter (simulator) as well as a disassembler. Now wouldn't that be a trip! A Tiny BASIC program on your computer interpreting a Tiny BASIC interpreter interpreting a Tiny BASIC program (perhaps the same program, for yet another iteration); can you imagine how slow that would be? But, as with some of the other objectives of this book, my sights got lowered. If there is sufficient interest, I'll try to do it in Volume 2.

In principle, a disassembler is very simple. You look at the next byte, and find it in your opcode table; if it is a multi-byte instruction, you get the extra bytes. All the bytes are printed in hex, then the opcode mnemonic is printed and the operands are decoded and printed out. Then you repeat, until you reach the ending address.

In this disassembler, the initialization is in the line numbers less than 1000. The lines 1xxx analyse the opcode. 2xxx print it, and the operand bytes in hex, then jump to the mnemonic printer. Lines 8xxx input and print funny-hex numbers. The mnemonic tables and operand representations are encoded in the program from line 10000 on. There is almost nothing in here that is not rather plain and understandable. A few places use long complicated arithmetic expressions to get a fancy result. I'll try to explain what they are doing. Otherwise almost anything I could say would be trivial.

7.8 -- Variables

7.9 -- Opcode Maps

Part of the analysis of the opcode necessary to determine how many bytes are involved, and what the operand format is, is to determine which class of instruction it is. Most CPUs have a (usually small) number of fixed formats, and the trick is to discover these and use them to advantage in a disassembler. Nice CPUs like the 8080 and 1802 put their fields on hexadecimal or octal boundaries.

Then there is the 6502. Actually the 6502 fields are reverse octal (3-3-2 instead of 2-3-3). The 6800 and 1802 have a well-defined major field, then sub-fields within that. The 6502 is much messier.

Let's start with an easy one. The 1802 comes in two formats, determined by the upper hex digit: 1111 RRRR and 1111 N SSS. The four major instruction bits select one of sixteen groups of instructions. Some of these are an opcode by themselves, and the lower four bits are a register number (an operand); this is the first format above. The others generally have two subfields, where the low three bits select one of eight functions and bit 3 selects an option or variation, or bit 3 defines a major group and the low three bits determine a port number (in the case of 1/0). For the disassembler, I linearized the sequence by giving every instruction of the first format an opcode number between 0 and 15, then (generally, with special-case exceptions) assigning the second format to opcodes of a full eight bits.

The 6800 has more formats, but fewer special cases: 00 11 SSSS, 01 AA SSSS, and 1 R AA SSSS. The most significant bit or two selects the format, the next two bits determine an operation class or addressing mode, and the low four bits select the function within that class. These are linearized by using the full eight bit value for the first quadrant (high two bits zero), then taking the other two formats as a sequence of 16 to 87 opcodes each (on the low four bits, offset to not overlap).

The 6502 has basically two formats: SSS AAA I I and SSS SSS 00. The low two bits are the major distinction, with the two non-zero cases most well-defined (all opcodes with 11 in the low two bits are illegal). If the low two bits are zero, the fields are very ill-defined. In one case (the middle three bits 100) the upper three bits define the instruction. In another case (most significant bit 1, middle three bits not 100), it follows the first format. It is obvious that this CPU was not a top-down design like the others. I linearized the opcode set by making the low two bits major, and the high three minor within that. The exceptions then got tacked on to the end of the sequence. In all cases, the addressing mode bits of the opcode are saved out (not considered part of the opcode) to affect the format of the operand printing but not the mnemonic, as per the IEEE requirements. The linearized opcode is used to compute the GOTO in line 2180; so peculiarities in the computation of it can be understood in the light of the correspondence of line numbers to mnemonics in the mnemonic table section. For example, line 1530 looks complicated enough: if it is a one-byte instruction (W=O), we are interested in the case that the low five bits are 01010 and the most significant bit is 0. All others get a new sequence value (variable 0) that moves them out of the way. Y'know, now that I look at this I gotta say, there must be a better way to do this. Maybe I should offer a prize to the first person who can provide a lucid description of how this works. How about a free copy of Volume 2 (if I do a Volume 2)?

The moral of the story is that Tiny BASIC is the worst of all possible programming languages for stuff like like this, except maybe binary absolute is probably worse.

Line 8170 is used to convert a number read in as decimal into the binary number that should have come in if it had been read in as hexadecimal. For example, if you type in "1234", the digit "31" is weighted by the decimal input routine with a value of 10; but, as hexadecimal, it should be weighted with a value of 16, so the difference (six) times that digit's value must be added on. Similarly, the "2" was weighted 100 (i.e. came in as 200), but we want it to be weighted by 256. The correction to the tens digit has added another weighting of 60 to this digit (bringing it to 160), so there remains yet 96 to add. And so on. Try working a number through.

In line 8820, the objective is to do a right shift by division, but negative numbers do not shift correctly. Therefore, if the number in H is negative, we remove the sign bit (by subtracting off 32768), do the divide, then re-insert the removed bit (which as shifted, now has the value 8).

Line 10820 does exactly the same thing.

7.10 -- Program Listing