Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.


Once upon a time - specifically, in the mid-60s - John G. Kemeny and Thomas E. Kurtz decided that they wanted to get students more interested in computers, even the ones that weren't in math and science programs. To that end, they developed two programs. One of them was the Dartmouth Time Sharing System, an operating system that allowed more than one person to use the minicomputers of the time simultaneously. (Minicomputers were essentially smaller, cheaper mainframes, the size of a large refrigerator rather than the size of a room.) People would sit in front of a teleprinter, a device like a typewriter that communicated with the minicomputer, and type on it. What they typed would be both literally typed, on paper, and sent to this computer, which would then devote a fraction of its processing power - the other fractions being devoted to people at other teleprinters - to processing this information. It could then send back additional text for the teleprinter to print.

The other program they developed was BASIC, Beginners' All-purpose Symbolic Instruction Code. With this, anyone could sit down at the teleprinter, create a simple program using relatively easy-to-understand commands, and see the results immediately instead of having to submit their program to be processed in a large batch. This got a lot of people into programming who wouldn't have otherwise bothered. They made BASIC available free of charge, and various people customized it to add desired features or handle the idiosyncracies of the machine they were running it on.

Needless to say, one of the first things people wanted to do with computers was play games on them. Many people wrote games in BASIC, and these games were copied far and wide. Some years later, in 1973, David H. Ahl collected a number of these games into the book 101 BASIC Computer Games, which used the BASIC dialect used by Digital's minicomputers. The book eventually sold 10,000 copies, which Ahl said “was far more books than there were computers around, so people were buying three, four, five of them for each computer.”



Even as he did, though, the first microcomputers, computers that you could actually own and have in your house, started to come out. Most of them wound up having some form of BASIC pre-installed; it was a language powerful enough to actually do things with yet small enough to fit into the limited memory of the systems. In response, Ahl republished the book in 1974, now called simply BASIC Computer Games and with the programs (slightly) adapted to run on these microcomputers. (The versions of BASIC these machines came with were further customized to the machines' capabilities, so the BASIC of the books was kept quite generic, and two pages of info on porting the programs were added.)

The continuing popularity of BASIC led Ahl to later publish three more books of computer games: More BASIC Computer Games (1979), Big Computer Games (1984), and BASIC Computer Adventures (1984). The first book was even re-released in 2010, adapted to Microsoft Small Basic.

BASIC is still in use today, even with the rise of more elegant or powerful programming languages. It's evolved significantly, taking features from newer languages and making them its own, but the older varieties persist in retro equipment, emulators, and programs written to bring back those good old days.

But let's wind the clock back a bit. Back in 1973, when David Ahl was publishing his first book of games, I entered the picture, so to speak. My interest in computers appeared early, though not so early that I actually got to use a teleprinter as intended (though I did get to type my name into one, keeping the little strip of paper tape that came out for some time afterward). My first computer was a TRS-80, Model I, Level I, though my parents shelled out for the extra hardware that gave it upper and lower case characters. And I eagerly learned the secrets of BASIC, mapping out graphics in the TRS-80's strange character set while I waited for the tape drive to laboriously load a game.

From there, I went on to own a Commodore 64, followed by a Commodore 128, an Amiga 500, and an Amiga 1200. Sadly, by the time I got the 1200, I was starting to see the writing on the wall, and my next computer, and all those thereafter, were IBM-compatibles. But throughout the whole journey, I kept my hand in the BASIC well - and part of that was reading and re-reading the library's copies of BASIC Computer Games and More BASIC Computer Games. (Dewey Decimal number 794.8, in case you're curious - and yes, I did have the first three digits from memory.)

So in celebration of nothing in particular, let's dig those books out (in this case, out of Atari Archives, but you can find them elsewhere). We'll go through the programs one by one, see what they do and how they do it, and try to come up with ways to make them better, or at least more interesting. Some of them may even be interesting enough to actually play for a bit!

Adbot
ADBOT LOVES YOU

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.




Before I get started analyzing this program in particular, I'm going to discuss some features of BASIC in general. Be warned, however, that BASIC existed in many shapes and forms even back then, so I'm only describing a sort of generic BASIC, influenced by the programs in this book and by my own experience with Commodore BASIC V2 and QuickBASIC. Stick "usually" in wherever needed.

Interpreters versus compilers

Many languages are compiled languages. You write code in one or more text documents, then run a program that takes that code and turns it into another program that you can run. BASIC, however, is an interpreted language; the BASIC program takes the written code and executes it immediately. This is convenient for the purposes of debugging and testing, but it means that you must have access to the BASIC interpreter to run BASIC programs. Some modern BASIC implementations compile your code, and some act as both interpreters and compilers.

Lines and Line Numbers

A line of code contains one or more statements, separated by colons. It usually doesn't matter if code is on one line or multiple lines, but there's a limit on how many characters can be on a single line (and what that limit is depends heavily on which version of BASIC you're using), and the IF - THEN command (more on that later) cares about what else is on that line in particular.

Every line of code in a BASIC program has a line number, which appears at the start of the line. These numbers must be integers in the range 1-65535, but other than that, there are no restrictions, and it's commonplace to leave gaps between numbers in case you decide to insert a line or twelve later. Barring other factors, the BASIC interpreter goes through the code in ascending line number order, then stops. Line numbers are also used for flow control; more on that when it becomes relevant.

Note that, when entering a BASIC program, if you enter a line number that's equal to an existing line number, the line you enter replaces the old line; if you enter a line number before an existing line number, the line you enter is inserted into the program at that point. Feeding a text file to the interpreter wasn't generally supported, but if you try it with a more modern interpreter, it's likely to complain if the line numbers aren't in order, and if it doesn't it'll probably reorder the lines automatically.

Needless to say, line numbers are among the first things ditched by more modern BASICs.

Variables

The most common type of variable in BASIC is the floating-point number. Variable names are one or two characters, with the first character being a letter and the second, if it exists, being a letter or digit. BASIC is not a case-sensitive language, and in the earliest examples didn't use lower case characters at all.

Variables can also contain strings of zero or more characters. (How many characters maximum is platform-dependent, but expect no more than 255, and possibly less.) A string variable has the character $ immediately after its name. Literal strings in code are enclosed by quote marks. I'll discuss putting a quote mark in a string when it becomes relevant.

Some variants of BASIC have additional types of variables. For instance, QuickBASIC offers the suffixes % (integer in the range -32,768 to 32,767), & (long integer in the range -2,147,483,648 to 2,147,483,647), and # (double-precision floating point number, using eight bytes of storage instead of four).

You can also have a variable array. A(3,4) is a specific element of a two-dimensional array of floating point numbers. Arrays can be of any type that variables are, but cannot store mixed types of variables. Arrays are typically zero-indexed (for instance, A(0,4) is a valid element), but many BASIC programs don't take advantage of that.

Variables do not, in general, have to be defined before they can be used. A numeric variable's value defaults to 0, while a string variable's value defaults to "" (the empty string). Arrays are a partial exception to this rule. An array will be automatically defined as having a single dimension with elements 0 to 10 the first time it's used; if you want a larger array (or a smaller one, if you're saving memory) or one with more dimensions, you need to define it first, using the DEF command. (For instance, you might use DEF CH(7,7) to create a two-dimensional array suitable for storing information about a checkerboard.) That said, whether explicitly or implicitly defined, an array's elements have the same default value as variables of that type.

All variables have global scope. That means that they can be read, and changed, anywhere within the program.

Last, but not least, A, A$, A(), and A$() are all unique variables. (You can't have more than one array of the same name and type, though, even if their numbers of dimensions are different.) This can be a source of confusion both in reading code and in porting it to more modern versions of BASIC, which won't let you do that.

I Told You That Story So I Could Tell You This One

10 PRINT TAB(26);"ACEY DUCEY CARD GAME"
20 PRINT TAB(15);"CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"

A lot of BASIC programs start with line number 10 and advance by tens. You'll see why in just a couple of lines. For now, though, let's focus on this one.

The command PRINT sends output to the screen, or in this case the teleprinter. It can take zero or more arguments separated by commas or semicolons; if a comma is used, it advances to the next tab stop, while if a semicolon is used, it doesn't. It can also have a semicolon at the end of the arguments; if it does not, the cursor (or printhead) moves to the start of the next line.

The function TAB, when called by PRINT, advances the cursor to the given column. Note that this does not mean 'advance this many spaces' or, on a screen, 'print a number of spaces'; it doesn't erase text. It's used in this line and the next to center the title of the game, presuming the screen is 80 columns wide.

21 PRINT
22 PRINT
23 PRINT

"Oh hey," said the programmer, "this would look nicer if there were a couple of blank lines here. It's a good thing I have nine whole line numbers unused to put these lines in." (Note that the programmer didn't use any multi-command lines; they could have used PRINT:PRINT:PRINT to get the same effect. Perhaps their version of BASIC didn't support that.)

30 PRINT"ACEY-DUCEY IS PLAYED IN THE FOLLOWING MANNER "
40 PRINT"THE DEALER (COMPUTER) DEALS TWO CARDS FACE UP"
50 PRINT"YOU HAVE AN OPTION TO BET OR NOT BET DEPENDING"
60 PRINT"ON WHETHER OR NOR YOU FEEL THE CARD WILL HAVE"
70 PRINT"A VALUE BETWEEN THE FIRST TWO."
80 PRINT"IF YOU DO NOT WANT TO BET, INPUT A 0"

Nothing much to say here, except that I see a few typos in the instructions.

100 N=100
110 Q=100

Our first two variables! I'm so proud. Some varieties of BASIC provide the LET command to explicitly assign a value to a variable, and I'm sure some varieties of BASIC require it, but none I'm familiar with. As we'll see later, Q is the amount of money we have at any given point, while N will be used for the amount of our bet. (Why was it assigned a value here? I'm not sure. I'll bring it up later.)

The = symbol is used both for assigning a value and for testing for equality. Which it does is determined by context. The command A=B=2 would assign a value to A depending on whether or not B is 2, but it would not change B.

120 PRINT"YOU NOW HAVE ";Q;" DOLLARS"

Looking at this command, you might think there should be only one space on either side of the value of Q, but if you look at the sample output, you'll see two. The rules for spacing when printing a number are one of the things that change most from one interpreter to another. Will they print a space before the number? Will they only do so if the number is positive (so 1 takes the same room as -1)? Will there be a space after the number? The programmer made an incorrect assumption here.

There are some hijinks you can get up to if your flavor of BASIC forces spaces you don't want; I'll cover them if and when they become relevant.

130 PRINT
140 GOTO 260

I mentioned earlier that line numbers are also used for flow control. The GOTO command immediately jumps command execution to the line number given. (If there are commands on the same line after the GOTO, they aren't actually run.) Liberal use of the GOTO can lead to what's known as 'spaghetti code', and is ideally avoided, but if you run out of line numbers in a certain section, it's so tempting to jump somewhere fresh to finish what you're doing...

Note that whether you can put an equation instead of a number and jump to the line number the equation equals is, again, dependent on the interpreter, as is what happens if you try to jump to a line number that doesn't exist.

We'll skip over lines 210-250, much as the interpreter is, for now.

260 PRINT"HERE ARE YOUR NEXT TWO CARDS "
270 A=INT(14*RND(1))+2

You can't have a game about drawing cards without randomness! The RND function works differently on different interpreters depending on the number you feed it, but in this particular flavor of BASIC, feeding it a positive number causes it to return a theoretically random number from 0 to not quite 1. Multiplying it by 14 gives a number between 0 and not quite 14; the INT function returns that number rounded down to the next integer, so we have 0-13. Last, but not least, we add 2, so we now have 2-15.

280 IF A<2 THEN 270
290 IF A>14 THEN 270

Which is weird, because if we do get a 15, we immediately throw it away. The IF - THEN command checks if the equation between IF and THEN is true; if it is, it executes the remaining commands on the line, but if it isn't, it skips them. If you just give a number, the command GOTO is assumed.

Note that, if the equation in line 280 is ever true, something weird is going on.

300 B=INT(14*RND(1))+2
310 IF B<2 THEN 300
320 IF B>14 THEN 300

We pick a second random number, with the same weirdness about its bounds, here. Note the repeated code, a common source of errors in BASIC. Many flavors of BASIC have some way to avoid this, but it's often limited.

330 IF A>=B THEN 270

Here's a bit of sloppy code. If the first card we draw is higher than or equal to the second, we throw both cards away and start over. What I'd do is start over if the cards are equal, but do IF A>B THEN ZZ=A:A=B:B=ZZ to swap the cards. (Variables starting with Z were always my favorite for 'a value I just need for a little bit', and ZZ was the most common of those.)

Actually, I'd do quite a bit more regarding the implementation of drawing cards in this program, but I'll get to that some other time.

350 IF A<11 THEN 400
360 IF A=11 THEN 420
370 IF A=12 THEN 440
380 IF A=13 THEN 460
390 IF A=14 THEN 480
400 PRINT A
410 GOTO 500
420 PRINT"JACK"
430 GOTO 500
440 PRINT"QUEEN"
450 GOTO 500
460 PRINT"KING"
470 GOTO 500
480 PRINT"ACE"

All of this code is just to say that if A is less than 11, print the number A; otherwise, print the name of the face card (or the ace, which is high in this game). The version of BASIC the programmer is using must be very limited to require these gymnastics; I can think of a number of ways to improve on this.

(In the sample printout, you can see that there's a space in front of the numbers, but not in front of the named cards. There are a few ways this could be handled.)

500 IF B<11 THEN 550
510 IF B=11 THEN 570
520 IF B=12 THEN 590
530 IF B=13 THEN 610
540 IF B=14 THEN 630
550 PRINT B
560 GOTO 650
570 PRINT"JACK"
580 GOTO 650
590 PRINT"QUEEN"
600 GOTO 650
610 PRINT"KING"
620 GOTO 650
630 PRINT"ACE"
640 PRINT
650 PRINT

And by repeating the code, we introduce a bug. Note that, if the second card is an ace, we print two lines after it, but if it's any other card, we only print one.

660 INPUT"WHAT IS YOUR BET";N

The INPUT command works just like the PRINT command, except its last argument is a variable into which player input is put. In this case, it prints WHAT IS YOUR BET. If a comma followed this, it would advance to the next tab, but instead it leaves the cursor in place. Then (in this flavor of BASIC, at least) it prints a question mark and a space before allowing the user to enter something, which is stored in N.

Note that we don't check to see whether what the player entered is a number. In fact, if it's not, the interpreter is well within its rights to stop execution. (In Commodore BASIC V2, it displays the error ?REDO FROM START, then displays the prompt again.) This is only the first place where the programmer assumes the player isn't making a mistake.

Note also that it is legal to have the command INPUT A. In this case, it would just print a question mark and a space before taking input.

Note also also that in at least some flavors of BASIC, entering nothing at all doesn't change the variable's previous value. This may be why N was assigned a value back in line 100.

670 IF N<>0 THEN 680
675 PRINT"CHICKEN!!"
676 PRINT
677 GOTO 260

The wedged-in lines suggest that the taunt was a last-minute addition. Line 670 probably originally read IF N=0 THEN 260. (Since it hasn't come up yet: <> is BASIC's 'is not equal to' operator.)

680 IF N<=Q THEN 730
690 PRINT"SORRY, MY FRIEND BUT YOU BET TOO MUCH"
700 PRINT"YOU HAVE ONLY ";Q;" DOLLARS TO BET"
710 GOTO 650

And this is the last of the game's sanity checks. What do you think has been missed out?



The correct answer is 'checking that the player entered a positive integer'. It will happily let us bet -3829.7386 dollars.

730 C=INT(14*RND(1))+2
740 IF C<2 THEN 730
750 IF C>14 THEN 730
760 IF C<11 THEN 810
770 IF C=11 THEN 830
780 IF C=12 THEN 850
790 IF C=13 THEN 870
800 IF C=14 THEN 890
810 PRINT C
820 GOTO 910
830 PRINT"JACK"
840 GOTO 910
850 PRINT"QUEEN"
860 GOTO 910
870 PRINT"KING"
880 GOTO 910
890 PRINT"ACE"
900 PRINT

Yet again, we have repeated code, and the out-of-bounds bug and the issue with only printing a blank line after an ace rear their ugly heads.

910 IF C>A THEN 930
920 GOTO 970
930 IF C>=B THEN 970
950 PRINT"YOU WIN!!!"
960 GOTO 210

Each card has a numeric value from 2 to 14, with 11, 12, 13, and 14 being Jack, Queen, King, and ace respectively. We check here if the player has won or not. This is another place where the code is unnecessarily convoluted. If the player wins, we go to line 210...

210 Q=Q+N
220 GOTO 120

...which applies their winnings and goes to show them. If they lose, however, we go to line 970...

970 PRINT"SORRY, YOU LOSE"
980 IF N<Q THEN 240

...and unless they've bet their last dollar...

240 Q=Q-N
250 GOTO 120

...we go there. But what if they have?

990 PRINT
1000 PRINT
1010 PRINT"SORRY, FRIEND BUT YOU BLEW YOUR WAD"
1020 INPUT"TRY AGAIN (YES OR NO)";A$

Note that we can collect input in a string. Note also that A$ is a different variable from A.

Also note that there's a bug in line 980. Do you see it? Here's a hint: it's technically not a bug in that line.

1030 IF A$="YES" THEN 110
1040 PRINT"OK HOPE YOU HAD FUN"

Finally, note that the player must type YES, exactly, to have another go. Typing Y will not suffice.

1050 END

The END command immediately ends program execution. There's no point in having it at the end of the program, but it does have its uses.

The final analysis
Leaving aside how fun it is to play a gambling game on a teleprinter (and there are several more gambling games to come), this game is coded using the bare basics, pardon the pun, of the BASIC language. Even without using other features, the code could be cleaned up, and we found a few bugs. Bringing in features like arrays and subroutines (which we haven't touched on) would make significant improvements. And the presentation could be fixed up too.

So if you were sitting down to this game back in the day, saying to yourself, "I could do better than this," what would you want to change?

Tiggum
Oct 24, 2007

Your life and your quest end here.


I haven't tested this but I think this should work.
code:
10 PRINT TAB(26);"ACEY-DUCEY CARD GAME"
20 PRINT TAB(15);"CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"
30 PRINT
40 PRINT
50 PRINT
60 PRINT"ACEY-DUCEY IS PLAYED IN THE FOLLOWING MANNER:"
70 PRINT"THE DEALER (COMPUTER) DEALS TWO CARDS FACE UP."
80 PRINT"YOU HAVE AN OPTION TO BET OR NOT BET DEPENDING"
90 PRINT"ON WHETHER OR NOR YOU FEEL THE NEXT CARD WILL"
100 PRINT"HAVE A VALUE BETWEEN THE FIRST TWO."
110 PRINT"IF YOU DO NOT WANT TO BET, INPUT A 0."
120 PRINT "TO QUIT, ENTER -1."
130 N=100: Q=100: DIM D(52)
140 Y=0
150 FOR Z=1 TO 4
160 FOR X=1 TO 13
170 Y=Y+1
180 D(Y)=X
190 NEXT
200 NEXT

300 PRINT
310 PRINT"YOU NOW HAVE ";Q;" DOLLARS"
320 PRINT
330 PRINT"HERE ARE YOUR NEXT TWO CARDS "
340 PRINT
350 A=INT(52*RND(1))+1
360 B=INT(52*RND(1))+1
370 IF A=B THEN GOTO 360
380 IF D(A)=D(B) THEN GOTO 360
381 IF D(A)=D(B)+1 THEN GOTO 360
382 IF D(A)=D(B)-1 THEN GOTO 360
390 C=INT(52*RND(1))+1
400 IF C=A OR C=B THEN GOTO 390
410 P=A
420 GOSUB 1000
430 P=B
440 GOSUB 1000
450 INPUT"WHAT IS YOUR BET?";N
460 IF N=0 THEN GOTO 300
470 IF N=-1 THEN GOTO 700
480 IF N<0 THEN GOTO 450
490 IF N>Q THEN GOTO 450
500 P=C
510 PRINT
520 PRINT "YOUR CARD IS: ";
530 GOSUB 1000
540 IF D(A)<D(C) AND D(C)<D(B) THEN GOTO 600
550 IF D(A)>D(C) AND D(C)>D(B) THEN GOTO 600
560 PRINT "BAD LUCK."
570 Q=Q-N
580 IF Q=0 THEN GOTO 700
590 GOTO 300

600 PRINT "WELL PLAYED."
610 Q=Q+N
620 GOTO 300

700 PRINT "GAME OVER!"
710 PRINT "YOU HAVE $";Q
720 END

1000 IF D(P)<11 AND D(P)>1 THEN PRINT D(P);" ";
1010 IF D(P)=1 THEN PRINT "ACE ";
1020 IF D(P)=11 THEN PRINT "JACK ";
1030 IF D(P)=12 THEN PRINT "QUEEN ";
1040 IF D(P)=13 THEN PRINT "KING ";
1050 IF P<14 THEN PRINT "OF HEARTS."
1060 IF P>13 AND P<27 THEN PRINT "OF SPADES."
1070 IF P>26 AND P<40 THEN PRINT "OF CLUBS."
1080 IF P>39 THEN PRINT "OF DIAMONDS."
1090 PRINT
1100 RETURN
You could do it without the GOSUB, obviously, either by using an incrementing variable and GOTOs or just repeating the section of the code. You could also use a proper function instead of GOSUB if you have those. I can't remember how they work in BASIC though. Also, I think I might have messed up the spacing after PRINTed numbers.


Edit: Fixed some stuff after checking if it actually works or not (on this site). It does now.

Tiggum fucked around with this message at 13:47 on Jun 22, 2020

Tiggum
Oct 24, 2007

Your life and your quest end here.


And I just realised I should have added
code:
381 IF D(A)=D(B)+1 THEN GOTO 360
382 IF D(A)=D(B)-1 THEN GOTO 360
because there's no point betting if you can't possibly win.

Carbon dioxide
Oct 9, 2012

I was thinking, assume this basic interpreter is so simple there's no support for subroutines or functions or whatever, this game is simple enough that you could wing it.

Just have the repeated code (e.g. the card printing function) in a block of line numbers at the end like Tiggum had.

Then, you need a single extra variable on top of the P variable that Tiggum introduced, one that's the equivalent of a call stack. Doesn't even need to be an array because you'll call every subroutine directly from your main. Just set it to the line number of the "GOTO 1000" + 1 and that should do the trick, as long as you replace the "RETURN" to GOTO whatever you stored in that variable.

Tiggum
Oct 24, 2007

Your life and your quest end here.


Carbon dioxide posted:

I was thinking, assume this basic interpreter is so simple there's no support for subroutines or functions or whatever, this game is simple enough that you could wing it.

Just have the repeated code (e.g. the card printing function) in a block of line numbers at the end like Tiggum had.

Then, you need a single extra variable on top of the P variable that Tiggum introduced, one that's the equivalent of a call stack. Doesn't even need to be an array because you'll call every subroutine directly from your main. Just set it to the line number of the "GOTO 1000" + 1 and that should do the trick, as long as you replace the "RETURN" to GOTO whatever you stored in that variable.
And if it GOTO variable isn't allowed then you can just use IF calledfrom = 1 THEN GOTO resumepoint etc. That's what I meant by "using an incrementing variable and GOTOs".

Kangra
May 7, 2012

Hell is other people's code.

I loved the Ahl book and used to go to the library and just read through it. I tried a few of the longer games once we had a computer at home.

My instinct any time there's a deck of cards is to dim an array of strings, which could be done here. Gets rid of the whole subroutine issue as well, because you just have PRINT R$(A) or whatever. I feel like for a lot of the programmers working at the time, there's a tendency to avoid using up space unless there was a necessity for it, because they may have been coming from an era where memory was nearly nonexistent. On the other hand. having enough memory for your code is an issue as well.

Gnoman
Feb 12, 2014

Come, all you fair and tender maids
Who flourish in your pri-ime
Beware, take care, keep your garden fair
Let Gnoman steal your thy-y-me
Le-et Gnoman steal your thyme




Keeping the source code compact is pretty valuable in this context, given that you're expected to type it in. Hard to learn to program when the sample is really long, and you have to worry about typos throwing it off.

Tiggum
Oct 24, 2007

Your life and your quest end here.


Kangra posted:

My instinct any time there's a deck of cards is to dim an array of strings, which could be done here. Gets rid of the whole subroutine issue as well, because you just have PRINT R$(A) or whatever.
You'd still need to track the card values separately though to be able to compare them, which strikes me as adding a little bit of unnecessary complication.

Carbon dioxide
Oct 9, 2012

I've heard that there used to be programming shows on the radio. They would literally play a bunch of weird noises but if you recorded that to a tape and then put that in your home computer's tape reader the result would actually be a program or game you could run.

Tiggum posted:

You'd still need to track the card values separately though to be able to compare them, which strikes me as adding a little bit of unnecessary complication.

You COULD use the array indices for that, especially if you use a 2D array where the first dimension is the suits.

In a modern language I would honestly just make a card data type that contains a name, suit, and value and just dump one of each in a big list and be done with it.

Carbon dioxide fucked around with this message at 15:14 on Jun 22, 2020

Tiggum
Oct 24, 2007

Your life and your quest end here.


Carbon dioxide posted:

You COULD use the array indices for that, especially if you use a 2D array where the first dimension is the suits.

Oh yeah, that is an excellent idea that I'm annoyed I didn't think of. :doh:

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
I should have expected some people to already be familiar with BASIC and want to do it themselves rather than just throw out suggestions! Let's see what we've got.

Tiggum posted:

I haven't tested this but I think this should work.

Okay, let's see what you added.

Subroutines

You're making use of a subroutine! Subroutines are shown in later programs in the book, so let's go ahead and cover them now. The GOSUB command tells the interpreter to go to a different line and start executing the code there. The difference between that and GOTO is, eventually you (should) use the command RETURN, which tells the interpreter to go back. (That means, among other things, that code after a GOSUB on the same line does eventually get executed, while code after a GOTO does not.)

That said, subroutines are very limited in the BASIC of the era. The only thing GOSUB gives you is the ability to return from whence you came. Subroutines don't have variables of their own, and the only way you can pass them values or have them return values is to pre-arrange variables set aside for that purpose. You can nest GOSUB commands, but recursively calling a subroutine isn't useful. And because the code of the subroutine isn't special, you have to make sure execution doesn't fall through to them, which is one of the two primary uses of the END command. (The other being stopping the program immediately, of course.)

Tiggum's Rewrite

Your subroutine takes the value of P (the program copies the value of interest into P before calling the subroutine) and prints the name of a card, followed by a blank line. But what is P? It's a number between 1 and 52, because you've changed the program so it tracks suits as well, even though they're not relevant to the game. Then you're using D() to translate these internal card values into ranks. I'd make use of modulo math myself, but I haven't introduced that, and at any rate your approach works. You've made a mistake, though: in Acey Ducey, aces are high, and your code assumes they're low. I'd change line 160 to FOR X=2 TO 14 to fix this and make adjustments elsewhere accordingly.

You've got code in to make sure you draw three different cards and to skip unwinnable scenarios. Cool beans. And you've added code to allow the two first cards to be either high-low or low-high and handle the scoring properly. That's not the approach I suggested before, but it certainly works!

You haven't really done anything in the vein of sanity-checking input, but that's fine; I haven't introduced those tools anyway. And you've added a way to quit before you bust. That's a nice quality-of-life feature.

I see you tested this in Applesoft BASIC, which means line 310 is correct, since that variant of BASIC doesn't put spaces around numbers. I'm not sure why you've inserted GOTO after each THEN command, though; it may be required in some BASIC variants, but not that one. That said, the Apple ][ has a 40-column screen. How would you adapt the program to take that into account?

A tiny nit: you have an extraneous space at the end of line 330.

Carbon dioxide posted:

I was thinking, assume this basic interpreter is so simple there's no support for subroutines or functions or whatever, this game is simple enough that you could wing it.

The only problem with your suggestion is -

Tiggum posted:

And if it GOTO variable isn't allowed then you can just use IF calledfrom = 1 THEN GOTO resumepoint etc. That's what I meant by "using an incrementing variable and GOTOs".

- beaten, both on the nitpick and on the workaround.

As for the discussion on storing card names in a string array, that could work! There are a couple of ways to do it. One approach would be DATA statements, which I'll cover when they become useful. The approach I'd take, though, is to run Tiggum's subroutine on every possible card value; instead of printing the strings, it'd store them in the entries of the array. Then the program can just print CD$(5, 1) to (say) print 5 OF HEARTS.

Carbon dioxide posted:

I've heard that there used to be programming shows on the radio. They would literally play a bunch of weird noises but if you recorded that to a tape and then put that in your home computer's tape reader the result would actually be a program or game you could run.

I've seen one better on a Twitch channel that streams old computer-related TV shows. One program had a DIY project, spread out over multiple shows, to build a light sensor that would decode flashes into the sounds that one of the computers of the day used on its tape drive. Once you'd built the sensor, you would then stick it to your TV screen at the appropriate time, and they'd flash a light in that corner of the screen, and you'd record the output of that. There's also more than one vinyl record out there that has a program recorded on a section of track that the needle never goes to on its own. Some crazy Easter eggs of the era, lemme tell you.

Anyway. I'd had some plans of my own on how to improve this program, starting with actually implementing a deck of cards (so you don't pull the ace of hearts multiple times until the deck's mostly been gone through). Then I realized I was missing a step, and I looked up the actual rules for Acey Deucey. Not only did I learn that that is the correct spelling of the name, I learned that it's actually a multiplayer game!

Wikipedia posted:

Before the action, each player must add their ante into the pot. Two cards are then dealt face-up to one player. That player then bets from nothing to the amount that is in the pot at the time whether or not the third card will numerically fall in between the first two. If the third card falls in between the two other cards, the bettor takes the amount he bet out of the pot; if the third card falls outside of the two other cards, the bettor must add what he bet to the pot; and if the third card matches the numerical value of one of the other two cards, the bettor must add to the pot double what they bet. If two cards of the same value come up, e.g. 2,2 the bettor picks if the next card will be higher or lower and bets. If the next card is the same as the last two, i.e. a 2, the bettor must triple their bet.

In addition to this, there is a special rule for Aces. If the first card turned is an Ace the player may choose its value as either the high Ace or the low one. Low Ace is always lower than any other card, including the deuce. If an Ace comes up as the second card turned it is always considered the high Ace. If a player "Posts" on an Ace they are required to pay four times their bet for that hand. Aces also cause an automatic loss if it is the third card turned when the first two cards are a match, e.g. 6,6. The best spread in the game is considered to be a low Ace on the left and a high Ace on the right. This is also one of the worst hands to get as you run the risk of the third card being an Ace and having to pay four times your bet for the hand.

So here's a project for you: how would you rewrite the game so that you're playing against a computer opponent using these rules? If you don't want to write actual code, that's fine; if you do, but you're inexperienced with BASIC, just ask 'how would I' and I'm sure someone will be glad to help you out. I'll put my answer to the question up some time later.

e: if making a computer opponent seems too daunting, you can make it a two-player game. If making the game at all seems too daunting, don't worry! I'm not collecting assignments or anything, and we'll move on to the next program in due course.

FredMSloniker fucked around with this message at 20:33 on Jun 22, 2020

Tiggum
Oct 24, 2007

Your life and your quest end here.


FredMSloniker posted:

I'm not sure why you've inserted GOTO after each THEN command, though; it may be required in some BASIC variants, but not that one.
Mostly just habit. But also I think it's a little easier to read with them in.


FredMSloniker posted:

That said, the Apple ][ has a 40-column screen. How would you adapt the program to take that into account?
Yeah, the lines run over the edge of the screen on that. It's an easy fix - just count characters and insert line breaks - but I couldn't be bothered.

ManxomeBromide
Jan 29, 2009

old school
Oh, this should be fun to follow along with, and I like the idea of this being more about reading the code as opposed to trying to actually pretend to enjoy the games. I too grew up with the Ahl books in the library, and thanks to your posts earlier in the retrogaming forum, I wound up nerdsniping myself with the next game in this article.

... good luck explaining the program code as-is. I managed to work out its logic, but my conclusion for improving the program involved burning something like 80% of it completely to the ground.

VomitOnLino
Jun 13, 2005

Sometimes I get lost.
I just wanted to honor the OPs excellent post by both typing in the listing and running it on actual C64 hardware. I could of course have copied the text of the listing over (be sure to convert it to lowercase for the C64 first in that case!) and done it like that, but it wasn't a particularly huge listing, so I typed it in from the posted listing as that was more legible than the magazine-scan.

As you can see it works perfectly and there are no typos I can see. Sorry for the poo poo-tastic image of the CRT, obviously looks much better in person.


After this, I bet the house and lost...

EDIT: So that this is not just content free gush: On the C64, PET, C16 and the likes you can speed up this code a tiny bit by substituting RND(1) for RND(.) - it does the same thing but is faster as all of these machines are slower with integers than they are with floats. This is apparently a result of being severely constrained for ROM space when they implemented BASIC on those. Go figure.

VomitOnLino fucked around with this message at 06:10 on Jun 23, 2020

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
So I whacked together a game of Acey Deucey following the rules off of Wikipedia in several hours. I didn't want to bother coding a CPU opponent, so it requires 2-8 players. For whatever reason, I decided to only have one statement per line, which means some of the code is kind of messy. And even though I verified my code in Commodore BASIC, I didn't use any of that language's unique features, like actually having card suits in the character set. So I'm not going to throw the code for the whole thing up unless there's genuine interest (it's 190 lines). That said, I'm rather proud of this subroutine I worked up for handling drawing a card from the deck. See, since we don't need the suits, just the ranks, it's possible to do it fairly compactly.

code:
1420 IF TC > 0 THEN 1480
1430 PRINT "I NEED TO SHUFFLE THE DECK."
1440 TC = 52
1450 FOR FZ = 1 TO 13
1460 RC(FZ) = 4
1470 NEXT
1480 FA = INT(RND(1) * TC)
1490 FZ = 1
1500 IF FA < RC(FZ) THEN 1540
1510 FA = FA - RC(FZ)
1520 FZ = FZ + 1
1530 IF FZ < 13 THEN 1500
1540 RC(FZ) = RC(FZ) - 1
1550 TC = TC - 1
1560 FZ = FZ + 1
1570 RETURN
TC (short for Total Cards) is the number of cards remaining in the deck. If it's 0, as it is at the start of the game, we fall through to 1430, print a message, set it to 52, then set the 13 elements of RC() to 4. (Shuffling two decks together would be as simple as changing 52 to 104 and 4 to 8. Similarly, if we changed the condition in line 1420, we could shuffle the deck before it ran empty.)

Once that's out of the way, we set FA to a random number in the range [0, TC - 1]. (Variables starting with F are my choice for variables that a subroutine will use, whether for input, output, or internal processing.) We also set FZ to 1. (We used it briefly as a loop variable, but it's going to be our output.)

The section from 1500 to 1530 is where the magic happens. IF FA < RC(FZ) at this point, then FA is in the 0-3 range, so we have one of the four twos (why twos? I'll get to that) and jump to 1540. If it isn't, though, we knock RC(FZ) off of FA, add one to FZ, and loop back around. Eventually FA will get low enough that we'll have found the rank of the card we drew. (Instead of shuffling the deck, we're counting down a random number of cards and drawing that.) Line 1530's IF isn't strictly needed, but I put it in when doing some debugging, and it doesn't do any harm to leave it there.

Lines 1540 and 1550 remove the card from the deck. (I forgot 1550 when I was first coding, which first caused a crash when FZ went over 13, then, when I added 1530's IF, caused too many aces to start showing up before I realized why.) Then line 1560 adds 1, moving the range to 2-14 (with 14 being an ace high - in the main body of code, when the rules permit an ace to be low, its internal value is 1) before I return from the subroutine.

Writing this up, I realize there's a major bug in the code, though. It just about works if you're drawing one card at a time and discarding them immediately, but if you need to draw three cards and there are only two left in the deck, the third one could be a duplicate of either of the first two - and that's not even considering if you were playing a game where the players had hands of cards! What I should do is have a second array for the discard deck, with its own total cards value and rank variable array; I can 'shuffle' it back into the play deck by adding the corresponding array values and card counts together, then setting the discard deck's numbers to 0.

But I'll revisit this the next time we look at a card game - and yes, there will be a next time. For now, though, our guest speaker was so eager to get started that he's barged in on the previous lesson, so put your hands together for ManxomeBromide as we move to the next program in the book, Amazing!



I'll cover whatever he doesn't cover once I've fully parsed this myself. It looks like that might take a while.

ManxomeBromide
Jan 29, 2009

old school
I've still got some prep work to do on it, but I'm going to just note before crashing out that there is a functionality-breaking typo in this listing. Line 695 is incorrect. It should be possible to figure out what it should be even without working out the full details of the listing.

Carbon dioxide
Oct 9, 2012

This BASIC code is hard to read. I'll just wait for you folks to come up with the bit-by-bit explanation if you don't mind.

ManxomeBromide
Jan 29, 2009

old school
Guest Lecture: Amazing

I'm not going to do a line-by-line exegesis of the code the way FredMSloniker has, for three reasons. First, many of the basics have already been explained, at a line-by-line level. Second, this code is extremely repetitive and, as we'll see, also very inefficient of programmer time. Finally, looking at the trees gives us no insight into the forest here.

What I will mostly instead be doing is describing how I analyzed the program, expecting to replicate it. There is one new BASIC construct in the code, though, so I will point that one out when we reach it.

That said, the first thing I did with the program was type it in while being mindful of what it was I was typing. I would not expect full understanding, but a core skill here is to be able to maintain incomplete truths in your head as you go, trusting that you will be able to make sense of them later.

The Prologue

The opening is mostly boilerplate, but we're definitely not off to a good start here:

code:
10 PRINT TAB(28);"AMAZING PROGRAM"
20 PRINT TAB(15);"CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY"
30 PRINT:PRINT:PRINT:PRINT
100 INPUT "WHAT ARE YOUR WIDTH AND LENGTH";H,V
102 IF H<>1 AND V<>1 THEN 110
104 PRINT "MEANINGLESS DIMENSIONS.  TRY AGAIN.":GOTO 100
110 DIM W(H,V),V(H,V)
120 PRINT
130 PRINT
140 PRINT
150 PRINT
The first three lines print the program header, and then lines 100-104 are a loop that prompts the user for dimensions until it gets a result it's happy with. Unfortunately, line 102 is the one doing the check here, and it's checking only that neither height nor width is exactly equal to 1. It's possible to enter 0, and in any BASIC I've ever used it's also possible to enter negative numbers, and this won't check for those. Better to instead check for greater than here.

Note that even though we're using IF/GOTO here, this is still basically a do-while loop in a language like C or Java.

Line 110 then dimensions two arrays named W and V that match the dimensions we've set. This is the first place I have to deviate in my personal copy of the code; I'm using a QBASIC-compatible variant of BASIC for this and that means I can't have a variable and an array be named V. My personal copy gets H and V renamed XM and YM, for X-Max and Y-Max.

Lines 120-150 produce a symmetrical spacing around the prompt to match line 30.

code:
160 Q=0:Z=0:X=INT(RND(1)*H+1)
Q and Z are mysteries to us right now, but at least they're both zero. X is a random X coordinate.

code:
165 FOR I=1 TO H
170 IF I=X THEN 173
171 PRINT ".--";:GOTO 180
173 PRINT ".  ";
180 NEXT I
190 PRINT "."
This is a FOR loop with an IF-GOTO and a standalone GOTO in it. If we actually trace through the possibilities, though, this is also representing a computation that can be readily stated without GOTO, should we have the expressive power to state it:
pre:
Repeat with I running from 1 to H
    If I=X
        PRINT ".--";
    Otherwise
        PRINT ".  ";
PRINT "." and end the line
This is printing out the first line of the maze, and the random X coordinate we chose back in line 160 was to select which column held the entrance to the maze.

(As an aside: when some thicket of GOTOs turns out to be organized in a way that I can write it out as clean Pascal-like pseudocode, I will be calling it well-structured. Otherwise, I will be calling it ill-structured.)

code:
195 C=1:W(X,1)=C:C=C+1
200 R=X:S=1:GOTO 260
The last bit of code in the program's prologue initializes three variables that are new to us: C, R, and S. It is not immediately clear why line 195 isn't just 195 W(X,1)=1:C=2 but it may be that the meanings of the variables are such that the author thought this was making it clearer.

The prologue ends by jumping into the middle of the next chunk of code. We have left the land of well-structured code behind.

The Main Program Logic

... is a big, ill-structured mess, in a way that makes it very hard to pin down what's actually happening on a first pass through the code. I do deduce some things about the variables, though:
  • Lines 210-1000 are some kind of master control loop.
  • C indexes that loop somehow, and seems to be counting up to H*V+1; we're essentially looping once for every cell in the grid.
  • W is only ever assigned the current value of C, and elements of W are regularly checked against the value 0. W is tracking a gradually spreading property of the grid, and a good first guess is that it's marking which cells in the grid have been "dug out" to build the maze, with 0 meaning "it hasn't been" and any other value being the time point where it was dug out. This makes line 195 make a little more sense; It's maintaining the invariant that W is only ever assigned the current value of C.
  • V is more mysterious; it's basically write-only; the only times its values are ever checked are right before overwriting them with something else. Since it's only ever assigned constants, though, we can restrict its set of possible values to 0-3.
  • X is consistently used as a value of a random choice of something. In the main loop the thing being chosen is a random location to GOTO, but it's being done in a controlled manner that we'll analyze when we get to it.
  • Q and Z are still mysteries, and they're tightly bound up with the most ill-structured parts of the code, but neither takes a value other than 0 or 1, and Z was assigned 0 in the prologue. If Z is modified in the main loop, though, the only possible value it can take is 1. Q and Z are event flags of some kind, and Z represents something irrevocable. Q, on the other hand, has its value bounce back and forth a lot.
Yuck. On the other hand, there's only one way out of the 210-1000 line block, and it's via one of four jumps to line 1010. There are fully four ways this "loop" can loop back—to lines 210, 250, 260, or 530—but there's only one point you can be at when you exit.

So let's put this mess behind us momentarily and look at the final part of the program, lines 1010-1073.

Printing Out The Maze

This is a series of nested, well-structured loops full of equally well-structured conditional printing statments:
code:
1010 FOR J=1 TO V
1011 PRINT "I";
1012 FOR I=1 TO H
1013 IF V(I,J)<2 THEN 1030
1020 PRINT "   ";
1021 GOTO 1040
1030 PRINT "  I";
1040 NEXT I
1041 PRINT
1043 FOR I=1 TO H
1045 IF V(I,J)=0 THEN 1060
1050 IF V(I,J)=2 THEN 1060
1051 PRINT ":  ";
1052 GOTO 1070
1060 PRINT ":--";
1070 NEXT I
1071 PRINT "."
1072 NEXT J
1073 END
Two loop variables, I, and J. The outer loop runs J from 1 to V, so is iterating over rows in the maze. The two inner loops loop I from 1 to H, iterating over each column twice. Each cell in the printout is two lines tall, basically, so the first loop is drawing the "middle" of each cell in the grid, and the second loop is drawing the southern wall of it. We drew the very first line of the maze up in the function prologue, presumably because that first line wasn't any cell's southern wall.

I'm a little bit cranky about that, to be honest; there's a very noticable delay between the first line of the maze and the rest of it, when you run it. Better, I think, to rely on the fact that the values in W() are ever increasing. We don't need to dump the top row right away, just as we've determined what the first cell is; as we discovered in the main program loop, writes to W() are ever-increasing and so we could identify column I as the entrance simply by seeing if W(I,1) was 1!

Lines 1045-1060 print out the southern walls of each cell based on, essentially, whether or not V(I,J) is even or not; it's checking against the specific values of 0 and 2, but we noticed when typing in the main program loop that V never receives a value above 3.

Similarly, and moving backwards slightly, the westernmost wall has always exists and is unconditionally printed at the start of each row by line 1011, and lines 1013-1030 conditionally print out an eastern wall depending on whether the value in V(I,J) is less than 2 or not; a value of 0-1 means there is a wall, and a value of 2-3 means there is a passage.

This means that we now know what the V() array is for! It's tracking the actual shape of the maze, and each entry is basically two flags. Each cell's value starts at zero. If there is a passage in this cell to the south, add 1. If there is a passage to the east, add 2.

This also means there's no need for any special logic to print out the exit the way we needed it for the entrance; it is stored in V() the same way everything else was.

Furthermore, alas, we have nowhere else to hide. We must now dive into the maelstrom of the maze generation logic.

Deciphering the Main Program Loop

The first time we enter the main loop from lines 210-1000, we skip to line 260. What did we miss?

code:
210 IF R<>H THEN 240
215 IF S<>V THEN 230
220 R=1:S=1:GOTO 250
230 R=1:S=S+1:GOTO 250
240 R=R+1
250 IF W(R,S)=0 THEN 210
This code has a GOTO on every line but 1, but it's actually well-structured!
pre:
Repeat
    If R is not H, increment R.
    Otherwise, if S is not V, reset R to 1 and increment S.
    Otherwise, R and S are both reset to 1.
While W(R,S) is zero
Not only is it well-structured, it actually defangs the loopback we saw to line 250. Line 250 is the "while" check here; essentially, when it looped back to 250 it was actually saying "If W(R,S)=0, loop back to 210; otherwise loop back to 260." We've dropped the number of ways to start an iteration from four to three, conceptually.

This code also shows R and S properly in use. R and S were initialized to X and 1. Throughout the program, they are only ever used by checking their values against 0, H, or V (H always for R, and V always for S), or as an index into the W() and V() arrays. Their initial value is (X, 1)—the maze entrance. When written, they are only ever incremented, decremented, or force-set to 1.

R and S are specifying a pair of coordinates within the maze grid, serving much the same purpose as a pointer or a reference, just, well, in a language that has neither. If you find some old enough textbooks, you will see this technique given a name: the (R,S) pair is a cursor into V() and W(). And very like a cursor, what this loop is doing is advancing the cursor through the maze as if it were scanning text on a page, looking for the next cell for which W(R,S) is not 0. If it hits the end, it starts over at the top left. This is a routine that causes the cursor to scan for the next maze cell that has already been visited.

The next wodge of code to structure is rather large and indigestible:
code:
260 IF R-1=0 THEN 530
265 IF W(R-1,S)<>0 THEN 530
270 IF S-1=0 THEN 390
280 IF W(R,S-1)<>0 THEN 390
290 IF R=H THEN 330
300 IF W(R+1,S)<>0 THEN 330
310 X=INT(RND(1)*3+1)
320 ON X GOTO 790,820,860
330 IF S<>V THEN 340
334 IF Z=1 THEN 370
338 Q=1:GOTO 350
340 IF W(R,S+1)<>0 THEN 370
350 X=INT(RND(1)*3+1)
360 ON X GOTO 790,820,910
370 X=INT(RND(1)*2+1)
380 ON X GOTO 790,820
390 IF R=H THEN 470
400 IF W(R+1,S)<>0 THEN 470
405 IF S<>V THEN 420
410 IF Z=1 THEN 450
415 Q=1:GOTO 430
420 IF W(R,S+1)<>0 THEN 450
430 X=INT(RND(1)*3)+1
440 ON X GOTO 790,860,910
450 X=INT(RND(1)*2+1)
460 ON X GOTO 790,860
470 IF S<>V THEN 490
480 IF Z=1 THEN 520
485 Q=1:GOTO 500
490 IF W(R,S+1)<>0 THEN 520
500 X=INT(RND(1)*2+1)
510 ON X GOTO 790,910
520 GOTO 790
The first two lines, at least, are a relief: they are two IF statements that branch to 530 (the line right after this block) if one of two conditions hold. This means we can make the master loop well-structured with the addition of two extra flags:
pre:
Loop forever:
    If NeedScan is true:
        (lines 210-250)
    If DoFirstBit is true:
        (lines 260-520)
    (rest of loop)
We could then replace GOTO 210 with "Set NeedScan and DoFirstBit to true, skip to next iteration", GOTO 250 with "Set DoFirstBit to true, and NeedScan to whatever W(R,S)=0 evaluates to, skip to next iteration", GOTO 260 with "Set NeedScan to False and DoFirstBit to true, next iteration" and GOTO 530 with "Set NeedScan and DoFirstBit to false, next iteration". GOTO 1010 translates to "break out of the loop." We do not actually do this, mind you, but if we wanted to translate this code into JavaScript or something as closely as possible without using GOTOs or simulations of them, this would let us do it.

It is also worth asking why line 260 isn't 260 IF R-1=0 OR W(R-1,S)<>0 THEN 530. (One might also ask why it's not testing R=1 rather than R-1=0.) This turns out to vary by BASIC dialect. In QBASIC, you could collapse those lines without incident, but in GW-BASIC, there's an ugly corner case. Tests that include AND and OR, in modern languages, are said to short-circuit; once it knows what the result must be it stops evaluating terms. This is true in QBASIC, but in GW-BASIC every term is evaluated. That means that line 260 is fine, but if you were to collapse, say, lines 290 and 300 into IF R=H OR W(R+1,S)=0 THEN 330. it would check if R=H, and then even if it was it would check to see if W(R+1,S)=0, which probably will crash your interpreter since we only DIMensioned W() to have a maximum width of H! QBASIC, like C, Javascript, or even Lisp (predating BASIC by a decade), halt evaluation when the first term comes out true.

If I were doing this, and I had the spare memory—and for any maze that will actually fit on my screen, I absolutely do, even if I only have 8KB of RAM—I would collapse them anyway for clarity and just dimension in an extra row and column for W().

ON-GOTO and ON-GOSUB

We also have a new BASIC statement! ON X GOTO L1,L2,L3...,Ln looks at the integer value of X and then consults the supplied table of N line numbers. If X is between 1 and N, it goes to the line number that is the Nth entry on the list. If it isn't, it proceeds to the next statement. If you replace the GOTO with a GOSUB then it calls the looked-up line number as a subroutine and with RETURN then proceeding to the next statement. I prefer ON-GOSUB myself because the out-of-range semantics are more consistent.

All the uses of ON-GOTO in this code look something like what we see in lines 310 and 320:

code:
310 X=INT(RND(1)*3+1)
320 ON X GOTO 790,820,860
X is never out of range, and we're essentially choosing one of three blocks of code to run here. There are four possible targets across all the code: 790, 820, 860, and 910. Those are our next targets. Anything that will improve our ability to summarize the code in the 260-780 block.

The Random Jump Targets

Blocks 790-815, 820-850, 860-905, and 910-1000 are all well-structured blocks, and are also the only way out of lines 210-780, except for one line that jumps directly to line 1000. Let's look at them in turn to see what alternatives are being randomly selected.

code:
790 W(R-1,S)=C
800 C=C+1:V(R-1,S)=2:R=R-1
810 IF C=H*V+1 THEN 1010
815 Q=0:GOTO 260
The cell to the west of the cursor is timestamped in 790. In 800 the timestamp is incremented, the cell to the west of the cursor is marked as having a passage to the east and no passage to the south, and then the cursor is decremented. (It's fine to set it so that there's a wall to the south; the cell to our west was never visited before this; that's why we got to timestamp it in 790.) Line 910 checks to see if the new timestamp is larger than the total number of cells in the maze, and if it is, it breaks out of the master loop and goes to print the maze out. If we still have work to do, though, the Q flag is reset to 0 and we jump back for the next iteration. We're skipping the scan at 210.

In summary, this block of code says "Dig West."

code:
820 W(R,S-1)=C
830 C=C+1
840 V(R,S-1)=1:S=S-1:IF C=H*V+1 THEN 1010
850 Q=0:GOTO 260
Exactly equivalent code, but now we dig north.

code:
860 W(R+1,S)=C
870 C=C+1:IF V(R,S)=0 THEN 880
875 V(R,S)=3:GOTO 890
880 V(R,S)=2
890 R=R+1
900 IF C=H*V+1 THEN 1010
905 GOTO 530
Digging east. We need extra logic here, though, because of the way we store exits. If we dig into our current cell from the south, we need to preserve that passageway as we dig east, so this code sets V(R,S) to 2 or 3 depending on whether the value is 0 or not.

The general logic of the code guarantees that if we're in this code at all, V(R,S) can only be 0 or 1; lines 870-880 could be replaced with 870 C=C+1:V(R,S)=V(R,S)+2 with no harm to the code. (What you really want is to bitwise OR it with 2, but that's out of the scope of our analysis here.)

This is the only branch that skips the 260-520 block in the main loop. We don't yet know why.

code:
910 IF Q=1 THEN 960
920 W(R,S+1)=C:C=C+1:IF V(R,S)=0 THEN 940
930 V(R,S)=3:GOTO 950
940 V(R,S)=1
950 S=S+1:IF C=H*V+1 THEN 1010
955 GOTO 260
If Q is not 1, we're digging south here, in a manner analogous to digging east. Note that we are doing the equivalent of adding 1 here, since the fact that we are here means that V(R,S) could only possibly be 0 or 2.

If Q is one...

code:
960 Z=1
970 IF V(R,S)=0 THEN 980
975 V(R,S)=3:Q=0:GOTO 1000
980 V(R,S)=1:Q=0:R=1:S=1:GOTO 250
1000 GOTO 210
960 is the only place in the main loop where Z can change value, so we know now that Z is an event flag tracking to see if we ever chose to dig south while Q=1. What it seems to do carve a southern exit out of the current cell, and then, if we had entered this cell from the north or the west, teleports the cursor to the upper left of the maze. It then jumps to 250 or 210, triggering the scan for the next maze position we've already visited. The jump to 250 suggests that if we teleported to the upper left and had already visited that cell, then this is an acceptable place to stop the scan.

It's not clear yet what the significance of these actions are, but this is also the only branch where C is not incremented and also the only branch where no new cells are dug out; it's simply opening a passage within the current cell and then scanning. We'll keep that in mind.

Back to the Main Code

We know what the random jumps do. Let's revisit the main loop again, this time up to the few random jumps:

code:
260 IF R-1=0 THEN 530
265 IF W(R-1,S)<>0 THEN 530
270 IF S-1=0 THEN 390
280 IF W(R,S-1)<>0 THEN 390
290 IF R=H THEN 330
300 IF W(R+1,S)<>0 THEN 330
310 X=INT(RND(1)*3+1)
320 ON X GOTO 790,820,860
330 IF S<>V THEN 340
334 IF Z=1 THEN 370
338 Q=1:GOTO 350
340 IF W(R,S+1)<>0 THEN 370
350 X=INT(RND(1)*3+1)
360 ON X GOTO 790,820,910
370 X=INT(RND(1)*2+1)
380 ON X GOTO 790,820
Lines 260 through 300 are only slight variations of each other; they are bounds-checks on R and S followed by checking if a neighbor of cell (R,S) has been visited. The basic check here is is it OK to go this direction? This lets us translate through to line 330 very easily, and then slot the 330-370 block into the larger structure:

pre:
If it's OK to go west
    If it's OK to go north
        If it's OK to go east
            Dig in a random direction chosen between (North, East, West)
        otherwise
            330 If we aren't at the bottom then GOTO 340
            334 If Z=1 then GOTO 370
            338 Q=1:GOTO 350
            340 If the cell to our south is visited then goto 370
            350 Dig in a random direction chosen between (North, West, South)
            370 Dig in a random direction chosen between (North, West)
Lines 330-370 are A Problem. We can't directly translate this into a set of IF/THEN/ELSE statements; we'll have to look at the conditions under which each line runs and then work out what the blocks should be from that.
  • Line 330 is where we start so it always runs.
  • Line 334 runs if the test in line 330 failed, and thus runs only if we are at the bottom of the maze.
  • Line 338 requires both previous tests to fail, which means we are both at the bottom of the maze, and Z is not 1. (Since the only value Z ever takes that isn't 1 is 0, that means Z must be 0 here.)
  • Line 340 runs if we aren't at the bottom of the maze.
  • Line 350 runs if line 338 ran (so, if we are at the bottom and Z=0), OR if line 340 ran and its test failed (so, the cell to our south exists and has not been visited).
  • Line 370 runs if the tests at 334 or 340 succeed, so, if we are at the bottom and Z=1, or if we aren't at the bottom and the cell to our south has already been visited.

This does teach us a few things. Remember how Z was set to 1 only when we dug south while Q was 1? Q is only set to 1 if we're on the bottom row of the maze and Z is still 0! We've finally figured out what Q and Z mean: Q is 1 if we may be trying to dig the exit from the maze, and Z is 1 if we have already done this.

This also means that Q is, strictly speaking, redundant. We can tell if we're trying to dig the exit just by looking at S and V! If we have selected the option to dig south, and we're on the bottom row, we are in fact digging the exit, thank you very much; we need no Q to keep track of this. More importantly, it means we can abstract away the entire dance involving Q and Z as being part of "is it OK to go south"—it's just that instead of doing a simple bounds-check and visitation-check like the other three directions, we have an extra rule that says that if we have not yet dug the exit, the bounds-check may be skipped. Now we can translate the whole 260-1000 block:

pre:
If it's OK to go west
    If it's OK to go north
        If it's OK to go east
            Dig in a random direction chosen between (North, East, West)
        otherwise
            If it's OK to go south
                Dig in a random direction chosen between (North, West, South)
            otherwise
                Dig in a random direction chosen between (North, West)
    otherwise
        If it's OK to go east
            If it's OK to go south
                Dig in a random direction chosen between (East, West, South)
            otherwise
                Dig in a random direction chosen between (East, West)
        otherwise
            If it's OK to go south
                Dig in a random direction chosen between (West, South)
            otherwise
                Dig west
otherwise
    If it's OK to go north
        If it's OK to go east
            If it's OK to go south
                Dig in a random direction chosen between (North, East, South)
            otherwise
                Dig in a random direction chosen between (North, East)
        otherwise
            If it's OK to go south
                Dig in a random direction chosen between (North, South)
            otherwise
                Dig north
    otherwise
        If it's OK to go east
            If it's OK to go south
                Dig in a random direction chosen between (East, South)
            otherwise
                Dig east
        otherwise
            If it's OK to go south
                Dig south
            otherwise
                We're stuck; scan for the next occupied space, proceed to next iteration
Well. We can do so with one terrifying exception. Line 695 appears to read 695 Q=1:GOTO 830 and there is no possible way that is correct (830 is the middle of a dig operation). By analogy with the other five places in the code that Q is set, the target should clearly be line 710, not 830.

Now that we've laid out the entire set of logic, we can also wee what it does: it is selecting a random valid direction to dig, or, if it is completely boxed in, moving the cursor to the next cell that's still part of the maze. It's just that it's spending nearly seventy lines of code to do so, by exhaustively enumerating every possible case. (The case where all directions are valid is missing, but that is an impossible case; you start at an edge, and in every other case you had to arrive at the current cell from some direction that is now illegal.)

This is... not the best way to perform this operation. Even in BASIC. Even without GOSUB. The alternative I devised was eight lines long, none of which were over 80 characters in length. Nevertheless, now we know what it's doing and how it does it.

On the bright side, we can also now see that there's no problem with that GOTO 530 in the loopback code; that branch was only taken when digging east, and essentially just skipped the entire "if it is OK to go west" block. It had come from the west, so it was skipping a test that it knew was guaranteed to fail. We don't really need a DoFirstBit flag after all. "Needs a scan" also may be more sensibly rephrased as "we are stuck".

The Big Picture

We've been talking about moving cursors around and setting and checking various cells in a grid. That's not what we're here for. We're here to generate a maze. What's the top-level view of the program?

pre:
Get valid maze dimensions from user
Select a random column (X) to start the maze
Print northern edge of maze with entrance marked
Mark (X,1) as visited
Mark (X,1) as current location.
Repeat until the maze and its exit have been fully dug out:
    Select a random valid direction if possible
    If it was not possible:
        Mark ourselves as stuck
    If it was possible, and we're not digging the exit:
        Dig in that direction, and set the current location to that new value
    If it was possible, and we *are* digging the exit:
        Open the exit
        If this cell has an exit to the east, mark ourselves as stuck.
        If this cell has no exit to the east:
            Change current location to (1,1)
            Mark ourselves as stuck if the (1,1) is still not dug out
    If we have marked ourselves as stuck:
        Scan through the grid left to right and top to bottom for the next visited cell
        Mark ourselves as not stuck
Print resulting maze
At this level, the program isn't bad. This is a reasonably efficient implementation of computing a spanning tree across a rectangular graph, with some tweaks that lend themselves to ensuring there are a small number of long twisting corridors instead of a vast number of bushy, instantaneous dead-ends. Those same tweaks also mean that it never needs GOSUB, so it's not 100% clear to me that they did that on purpose.

After sorting out how it worked, I created an equivalent but vastly shorter version of it, and then made some additional modest improvements. This post has gone on far too long as it is, though, so I will save that for another post.

ManxomeBromide fucked around with this message at 01:36 on Jun 24, 2020

ManxomeBromide
Jan 29, 2009

old school
EVEN MORE AMAZING

Here is my improved version of Amazing, rewritten with sensible data structures and algorithms and noticeably improved graphics. It is compatible with QBASIC and GW-BASIC, and it is less than fifty lines of code, none of which are more than eighty characters long:
code:
10 PRINT TAB(28);"AMAZING PROGRAM"
20 PRINT TAB(15);"CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY"
30 PRINT:PRINT:PRINT:PRINT
40 INPUT "WHAT ARE YOUR WIDTH AND LENGTH";XM,YM
50 IF XM<2 OR YM<2 THEN PRINT "MEANINGLESS DIMENSIONS. TRY AGAIN.":GOTO 40
60 DIM W(XM+1,YM+1),V(XM,YM),VD(4)
70 RANDOMIZE TIMER
80 R=INT(RND(1)*XM+1):S=1:W(R,S)=1:C=2:Z=0
90 N=0:X=0
100 IF R<>1 AND W(R-1,S)=0 THEN N=N+1:VD(N)=1
110 IF S<>1 AND W(R,S-1)=0 THEN N=N+1:VD(N)=2
120 IF R<>XM AND W(R+1,S)=0 THEN N=N+1:VD(N)=3
130 IF (S=YM AND Z=0) OR (S<>YM AND W(R,S+1)=0) THEN N=N+1:VD(N)=4
140 IF N>0 THEN N=VD(INT(RND(1)*N)+1)
150 ON N+1 GOSUB 390, 400, 410, 420, 430
160 IF X=0 THEN C=C+1:GOTO 210
170 IF R<>XM THEN R=R+1:GOTO 200
180 IF S<>YM THEN R=1:S=S+1:GOTO 200
190 R=1:S=1
200 IF W(R,S)=0 THEN 170
210 IF Z=0 OR C<XM*YM+1 THEN 90
220 PRINT CHR$(218);:IF W(1,1)=1 THEN PRINT "  ";:GOTO 240
230 PRINT CHR$(196);CHR$(196);
240 FOR X=2 TO XM:PRINT CHR$(194);:IF W(X,1)=1 THEN PRINT "  ";:GOTO 260
250 PRINT CHR$(196);CHR$(196);
260 NEXT X:PRINT CHR$(191)
270 FOR Y=1 TO YM:PRINT CHR$(179);:FOR X=1 TO XM
280 IF V(X,Y)<2 THEN PRINT "  ";CHR$(179);:GOTO 300
290 PRINT "   ";
300 NEXT X:PRINT:L$=CHR$(195):M$=CHR$(197):R$=CHR$(180)
310 IF Y=YM THEN L$=CHR$(192):M$=CHR$(193):R$=CHR$(217)
320 PRINT L$;:IF V(1,Y)=0 OR V(1,Y)=2 THEN PRINT CHR$(196);CHR$(196);:GOTO 340
330 PRINT "  ";
340 FOR X=2 TO XM:PRINT M$;
350 IF V(X,Y)=0 OR V(X,Y)=2 THEN PRINT CHR$(196);CHR$(196);:GOTO 370
360 PRINT "  ";
370 NEXT X:PRINT R$:NEXT Y
380 END
390 X=1:RETURN
400 W(R-1,S)=C:V(R-1,S)=2:R=R-1:RETURN
410 W(R,S-1)=C:V(R,S-1)=1:S=S-1:RETURN
420 W(R+1,S)=C:V(R,S)=V(R,S)+2:R=R+1:RETURN
430 IF S=YM THEN 450
440 W(R,S+1)=C:V(R,S)=V(R,S)+1:S=S+1:RETURN
450 Z=1:IF V(R,S)<>0 THEN V(R,S)=3:RETURN
460 V(R,S)=1:R=1:S=1:C=C-1
470 IF W(R,S)=0 THEN X=1:C=C+1
480 RETURN
The improvements and modifications are sixfold:
  • It now correctly rejects input that specifies zero or negative dimensions.
  • The 70-ish lines of code for choosing a direction (lines 260-780) are now more like seven. I create an expandable list, fill it with between 0 and 4 elements, and choose a result from the valid elements. BASIC doesn't have expandable lists as a data type, of course, but I'm not about to let that stop me.
  • The digging logic has also been simplified, merging "getting stuck" into the actual logic of executing a chosen direction.
  • The random number generator is re-seeded on each run to make sure it won't keep producing the same maze each time.
  • I removed the blank lines from after the part where you choose the dimensions, mainly so that a 10-row maze can keep the dimensions visible on a 25-line screen.
  • The printing of the maze is slightly more sophisticated, making use of the box-drawing characters in the IBM BIOS codepage that we now know as Code Page 437. The box-drawing characters aren't quite up to the level I'd want them to be at to get a properly proportioned set of maze passages, and even if they were the drawing code would be quite a bit harder to manage, but even if I keep the logic mostly intact I can still get a perfectly acceptable, if spiky, outcome.


A Challenge

There's still a bug lurking in this code somewhere, and it's visible in the screenshot. Can you find it?

Commodore Conversion

The Commodore 8-bits do not have the same graphics characters as the PC, but they do share some of them even when they are in different places. This program is pretty trivially portable to the 40-column Commodore computers (PET 4016, C64, C128, C16, C/+4) with only a few changes:

code:
10 PRINT TAB(12);"AMAZING PROGRAM"
20 PRINT TAB(11);"CREATIVE COMPUTING":PRINT TAB(9);"MORRISTOWN, NEW JERSEY"
70 R=RND(-TI)
220 PRINT CHR$(176);:IF W(1,1)=1 THEN PRINT "  ";:GOTO 240
230 PRINT CHR$(195);CHR$(195);
240 FOR X=2 TO XM:PRINT CHR$(178);:IF W(X,1)=1 THEN PRINT "  ";:GOTO 260
250 PRINT CHR$(195);CHR$(195);
260 NEXT X:PRINT CHR$(174)
270 FOR Y=1 TO YM:PRINT CHR$(221);:FOR X=1 TO XM
280 IF V(X,Y)<2 THEN PRINT "  ";CHR$(221);:GOTO 300
290 PRINT "   ";
300 NEXT X:PRINT:L$=CHR$(171):M$=CHR$(219):R$=CHR$(179)
310 IF Y=YM THEN L$=CHR$(173):M$=CHR$(177):R$=CHR$(189)
320 PRINT L$;:IF V(1,Y)=0 OR V(1,Y)=2 THEN PRINT CHR$(195);CHR$(195);:GOTO 340
330 PRINT "  ";
340 FOR X=2 TO XM:PRINT M$;
350 IF V(X,Y)=0 OR V(X,Y)=2 THEN PRINT CHR$(195);CHR$(195);:GOTO 370
Lines 10 and 20 change to match 40-column formatting, and line 70 changes because Commodore BASICs manage reseeding the random number generator differently from PC BASICs. Lines 220-270 don't all change, but it's easier to just reproduce the entire print routine than it is to pick out which lines happened to not change.

The VIC-20 has only 20-odd columns so you won't get a maze wider than 6 cells, but this code will work even on an unexpanded VIC-20 (3.5KB of RAM!)... as long as you shorten the prompt in line 40:

code:
40 INPUT "MAZE W/H",XM,YM
Long INPUT prompts seem to confuse the VIC-20 BASIC for some reason.

A Final Thought

You know how people often talk about the Grand Old Days when people had to actually be good at programming to get decent results out of their computers?

Don't. Believe. The. Hype.

Check out how much slack there was to squeeze out of this, and it worked fine on 1970s personal hardware.

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

ManxomeBromide posted:

There's still a bug lurking in this code somewhere, and it's visible in the screenshot. Can you find it?

I see that there's a square that never gets entered, but I'm not sure what causes that. I'm also not too fussed about finding it; given how thoroughly you've dissected and reconstructed this program, it's no surprise that your watch is missing. Thank you very much!

Carbon dioxide posted:

This BASIC code is hard to read. I'll just wait for you folks to come up with the bit-by-bit explanation if you don't mind.

I can't do anything about incomprehensibly, but I can improve the legibility, I think; I found a PDF scan of the book on Archive.org, and I'll pull pictures from it. Starting with this one!



The actual listing is on the next page, but I'll save you the eyestrain and reproduce it in text. It's shorter and much more sensibly coded - except for one major sin.

code:
10 PRINT TAB(32);"ANIMAL"
20 PRINT TAB(15);"CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY"
30 PRINT: PRINT: PRINT
40 PRINT "PLAY 'GUESS THE ANIMAL'"
50 PRINT "THINK OF AN ANIMAL AND THE COMPUTER WILL TRY TO GUESS IT."
60 PRINT
70 DIM A$(200)
80 FOR I=0 TO 3
90 READ A$(I)
100 NEXT I
Lines 10-70 are nothing new, but what's going on in lines 80-100? Well, for that I have to talk about...

DATA, READ, and RESTORE
The DATA command can be used anywhere in a BASIC program. You put one or more items after it, separated by commas; these items can be numbers or literal strings. (Some BASICs don't require quote marks around strings, or at least single-word strings, but better safe than sorry.) At least some flavors of BASIC allow other commands to be on the line after the data, but I don't think that looks very clean.

The DATA command itself doesn't do anything; it just holds information for the READ command. When first called, the READ command takes the first data item in the program (by line number, naturally) and assigns it to a variable. Each time after that, it'll take the next data item. Assigning numbers to string variables is legal - they get converted to strings - but attempting to assign a string to a numeric variable will crash the program with a TYPE MISMATCH error. Attempting to read a data item when you've gone through them all will also crash the program, this time with an OUT OF DATA error.

There's one more command that interacts with DATA. The RESTORE command changes what the next data item READ will take is. Almost all varieties of BASIC support a plain RESTORE, which winds the 'next data' pointer back to the beginning; some also support RESTORE (n), which moves the 'next data' pointer to the beginning of the given line number. (If that line number doesn't actually have any data on it, the program will keep looking from there until it finds some.)

So this bit of code reads four strings into A$(0) through A$(3). What are those strings? Well, let's jump ahead.

code:
530 DATA "4","\QDOES IT SWIM\Y2\N3\","\AFISH","\ABIRD"
I can just about figure out how A$() works just from this, but let's see how the program handles it.

code:
110 N=VAL(A$(0))
The VAL() function takes a string and returns its numerical representation. If it doesn't have a numerical representation, it returns 0. Similarly, the STR$() function, which we'll see later, takes a number and returns its string expression. How this handles very large or very small numbers, and whether it uses a leading and/or trailing space, is, you guessed it, up to the variety of BASIC.

code:
120 REM MAIN CONTROL SECTION
The REM command tells the BASIC interpreter to ignore the entire rest of the line, even if there are any colons. This is how you put comments in your code.

code:
130 INPUT "ARE YOU THINKING OF AN ANIMAL";A$
140 IF A$="LIST" THEN 600
150 IF LEFT$(A$,1)<>"Y" THEN 120
We'll take a look at the code on line 600 when we get there, as reading the rest of the code will help us understand it. Note the little joke on the programmer's part in line 150. Note also the simultaneous use of A$ and A$().

code:
160 K=1
170 GOSUB 390
Going to 390 introduces us to the wonderful world of:

Manipulating strings

code:
390 REM     SUBROUTINE TO PRINT QUESTIONS
400 Q$=A$(K)
410 FOR Z=3 TO LEN(Q$)
415 IF MID$(Q$,Z,1)<>"\" THEN PRINT MID$(Q$,X,1);:NEXT Z
There are a number of functions that allow you to manipulate strings beyond just assigning them. Here we see three of them; we also skipped past one earlier without comment. LEN() returns the length of a string. (The length of an empty string is 0.) MID$(A$,B,C) starts at the Bth character of A$ and returns a string of the next C character(s), including the Bth character. (If A$ doesn't have a Bth character, or if C is 0, it returns the empty string. If the string is too short for the function to return C characters, it returns what it can.) The function we skipped past, LEFT$(), gives you the leftmost however characters of a string, while RIGHT$() does the same for the rightmost however characters.

Finally, there's the <> operator. Comparing strings works by comparing the numbers used internally to represent the characters; the most typical set of these is ASCII, which defines printable characters in the 32-126 range and reserves 0-31 and 127 for other tasks. < means 'earlier in an alphabetical sort' while > means 'later in an alphabetical sort', but the definition of 'alphabetical' differs from your grade school library class; generally speaking, numbers come before letters and capital letters come before lower-case letters, but symbols are sprinkled throughout. "" < " " < "*" < "0" < "=" < "A" < "[" < "a" < "{"... in standard ASCII, at least.

This is one of the places where BASIC varieties change the most. QuickBASIC and its descendants add the IBM's Code Page 437 characters, allowing you to use symbols like and Θ. The ZX Spectrum's character set includes symbols like Ł and . And the Commodore 64 has two character sets, one with uppercase letters and many graphics symbols (for instance, and ) and one with fewer graphics symbols but with lowercase letters instead - and to make matters more confusing, it's the graphic symbols in the first mode that become uppercase letters in the second, with the uppercase letters becoming lowercase. In what's called PETSCII, "a" < "A"! (Or, if you prefer, "A" < "♠".) Which caused no end of headaches trying to connect to non-Commodore computers, lemme tell ya.

...and speaking of headaches, why you replace that Unicode with an image, forum software?

Anyway. Went on a bit of a tangent there. More string functions as they come up. In this specific case, since A$(K) is "\QDOES IT SWIM\Y2\N3\", the code starts at the third character, "D", and continues until - well, you'd think by looking at that code that it'd continue to the end, printing everything but the backslashes, right?

Wrong. The NEXT command, being after the IF - THEN command, is only called if the IF part is true. The first time this code hits a backslash, it drops through to the next line - exiting the FOR - NEXT loop without finishing it.

This is a VERY BAD THING. The computer keeps track of what loops it's got going in much the same way it keeps track of where it has to return when it's done with a subroutine. The memory it has to do so is limited. Eventually, it'll run out, and your program will crash with an OUT OF MEMORY error - most likely nowhere near the actual problem. And because memory usage can vary from run to run, you may not even be able to reliably reproduce the error!

Fortunately, there's a fix for this. As I mentioned, variables have global scope. This includes loop variables. If we change line 415 to IF MID$(Q$,Z,1)="\" THEN Z=LEN(Q$)+1 and add a line, say, 417 that's PRINT MID$(Q$,X,1);:NEXT Z, then when Z reaches 15, the location of the backslash after SWIM, it'll be set to 22, the program will try to print the 22nd character of Q$ (but since there isn't one, won't print anything), and the loop will immediately end.

Now where were we?

code:
420 INPUT C$
430 C$=LEFT$(C$,1)
440 IF C$<>"Y" AND C$<>"N" THEN 410
Getting the yes or no answer to the question. Note that, if the player doesn't type Y or N, it reprints the question.

code:
450 T$="\"+C$
455 FOR X=3 TO LEN(Q$)-1
460 IF MID$(Q$,X,2)=T$ THEN 480
470 NEXT X
475 STOP
The + operator concatenates strings. It is sometimes legal to concatenate a string with a number (in which case the number gets converted to a string first); it is never legal to add a string to a number. Let's suppose the player types Y. T$ is set to "\Y", and then we loop through Q$ until we find the location of "\Y" in Q$ - at which point we jump out of the loop.

(The STOP command, by the way, immediately ends the program, but unlike END prints a BREAK IN (line number) error. If we somehow manage to get through the question without finding "\Y", something's gone wrong.)

This isn't as easy to fix as the previous example of jumping out of a loop, because...

code:
480 FOR Y=X+1 TO LEN(Q$)
490 IF MID$(Q$,Y,1)="\" THEN 510
500 NEXT Y
505 STOP
...we immediately use the value of X to find the next backslash in Q$. And then jump out a loop again, this time to use the value of Y. As an exercise for the reader: how would you adjust this code to find X and Y without jumping out of any loops? (Hint: you'll probably want to also use the variables ZX and ZY for something.)

code:
510 K=VAL(MID$(Q$,X+2,Y-X-2))
520 RETURN
Line 510 is fairly dense, so let's pick it apart. Q$ is "\QDOES IT SWIM\Y2\N3\". X is the location of the backslash immediately before "Y" or "N", depending on what answer the player gave; Y is the location of the next backslash. So the MID$() function starts at character X+2 of Q$ - if the player picked "Y", that's the character "2" - and goes on for Y-X-2 characters. Since, in this case, Y = X + 3, this resolves to 1, so MID$() returns "2". The VAL() function then turns this into the number 2; this is stored in K before the subroutine returns.

This is all a very long way of saying that A$() stores several pieces of information in a single string. A$(1), our starting point, starts with "\Q", which I'll go ahead and spoil is the marker put at the beginning of a question string. After the question, there's a "\Y" marker to tell us where to go if the answer is yes - in this case, A$(2) - followed by an "\N" marker directing us to A$(3), and finally a trailing backslash. The answer strings in those two locations start with "\A", while A$(0) is a special case that holds the number of the next unused element of A$().

So now we've asked a question and gotten a response. What next?

code:
180 IF LEN(A$(K))=0 THEN 999

...

999 END
This is another 'this should never happen' protection, but it does it oddly in two ways. First, why jump to another line just to end execution? Second, shouldn't it use STOP?

code:
190 IF LEFT$(A$(K),2)="\Q" THEN 170
If our new position in A$() is another question - which can't be true now, but will be possible eventually - we jump back to 170 and ask another question. Eventually, though, we'll hit an answer string. (Or run out of memory from jumping out of all those loops.)

code:
200 PRINT "IS IT A "RIGHT$(A$(K),LEN(A$(K))-2);
210 INPUT A$
220 A$=LEFT$(A$,1)
230 IF A$="Y" THEN PRINT "WHY NOT TRY ANOTHER ANIMAL?": GOTO 120
Line 200 is one way to print A$(K) without the "\A" at the start. Can you think of another? Also, something I discovered/rediscovered while I was working on the Acey Deucey program: although INPUT will take a literal string before the variable that will receive the input, it will not take any old things to print like PRINT will. That's why we can't put the stuff in line 200 into the INPUT command in 210, though we could append the command with a colon.

code:
240 INPUT "THE ANIMAL YOU WERE THINKING OF WAS A ";V$
250 PRINT "PLEASE TYPE A QUESTION THAT WOULD DISTINGUISH A"
260 PRINT V$;" FROM A ";RIGHT$(A$(K),LEN(A$(K))-2)
270 INPUT X$
280 PRINT "FOR A ";V$;" THE ANSWER WOULD BE ";
290 INPUT A$
300 A$=LEFT$(A$,1): IF A$<>"Y" AND A$<>"N" THEN 280
We now have the information we need to update A$(). But how do we put it together?

code:
310 IF A$="Y" THEN B$="N"
320 IF A$="N" THEN B$="Y"
330 Z1=VAL(A$(0))
340 A$(0)=STR$(Z1+2)
350 A$(Z1)=A$(K)
360 A$(Z1+1)="\A"+V$
370 A$(K)="\Q"+X$+"\"+A$+STR$(Z1+1)+"\"+B$+STR$(Z1)+"\"
380 GOTO 120
This is another dense bit of code, so let's pick it apart. B$ gets set to the opposite answer from the one the player gave. Z1 is then set to the number in A$(0). (Note that this means the value of N set in line 110 is never used.) We add two to this and store it back in A$(0). (Personally, I'd just keep the value in a separate variable all the time, but if you had some sort of 'save this entire array' function, this would simplify that.)

We then copy A$(K) to A$(Z1). The first time we add a new animal, Z1 is 4, so (again, assuming we answered "Y" to the question "DOES IT SWIM" we copy "\AFISH" from A$(2) to A$(4) (which was previously empty. As for the animal we just entered - let's say "DOLPHIN" - we put "\ADOLPHIN" into A$(5). (Which is why we increased the number in A$(0) by 2.)

But what of the now-redundant A$(2)? Well, that's where we're going to put our new question so the first question goes there after a 'yes' response. That's what the whole mess on line 370 does. Since X$ is, let's say, "IS IT A MAMMAL", and A$ is "Y" (because dolphins are mammals, but fish are not), B$ is "N". Which means the final contents of A$(2) become "\QIS IT A MAMMAL\Y5\N4\". Or possibly "\QIS IT A MAMMAL\Y 5 \N 4 \", depending on the BASIC variant. But whatever STR$() puts out, VAL() will read. No biggie. (Note that if the correct answer to the disambiguating question is no, A$(2) would become, for instance, "\QDOES IT HAVE GILLS\N5\Y4\".)

Then we hop back to line 120. You'll note there's no way to stop playing; the person who created the sample output had to push a special key to cause the program to stop the same way the STOP command does. What that key is labeled, and indeed if it is a specific key and not a key combo like Ctrl-C, say it with me, depends on the BASIC interpreter.

Oh, but we're not quite done. After the subroutine we used earlier...

code:
600 PRINT: PRINT "ANIMALS I ALREADY KNOW ARE:"
605 X=0
610 FOR I=1 TO 200
620 IF LEFT$(A$(I),2)<>"\A" THEN 650
624 PRINT TAB(12*X);
630 FOR Z=3 TO LEN(A$(I))
640 IF MID$(A$(I),Z,1)<>"\" THEN PRINT MID$(A$(I),Z,1);: NEXT Z
645 X=X+1: IF X>5 THEN X=0: PRINT
650 NEXT I
660 PRINT
670 PRINT
680 GOTO 120
999 END
...is the code that prints the animals in the database if the player types "LIST" when asked if they're thinking of an animal. I won't go into it in a whole lot of detail, except to mention that it makes two assumptions, one about what it's outputting and one about where it's outputting it; can you find them?

So. This game, unlike the previous program, is fairly neatly coded, aside from the aforementioned grave sin. There's a variable that's defined but never used, but we can get rid of that easily enough. (In case you're playing along on a Commodore 64 or something, typing a line number without anything after it will delete that line.) As it stands, though, it has two major limitations. For one thing, it has no way of saving the database from run to run; file I/O is so interpreter-dependent that I'm not going to even bother to suggest a fix. The other is that it only has room for 200 items (questions and answers) in its database, and when it runs out, it'll crash when it tries to store a string in A$(201) or above. (Can you see a good way to fix that?)

Unfortunately, BASIC doesn't allow us to resize variable arrays once they're defined. (Technically, there is a way, but it involves erasing the values of all variables; the command, if you're curious, is CLR.) So there's only so smart this program can get in the short term, and it has no long-term memory. That said, you could take the output of a play session and put extra information into the DATA statements, because that's what you do with BASIC: rewrite other people's code, because the code is right there.

So. As more than one of my college professors said, questions? Comments? Difficulties? For that matter, any BASIC anecdotes you want to share?

FredMSloniker fucked around with this message at 05:08 on Jun 24, 2020

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
So there's some Bad Stuff going on right now (in other news, welcome to 2020; you may have missed it), and the future of the forums is in question. I, for one, am in no hurry to hurl myself into the sea, but if the boat explodes or something, I've got my posts backed up, and I'll find a retro forum to resurrect the thread on. (If you have any suggestions in that regard, put them over here.) That said, anybody have anything they want to say or hear, or should I move on to the next program?

Kangra
May 7, 2012

The original Amazing looks very much like assembly. Either it was ported or the programmer just had that mental construct of it. If not, they must have been a big fan of flowcharts, which would probably be the only other way to arrive at that structure.

It's good to see this sort of analysis of stuff I never really went back to. I hope that it can continue in some way or another.

ManxomeBromide
Jan 29, 2009

old school
There's a strong connection between these old dialects of BASIC and 80s-era assembly language, to be honest. All variables are global because your chip isn't very good at stack variables, with GOTO and GOSUB serving almost exactly the same purpose as JUMP and CALL statements, and with every branch being some kind of IF ____ THEN GOTO statement.

Even if it were designed for assembly language, generalizing "pick a random valid direction" would have been a vast improvement even there.

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
I forgot to mention that I really like the way you implemented 'pick a random direction'. It's not dissimilar to how I would implement it in Lua (stuff valid elements into a sequence, then pick a random sequence member), and it also bears similarities to the deck-handling code I posted earlier. It's also given me an idea for improved deck-handling in general, which'll come in handy when I get to Blackjack. (Er, spoiler alert: there's a Blackjack program in this book.)

For now, though, I'm going to cover the 'exercises' I mentioned regarding Animal. Here's a simple fix for the issue of jumping out of the X and Y loops:

code:
455 FOR ZX=3 TO LEN(Q$)-1
460 IF MID$(Q$,X,2)=T$ THEN X=ZX:ZX=LEN(Q$)
470 NEXT ZX
480 FOR ZY=X+1 TO LEN(Q$)
490 IF MID$(Q$,Y,1)="\" THEN Y=ZY:ZY=LEN(Q$)
500 NEXT ZY
This takes out the 'just-in-case' STOP commands; if you wanted to add a paranoia check, you'd have to do it a bit differently. (I'd set X and Y to 0 before starting the loops and change the relevant lines to IF X=0 THEN STOP or what have you.)

(I may have said earlier, but back in my C64 days, I got used to using variables starting with Z as temporary values. ZZ, of course, was the most ephemeral of those, sometimes destined to be thrown away immediately in a timing loop.)

Another way to get all but the first two characters of A$(K) (as seen in line 200) would be MID$(A$(K),3,LEN(A$(K))-2). However, since MID$() gracefully handles running out of characters, you could easily make that MID$(A$(K),3,LEN(A$(K)) - or, indeed, MID$(A$(K),3,999), presuming your version of BASIC can't have strings that long. This is why, incidentally, you would bother with using MID$() in this situation - so you don't have to also use LEN(). You could also amend my modified lines 460 and 490 with that in mind.

On an unrelated note: it'd probably be a good idea to set the various variables used for input to "" before using INPUT. I've confirmed that, at least in Commodore BASIC V2, if you don't enter anything, it doesn't change the original value of the variable, and one carriage return too many could confuse the program. Also, there's no sanity check for animal names, so you could stick a backslash in the name or not type anything and really confuse matters.

As for the assumptions made in lines 600 on, I said there were two. One is that the screen/paper is at least 60 columns wide. The other is that no animal will have a name longer than 11 characters. Both assumptions appear in line 624, which tries to format the animal names into pretty columns but only works if those constraints are met.

Finally, to keep from overflowing the A$() array, I'd suggest checking the value of A$(0) before adding new information - ideally, after confirming that the player has a new animal in mind but before bothering to ask for information about it - and throwing some sort of complaint before returning to line 120. And if I were somehow in the Teletype age, I might consider adding a 'dump database' subroutine that would format and print BASIC DATA commands that I could then type back in to update the code and give it more brainpower! (If the teleprinter had a paper tape attachment, I could even write the lines to tape, then read them back in to make the changes automatically. That'd be one way to save and load data!)

Anyway. I'll have more on the next program in the book tomorrow sometime; right now I'm nursing a broken foot. (It's been broken for a couple months now, and I'm back in regular shoes, but I'm still being careful about long periods of verticality, and that can include sitting.) Next time, though, we see a game that's actually halfway worth playing!

FredMSloniker fucked around with this message at 21:34 on Jun 25, 2020

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.


Okay, let me make this clear right from the start: this is not Awari (or Oware, as it's more properly known). Nor is it Mancala, any more than you're Primate. Mancala is a name for a large body of games with similar mechanics, some of which are simple enough to have been solved by computers, some of which are rather complex. This game is most similar to a variant called Kalah, but has several differences in play. Familiarize yourself with the rules above before we proceed.

The listing for this game is surprisingly short, considering it actually implements a learning AI. It's well-organized, too, which is a treat. That said, it commits not one but two major sins, and it has a bug or two besides. Let's get started breaking it down.

(By the way, I'm making some assumptions about how much you know about programming that may not be correct. I don't mean to talk down to you. If I'm talking up to you, let me know what needs clarification.)

code:
5 PRINT TAB(34);"AWARI"
7 PRINT TAB(15);"CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY"
10 DATA 0
15 DIM B(13),G(13),F(50):READ N
The B() array stores the state of the board. B(0) is the number of beans in the player's left-most pit. The numbers ascend counter-clockwise, so B(6) is the number of beans in the player's home pit, and B(13) is the number of beans in the computer's home pit. The G() array is used to back up that state as part of the computer's move-making code.

The F() array and N are used in the computer's learning AI. I'll go into more detail as we explore that code. I don't know why N is set with a DATA - READ combo, though; neither command is used elsewhere in the program, so you could just say N=0. This will be the mildest of the program's technically legal, but kind of strange, decisions.

code:
20 PRINT:PRINT:E=0
25 FORI=0 TO 12:B(I)=3:NEXT I
30 C=0:F(N)=0:B(13)=0:B(6)=0
35 GOSUB 500
40 PRINT "YOUR MOVE";:GOSUB 110
45 IF E=0 THEN 80
50 IF M=H THEN GOSUB 100
55 IF E=0 THEN 80
60 PRINT "MY MOVE IS ";:GOSUB 800
65 IF E=0 THEN 80
70 IF M=H THEN PRINT ",";:GOSUB 800
75 IF E>0 THEN 35
80 PRINT:PRINT"GAME OVER"
85 D=B(6)-B(13):IF D<0 THEN PRINT "I WIN BY";-D;"POINTS":GOTO 20
90 N=N+1:IF D=0 THEN PRINT "DRAWN GAME":GOTO 20
95 PRINT "YOU WIN BY";D;"POINTS":GOTO 20
This is the main loop of the program. As you can see, it makes extensive use of subroutines, making the program as modular as BASIC allows. Let's go through it.

Line 20 sets E to 0. E is a flag used to determine if the game is not over (that is, if both players still have beans; I'll explain why I phrased it that way when we get there.) Lines 25 and 30 initialize the board and the learning AI's information storage for this game. (Again, more detail when we get there.)

Where line 20 starts the loop of the entire play session, line 35 starts the loop of a single game. The subroutine at line 500 prints the current state of the board.

Line 40 starts the section of the code that processes the player's turn. The subroutine at line 110 takes the player's move, updates the board and the learning AI's information storage, prints the updated board, and checks whether the game is over. If it is, it sets E to 1, and line 45 jumps to the 'game over' code. Line 50 determines whether the player gets a second move (I'll explain both M and H when we get there) and, if so, calls the subroutine at line 100 - which is just the subroutine at line 110, only instead of printing "YOUR MOVE", it prints "AGAIN".

After another check if the game is over (if needed), it's the computer's turn. The subroutine at line 800 makes a move for the computer; more detail when we get there. It's worth noting, though, that this subroutine does not print the updated board, which is why lines 60 and 70 print its moves (if it gets two) on the same line. If neither move ends the game, we then head back to 35, which prints the result of the computer's move(s) and starts things off again.

Starting at line 80, we handle a game over. As I said earlier, B(6) is the number of beans in the player's home pot, while B(13) is the number of beans in the computer's home pot. The difference, D, is positive if the player won, 0 if the player tied, and negative if the player lost. The appropriate message is displayed, and then the game loops back to line 20, starting a new game. Note that N is increased if the player wins or draws, but not if the player loses. That's part of the learning AI.

And that's it for the main loop! Time to look at some subroutines, starting with the one at line 500.

code:
500 PRINT:PRINT"   ";:REM 3 SPACES
505 FOR I=12 TO 7 STEP -1:GOSUB 580
510 NEXT I
515 PRINT:I=13:GOSUB 580
520 PRINT"                       ";:PRINT B(6):PRINT "   ";:REM 23 SPACES, THEN 3 SPACES
525 FOR I=0 TO 5:GOSUB 580
530 NEXT I
535 PRINT:PRINT:RETURN
580 IF B(I)<10 THEN PRINT " ";
585 PRINT B(I);:RETURN
I added the REM statements here because the scan doesn't have any real way to tell how much space is there other than comparing the text to the adjacent lines and counting the characters. You can leave them out.

This subroutine, as previously mentioned, is what we use to print the board. STEP is a new part of the FOR - NEXT combo. By default, each time we go through a FOR - NEXT loop, the loop variable goes up by 1. STEP lets us change that. Lines 500-510, therefore, print the first line of the board, showing the computer's pits; since the pits are numbered counter-clockwise, the leftmost is pit 12, the next 11, and so on.

The subroutine at line 580 - look, ma, we're calling a subroutine from a subroutine! - prints the number of beans in pit I, padding it with a space if it's less than 10. Since there are only 36 beans all told, this means each time we call this function, we use the same number of characters. This keeps the numbers on the board neatly aligned.

Line 515 prints the number of beans in the computer's home pit; line 520 moves over to where the player's home pit is on the screen, then prints that number as well. (Note that we do not use GOSUB 580 here; whether or not that's a good thing is a matter of taste, and it's easily changed in any event.) Then it sets up for line 525 to print the player's pits and adds a few blank lines before coming back.

You may need to adjust the number of spaces in line 520, based on how your version of BASIC prints numbers.

This subroutine is called elsewhere, but under unusual circumstances. You'll see how when we get there.

code:
100 PRINT "AGAIN";
110 INPUT M:IF M<7 THEN IF M>0 THEN M=M-1:GOTO 130
120 PRINT "ILLEGAL MOVE": GOTO 100
130 IF B(M)=0 THEN 120
140 H=6:GOSUB 200
As previously mentioned, line 100 is jumped to if this is the player's second move; line 110 is jumped to if it's the first. This handles what prompt is used for the player's move.

Line 110 does something legal, yet somewhat odd: it nests IF - THEN commands in order to make sure M is both less than 7 and greater than 0 (in other words, in the range 1-6). BASIC does provide logic operators, so you could write IF M>0 AND M<7 THEN M=M-1:GOTO 130; I'm not sure why the programmer chose this approach.

Note that, from the player's perspective, their pits are numbered 1-6, but internally they're 0-5. That's what the end of line 110 takes care of.

Line 130 makes sure there are actually beans in the pit the player wants to sow from; with that done, we call the subroutine at 200, which actually makes the move and checks to see if the game is over.

Then we do...

code:
150 GOTO 500
...this. Okay. Remember how I said BASIC has subroutines? I was a filthy liar. The only difference between GOTO and GOSUB is that GOSUB leaves a note on the top of what's called a stack saying where to go the next time the program RETURNs. (FOR - NEXT uses a similar stack to keep track of loops that haven't finished yet. Running out of memory for either causes a crash, which is why, much like not wanting to jump out of FOR - NEXT loops, you don't want to nest GOSUB calls too deep.)

It is perfectly legal, therefore, for us to GOTO 500, and when program execution hits the RETURN, it'll take the top note off the stack - and since we returned properly from GOSUB 200, the note on top is from GOSUB 110, back on line 40 or 50, depending. In other words, instead of explicitly calling the 'print the board' subroutine before returning from this one, the flow of the program basically tacks that subroutine onto the end of this one.

Let's move on to line 200.

code:
200 K=M:GOSUB 600
205 E=0:IF K>6 THEN K=K-7
210 C=C+1:IF C<9 THEN F(N)=N(F)*6+K
215 FOR I=0 TO 5:IF B(I)<>0 THEN 230
220 NEXT I
225 RETURN
230 FOR I=7 TO 12:IF B(I)<>0 THEN E=1:RETURN
235 GOTO 220
(Almost) the first thing this subroutine does is call another subroutine which actually updates the board for the move the player (or, eventually, the computer) chose. M, you will recall, is that move; it's passed to that subroutine (well, 'passed' to that 'subroutine'), which will change it, so we back it up to K first. (That's not the only reason we put it in K, but we'll get to that.)

Line 205 starts the code to check if the game is over. The first thing it does is set E to 0, i.e. 'the game is over'. Remember that I said E is whether the game is not over? In BASIC terms, 0 is 'false', and anything else is 'true' - I'll go more into that when we start seriously dealing with bitwise operations and logic statements. So if we reach the end of the subroutine and haven't set E to something else, then the game is over.

Lines 215-235 do the bulk of the work of deciding if the game is over. This is where the first major sin of the game is committed, as we jump out of not one but two loops. (Note also that those loops share a NEXT statement, which is, again, technically correct but kind of odd.) Line 215 determines if the player has a legal move, while line 230 determines if the computer has a legal move; if both are true (i.e., if both IF - THEN statements go off), E is set to 1 - the game is not over - and the function is returned from. If either loop finishes, either the player or the computer has no beans left, and the function is returned from without changing E - the game is over.

Incidentally, this subroutine is somewhat flawed. If either the player or the computer get more than half of the beans in the game into their home pot, then the other participant can't win. This code could check for that by seeing if either B(6) or B(13) is greater than 18 and returning immediately if so.

code:
600 P=B(M):B(M)=0
605 FOR P=P TO 1 STEP -1:M=M+1:IF M>13 THEN M=M-14
610 B(M)=B(M)+1:NEXT P
615 IF B(M)=1 THEN IF M<>6 THEN IF M<>13 THEN IF B(12-M)<>0 THEN 625
620 RETURN
625 B(H)=B(H)+B(12-M)+1:B(M)=0:B(12-M)=0:RETURN
Here's where the board is actually updated. M starts as the move the player or computer made. B(M) is therefore the number of beans in that pit; we assign this to P before removing those beans. P is therefore the number of beans we just picked up.

Lines 605-610 handle sowing the beans. Line 605 does another technically legal, but kind of weird, thing, using P as a loop variable starting at itself to count out the beans. We re-purpose M to mark the pit the next bean will go into, subtracting 14 each time we get back to the player's first pit, and proceed until we're out of beans, at which point M holds the location of the last bean we sowed.

Having sown the beans, we then need to know if a capture has occurred. The check in line 615 does this: IF there's exactly one bean in the current pit (i.e., if the pit was empty before we sowed the last bean into it), THEN IF that pit isn't the player's home pit, THEN IF that pit isn't the computer's home pit, THEN IF there are beans in the pit directly across from the current pit, THEN jump to line 625. If any of those aren't true, we just return from the function.

Line 625 scoops the beans from both pots into the home pot. Which home pot? Well, back on line 140, we set H to 6 before calling line 200, which called line 600. As pit 6 is the player's home pit, the first time we come here is when the player makes their first move. The subroutine that makes the computer's move must, therefore, set H to 13 before coming anywhere near this code.

code:
800 D=-99:H=13
805 FOR I=0 TO 13:G(I)=B(I):NEXT I
810 FOR J=7 TO 12:IF B(J)=0 THEN 885
815 Q=0:M=J:GOSUB 600
820 FOR I=0 TO 5:IF B(I)=0 THEN 845
825 L=B(I)+I:R=0
830 IF L>13 THEN L=L-14:R=1:GOTO 830
835 IF B(L)=0 THEN IF L<>6 THEN IF L<>13 THEN R=B(12-L)+R
840 IF R>Q THEN Q=R
845 NEXT I
850 Q=B(13)-B(6)-Q:IF C>8 THEN 875
855 K=J:IF K>6 THEN K=K-7
860 FOR I=0 TO N-1:IF F(N)*6+K=INT(F(I)/6^(7-C)+.1) THEN Q=Q-2
870 NEXT I
875 FOR I=0 TO 13:B(I)=G(I):NEXT I
880 IF Q>=D THEN A=J:D=Q
885 NEXT J
890 M=A:PRINT CHR$(42+M);:GOTO 200
Which it does. (I know that's a lot of code to go through, but don't worry; I'll repeat sections as needed.) D is used by the AI to determine which move it wants to make; I'll cover exactly how when we get there.

Line 805 backs up the current state of the board into G(); as you'll see, when the computer is trying to decide what move to make, it's going to be calling line 600, so we want it to be able to take back a move.

Line 810 starts a loop that goes through all of the computer's legal moves. J is the current move the computer is considering; if there are no beans in that pit, it jumps to 885, the end of the loop. (Don't worry about line 890 for now, except to note that it has another GOTO to a subroutine, in this case the one that applies the move the computer has chosen and checks for a game over; since we got there with a GOTO, its RETURN will take us back to line 60 or 70, depending.)

I don't know if it's my scan or a typo in the book or what, but the first part of line 815 looks like G=0; it should be Q=0, as shown above. Yet again, I'll cover what that means when we get there. We then set M to the move the computer is considering before calling line 600, which updates the board. (This is why we backed up the board earlier. In a language with proper scope and proper subroutines, we'd pass the subroutine a copy of the board for processing.)

Line 820 starts the real meat and potatoes of the AI. Having made this move, the computer asks, what could the player do in response? I is a possible move by the player; if that wouldn't be a legal move, it jumps to 845, the end of the loop. Line 825 sets L to the last pot the player would sow into and initializes R. The program then does some calculations with R and sets Q to the greater of Q and R before finishing the loop.

So why was I vague in that last sentence? Well, there's a bug in the code here. Q, as we'll discover, is how much the score (or, rather, the difference between how many beans the player has and how many the computer has) will change in the player's favor if they make the most optimal move (for a value of 'most' that takes into account the AI's limits). R is therefore how much the score will change in the player's favor if they make this move. But it's calculated incorrectly. Line 830 is supposed to account for the player putting a bean in their home pot if they go past it, while line 835 is supposed to account for them making a capture on their move, but both are incorrect. Here's a patch.

code:
830 IF L>13 THEN L=L-14:GOTO 830
832 IF L>5 THEN IF L<13 THEN R=R+1
835 IF B(L)=0 THEN IF L<>6 THEN IF L<>13 THEN R=R+B(12-L)+1
If the player has made a move that goes around the board and back into their home territory, they've put a bean into both their own home pit and the computer's, so the relative score hasn't changed. If, on the other hand, they've made a move that ends either in their home pit or in enemy territory, they've put a bean into their home pit but not the computer's, so the relative score has gone up by one in the player's favor. Finally, line 835 previously didn't account for a capture also scoring the bean doing the capturing, so we add 1 there.

Let's take a look at the rest of the J loop again.

code:
850 Q=B(13)-B(6)-Q:IF C>8 THEN 875
855 K=J:IF K>6 THEN K=K-7
860 FOR I=0 TO N-1:IF F(N)*6+K=INT(F(I)/6^(7-C)+.1) THEN Q=Q-2
870 NEXT I
875 FOR I=0 TO 13:B(I)=G(I):NEXT I
880 IF Q>=D THEN A=J:D=Q
885 NEXT J
890 M=A:PRINT CHR$(42+M);:GOTO 200
We previously set Q to the greatest change of the score in the player's favor from all of the player's possible moves - in other words, the change of the score in the player's favor if they make the best possible move in response to the move the computer is considering. At the start of 850, we change Q to be the computer's relative score in that case. (This isn't strictly necessary - we could change lines 860 and 880 to avoid it - but it does no harm.) We then potentially skip over a chunk of code. (Remember C? We're almost to the bit where we explain it.) Whether or not we run that code, we then go to line 875, which restores the backup of the board.

D was set to -99 when we started looking for a move. Whatever the computer's relative score is the first time we go through this loop, it has to be better than that, so we store the current move under consideration in A and update D. The next time through, we only do this if Q is greater than or equal to D. What this means is that, by the time we end the loop, A will contain the move that maximizes the computer's score even if the player tries to minimize it - the best-case scenario of the worst-case scenarios. This is called the maximin, which may sound familiar; min-maxing takes its name, somewhat improperly, from the term minimax, which is used when we're talking about costs instead of benefits - the minimum loss out of potential maximum losses.

Having decided on what move the computer wants to make, we need to actually make it. We set M to A, then - wait, what?

The CHR$() Function

The function CHR$() takes a number and returns its character representation. What's that? Well, you remember me talking about how strings are sorted, and how each character has an internal numeric representation that is used for that sorting (and what a mess that can be varying on implementation)? This function takes one of those numbers and returns the associated character. Fortunately, in every BASIC variety I know of, the digit characters have the same numeric representations: 48 is "0", 49 is "1", and so on up to 57 being "9". Therefore, if the computer decides to make move 7, we add 7 to 42, getting 49, which is "1". In other words, that command prints the digit 1-6, depending on what move the computer makes.

So why not just PRINT M-6;? Well, remember how different varieties of BASIC pad numbers differently when you print them? This is one way to get around that. Doing this guarantees that there isn't a space either before or after the digit of the computer's move. This approach only works for a single digit, mind, but it works, and it works across all versions of BASIC. (If you're curious about how to make a subroutine that does that for multi-digit numbers, just ask.)

Oh, and in case you're curious: the ASC() function reverses the process. ASC("0") = 48.

We've covered, at this point, everything about the program but one thing. Well, there are a few minor things, so let's get them out of the way real quick...

code:
50 IF M=H THEN GOSUB 100

...

70 IF M=H THEN PRINT ",";:GOSUB 800
Remember these two lines? Now that we know that M is the pit where the player or computer put their last bean and H is their home pit, we see how they get a second turn: by putting that bean into their home pit.

code:
900 FOR I=0 TO N-1:PRINTB(I):NEXT I
999 END
This code isn't called anywhere in the program. Even if it was, it doesn't do anything useful - I assume it was intended as debug code, but if it was, it should be printing the contents of F(), not B().

The learning AI

Okay, with that out of the way, let's talk learning AI. We already know how the computer chooses its moves in the first game of a session, when it hasn't learned anything. But how does it improve? Well, we'll need to look at a couple different parts of the code for that. Here's the first:

code:
200 K=M:GOSUB 600
205 E=0:IF K>6 THEN K=K-7
210 C=C+1:IF C<9 THEN F(N)=N(F)*6+K
Line 210 is where the magic starts. At the beginning of the program run, in line 15, we set N to 0, and line 30 sets F(N) to 0. Line 210 is called every time we check if the game is over, i.e. after someone has finished making a move. That means that, the first time we get there, we multiply 0 by 6 and add K, then store the result in F(0). Each time thereafter, we multiply the current value by 6 and add K again, until C reaches 9, at which point we stop doing this.

So what is K? Well, if it's the player's move, we set K to M, the move they chose, before actually making the move. (This happens in line 200.) That means that it's a number from 0 to 5. If the computer made the move, though, K is still set to M, but then we subtract 7 from it (since the computer's moves are in the 7-12 range) - which, funnily enough, makes K a number from 0 to 5.

This means that, after the first move made, either player or computer, F(N) contains the first value of K, or K1 for short. After the second move made, it contains 6 * K1 + K2. After the third move made, it contains 36 * K1 + 6 * K2 + K3. And so on until C hits 9.

At which point, F(0) contains a record of the first eight moves of the game. (With a honking big asterisk, but we'll get to that.) Since any given K is in the range 0-5, we can retrieve each move by dividing by six and taking the remainder, and we don't need the information about who made the move because we can reconstruct that by replaying the moves.

This is the first part of the learning AI. At the end of a game, if the player lost, we don't increase N, and the first thing we do in the new game is set F(N) to 0, which means the computer tosses out the information it just got. If the player won or drew, though, we increase N by one before looping back, which means that information is preserved. The computer is keeping the first several moves of that game in mind for later, hoping to avoid its mistakes.

code:
850 Q=B(13)-B(6)-Q:IF C>8 THEN 875
855 K=J:IF K>6 THEN K=K-7
860 FOR I=0 TO N-1:IF F(N)*6+K=INT(F(I)/6^(7-C)+.1) THEN Q=Q-2
870 NEXT I
Here's the second part of the equation. Starting at line 855, we figure out the K that would result from the computer's next move (for the record, the line could just be K=J-7, since J will always be greater than 6), then enter a loop with I.

The code, unfortunately, makes a misstep here, though not a lethal one. Unlike some languages, in BASIC a FOR - NEXT loop is guaranteed to run at least once. The intent of the code in line 860 is to compare the current game to previous games, but if we haven't had any previous games, we wind up comparing it to itself. I'd change line 850 to end IF C>8 OR N=0 THEN 875.

If the comparison in line 860 is true, then we subtract 2 from Q. Since Q is the score the computer expects to have after making the move it's considering, this says 'if we've been in this situation before, and didn't win, this position isn't as good as we think it is'. So why isn't the comparison just IF F(N)=F(I)? That is the final mystery.

Let's suppose that the first move the player makes is from their fourth pit (internally, pit three), which puts a seed in their home pit, giving them a second move. They then move from their first pit (internally, pit zero), putting their last seed in the now-empty pit and making a capture. Let us suppose further that the player has already won or drew a game. It is now the computer's move. N=1; F(N)=6*3+0=18; C=2; and K is the move the computer is currently contemplating, from 0 to 5. F(N)*6+K is therefore what will be stored in F(N) if the computer actually makes this move. F(0) is the first eight moves of the first game the player won or drew. ^ is BASIC's exponentiation operator, so 6^(7-C) is 65. Since dividing by six once removes the information from the previous move, dividing by six five times in a row removes the information from the five previous moves - and eight minus five is three.

So now we have the first three moves of that previous game... and a ratty bit of fraction, since F() is a floating-point array. (This variety of BASIC doesn't have integer variables.) We get rid of them with the INT() function - but we add .1 first. Why? Well, .1 is less than 1/6, so this won't ever push the fractional bit over 1, but it will handle any fractional weirdness where dividing 6 by 6 somehow produces 0.9999. (It has to do with how floating-point numbers in general, not just in BASIC, are handled. BASIC just handles it more poorly than other languages.)

In other words, if this third move would replicate the first three moves of a previous game - and if the player used this strategy in that game, at least one of the moves the computer's considering will - we mark that move as not being as good as we think. This is likely to make the computer choose a different move, which will hopefully increase its chances of winning, and if it doesn't, well, that's more data for next time. And if the player keeps winning by using those first two moves, the computer is going to be more and more unlikely to choose any option that led to a loss, since it can subtract two more than once.

Once we've made eight moves, we're in unknown territory, since that's as far as the computer can keep track of. At that point, it stops calling the 855-870 code and does the best it can.

There's a bug in this code, incidentally. If C=8, C^(7-C)=1/6, so we wind up multiplying F(I) by 6. C is updated after this part of the code, so C=8 here means the computer's making its ninth move. Line 850 should end IF C>7 OR N=0 THEN 875.

That Honking Big Asterisk

So that thing I said about F() storing the first eight moves of a given game? Weeeell... that depends on F() being capable of holding a number as large as 68-1, or 1679615, without issue. Fortunately, most varieties of BASIC can handle that. Unfortunately, they may not be able to handle it perfectly. I mentioned floating point weirdness; because of the way these numbers are stored internally, a number that displays as 6 may actually be something like 5.9999997 or something, which is why we added .1 in line 860.

So, you might say, your variety of BASIC supports integer arrays. Why not just use those? Well, you can, but there are a few things to be careful of. One is that the behavior you get when you divide an integer variable by something else also depends on your variety of BASIC. 11/6 may be 1.8333... and get rounded down by INT() to 1, but F%(N)/6, with F%(N) containing the integer 11, may give 1.8333... or it may give 1. Or 2, the division rounding to the nearest integer instead of rounding down.

The other thing you need to be aware of is that integers have limits, just like floating point numbers do. With floating point numbers, it's generally significant digits (i.e, you might have trouble storing exactly 1.795668586868469) but with integers, it's a hard limit on the minimum and maximum values - and in, say, QuickBASIC, that maximum limit is 32767! Fortunately QuickBASIC also provides the long integer type (the & suffix), which goes up to around two billion (precisely, 2147483647), but, say, Commodore BASIC V2 does not.

If your flavor of BASIC provides neither sufficiently accurate floating point numbers nor sufficiently large integers, you could change F() to have two dimensions, so F(N, C) would hold the Cth move of game N. You'd replace line 860 with a loop through the second dimension's elements, looking for all of them to match your current game's up until the point you're at.

Still. This program, once you deal with the major sins of 'jumping out of loops' and 'assuming a certain precision from variables', is a compact, well-organized program that plays an acceptable game of 'Awari' and learns from its mistakes. It'd be relatively easy to change it to follow the actual rules of Kalah, and the AI could be adapted to any reasonably simple game. I didn't expect much from this program going in, and I'm pleased by just how wrong I was!

In case you want to try it for yourself, here's the full, bug-patched listing. (It also fixes a bug I didn't mention: if N>50, F(N) is invalid. The fixed program will just stop learning at that point. I also added something to keep pressing enter at the input prompt from doing something unexpected.) It'll work on even a 40-column monitor, provided you adjust lines 5-7. Omit the comment on line 520, or else the line will be too long for (at least) Commodore BASIC V2.

code:
5 PRINT TAB(34);"AWARI"
7 PRINT TAB(15);"CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY"
10 DATA 0
15 DIM B(13),G(13),F(50):READ N
20 PRINT:PRINT:E=0:IF N>50 THEN N=50
25 FORI=0 TO 12:B(I)=3:NEXT I
30 C=0:F(N)=0:B(13)=0:B(6)=0
35 GOSUB 500
40 PRINT "YOUR MOVE";:GOSUB 110
45 IF E=0 THEN 80
50 IF M=H THEN GOSUB 100
55 IF E=0 THEN 80
60 PRINT "MY MOVE IS ";:GOSUB 800
65 IF E=0 THEN 80
70 IF M=H THEN PRINT ",";:GOSUB 800
75 IF E>0 THEN 35
80 PRINT:PRINT"GAME OVER"
85 D=B(6)-B(13):IF D<0 THEN PRINT "I WIN BY";-D;"POINTS":GOTO 20
90 N=N+1:IF D=0 THEN PRINT "DRAWN GAME":GOTO 20
95 PRINT "YOU WIN BY";D;"POINTS":GOTO 20
100 PRINT "AGAIN";
110 M=-1:INPUT M:IF M<7 THEN IF M>0 THEN M=M-1:GOTO 130
120 PRINT "ILLEGAL MOVE": GOTO 100
130 IF B(M)=0 THEN 120
140 H=6:GOSUB 200
150 GOTO 500
200 K=M:GOSUB 600
210 E=0:IF B(6)>18 THEN RETURN
220 IF B(13)>18 THEN RETURN
230 IF K>6 THEN K=K-7
240 C=C+1:IF C<9 THEN F(N)=F(N)*6+K
250 FOR I=0 TO 5:IF B(I)<>0 THEN E=1
260 IF B(I+7)<>0 THEN E=1
270 NEXT:RETURN
500 PRINT:PRINT"   ";:REM 3 SPACES
505 FOR I=12 TO 7 STEP -1:GOSUB 580
510 NEXT I
515 PRINT:I=13:GOSUB 580
520 PRINT"                       ";:PRINT B(6):PRINT "   ";:REM 23 SPACES, THEN 3 SPACES
525 FOR I=0 TO 5:GOSUB 580
530 NEXT I
535 PRINT:PRINT:RETURN
580 IF B(I)<10 THEN PRINT " ";
585 PRINT B(I);:RETURN
600 P=B(M):B(M)=0
605 FOR P=P TO 1 STEP -1:M=M+1:IF M>13 THEN M=M-14
610 B(M)=B(M)+1:NEXT P
615 IF B(M)=1 THEN IF M<>6 THEN IF M<>13 THEN IF B(12-M)<>0 THEN 625
620 RETURN
625 B(H)=B(H)+B(12-M)+1:B(M)=0:B(12-M)=0:RETURN
800 D=-99:H=13
805 FOR I=0 TO 13:G(I)=B(I):NEXT I
810 FOR J=7 TO 12:IF B(J)=0 THEN 885
815 Q=0:M=J:GOSUB 600
820 FOR I=0 TO 5:IF B(I)=0 THEN 845
825 L=B(I)+I:R=0
830 IF L>13 THEN L=L-14:GOTO 830
832 IF L>5 THEN IF L<13 THEN R=R+1
835 IF B(L)=0 THEN IF L<>6 THEN IF L<>13 THEN R=R+B(12-L)+1
840 IF R>Q THEN Q=R
845 NEXT I
850 Q=B(13)-B(6)-Q:IF C>7 OR N=0 THEN 875
855 K=J:IF K>6 THEN K=K-7
860 FOR I=0 TO N-1:IF F(N)*6+K=INT(F(I)/6^(7-C)+.1) THEN Q=Q-2
870 NEXT I
875 FOR I=0 TO 13:B(I)=G(I):NEXT I
880 IF Q>=D THEN A=J:D=Q
885 NEXT J
890 M=A:PRINT CHR$(42+M);:GOTO 200
Oh, there's one bug (that I know of) still left in this code; I only thought of it while I was proofreading this post. Do you know what it is? Hint behind spoilers: the computer's AI is making an incorrect assumption about the player's next move.

FredMSloniker fucked around with this message at 17:07 on Jun 26, 2020

Nidoking
Jan 27, 2009

I fought the lava, and the lava won.
I was going to ask "What if the computer's move ends the game? Its AI doesn't have a check for that." Apparently, you also spotted that.

EDIT: Actually, it looks like it also doesn't consider its own second move when evaluating the maximin. It just makes one move for itself and one for the player, although having made its first move, it will reconsider all of that for the second move.

Nidoking fucked around with this message at 00:09 on Jun 26, 2020

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

Nidoking posted:

I was going to ask "What if the computer's move ends the game? Its AI doesn't have a check for that." Apparently, you also spotted that.

EDIT: Actually, it looks like it also doesn't consider its own second move when evaluating the maximin. It just makes one move for itself and one for the player, although having made its first move, it will reconsider all of that for the second move.

You're correct on both counts. The AI assumes that, after the computer's move, the player will be the next one to make a move. This could be untrue because the move it's considering ends the game or because the move it's considering gives it a second move. Fortunately, this never causes the game to outright crash, but it does cause it to mis-weigh its options.

Fixing this is possible, but it isn't trivial. The subroutine at 800 needs to know whether it's the computer's first move or its second to know if its move could lead to a second move; we can provide that by changing lines 60 and 70 to set some variable. We also have to remove line 240 from its current position and put it somewhere else so that we can check if a certain move would end the game without falsely updating the AI's memory. Last but not least, the subroutine at 800 has to be changed to actually account for those possibilities. If anyone wants to come up with a patch, please do! As for me, I'll be preparing for my coverage of the next program in the book.

The preparations will involve a lot of grunting. I'll be quite out of grunts when I'm done.

FredMSloniker fucked around with this message at 05:40 on Jun 26, 2020

loaf
Jan 25, 2004



When I was little I read those books so much the bindings fell apart. Sometimes the typos and the syntax differences from GWBASIC really messed with me after spending a whole afternoon keying in a program. My first real programming job involved a lot of Hewlett-Packard BASIC, maybe Dijkstra was right.

ManxomeBromide
Jan 29, 2009

old school

loaf posted:

My first real programming job involved a lot of Hewlett-Packard BASIC, maybe Dijkstra was right.

He wasn't.

(The quote in question, for those unfamiliar with it, is "It is practically impossible to teach good programming to students that have had a prior exposure to BASIC: as potential programmers they are mentally mutilated beyond hope of regeneration.")
  • The battles over how programs should be structured were fought throughout the 1970s and early 1980s, with the ultimate conclusion being that everyone had the wrong idea. (The most famous example of this would be that the same faction that thought that variables ought to be local to the function that declared them was the same faction that thought that "break" and "continue" should not exist as control structures, and that functions should never have a "return this value immediately" operation. They didn't think this for no reason, but the real solution to the problem they were trying to address is basically "destructors"... and while that term wasn't really common back then, equivalents existed in the form of constructs like "unwind-protect".)
  • It's even less true now because by the early 1990s BASIC had become a language that mostly fit the rules for code structure that Dijkstra's faction argued for. I'm tempted to convert the Awari program to a fully modern dialect as an example; it looks very different. But let's ignore that for now...
  • Vast numbers of people who went into tech and who was born in the late 1970s or early 1980s was exposed to BASIC at a very young age via the Apple II or the BBC Micro or their competitors, and this exposure certainly did not slow them down.
  • Lessons from the future can be brought back to the past. I wasn't thinking of Lua when I wrote it, but it's not a coincidence that FredMSloniker saw my modifications to Amazing and concluded that I had replicated the logic he'd use if he were to take on the task in Lua. My own thought process was closer to "I need a dynamic list. I've got arrays. I can make an equivalent to java.util.ArrayList."
  • Pretty much all the modern programming disciplines existed in the 1970s. The kind of control we see in BASIC existed at the asm level for basically the entire history of the electronic computer. "Structured" programming's basis were the algorithmic specification languages of the 1960s. So was object-orientation, really; C++ is based on Simula 67 in part, and that "67" is for 1967. Smalltalk, which inspired all the other major approaches to OOP, was from 1972. So as Prolog, the archetypical declarative programming language. Functional programming is older than digital computers if we treat the lambda calculus as a form of it, and it reached a recognizable modern form with Scheme, released in 1975, the same year as Dijkstra's quote. Nowadays a skilled programmer is expected to be able to attack problems using several of these approaches, and the mark of an experienced one is to know which paradigm will be most effective given other aspects of the problem.

1980s BASIC is never the right approach, these days. I'll give Dijkstra that. But if you can't write something, even in 1980s BASIC, that is recognizably a map-reduce operation, you don't really fully grasp map-reduce. At best you can command it.

ManxomeBromide
Jan 29, 2009

old school

FredMSloniker posted:

Okay. Remember how I said BASIC has subroutines? I was a filthy liar. The only difference between GOTO and GOSUB is that GOSUB leaves a note on the top of what's called a stack saying where to go the next time the program RETURNs.

This is very fun, and the optimization that happens here can be both (1) made even more awful and (2) done mechanically.

The more awful bit: if that GOSUB were to a routine that started right after this one, you would not even need the GOTO. You could simply let control fall into the second function directly.

As for doing it mechanically... there are two transformations you can do to make this happen "automatically:"
  • First: it's always possible as noted to transform GOSUB nnn:RETURN into GOTO nnn for any N. It's also more efficient. If you want to make JavaScript devs squirm, point out to them that this is 1980s BASIC demonstrating generalized tail-call optimization. You won't even be completely lying, but don't try this on a Scheme or Haskell dev.
  • If there's a GOTO nnn line and nnn is the next line, you can just drop the statement entirely.
So, if the last thing you do in a function is call the next one, you can just drop the call and the return! :science:

FredMSloniker posted:

The other thing you need to be aware of is that integers have limits, just like floating point numbers do. With floating point numbers, it's generally significant digits (i.e, you might have trouble storing exactly 1.795668586868469) but with integers, it's a hard limit on the minimum and maximum values - and in, say, QuickBASIC, that maximum limit is 32767! Fortunately QuickBASIC also provides the long integer type (the & suffix), which goes up to around two trillion (precisely, 2147483647), but, say, Commodore BASIC V2 does not.

Nitpick: you're off by a factor of a thousand. That is two billion. That is also the range of a 32-bit signed integer, still a very common data range in modern programs!

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

ManxomeBromide posted:

Nitpick: you're off by a factor of a thousand. That is two billion. That is also the range of a 32-bit signed integer, still a very common data range in modern programs!

To quote myself,

FredMSloniker posted:

You're correct on both counts.

I've amended the post accordingly. Interestingly, Commodore BASIC uses 16-bit signed integers, but... 40-ish bit floating point numbers? I don't entirely understand the specification.

Carbon dioxide
Oct 9, 2012

Is it legal to do a complete for loop in a single line? so FOR blabla : some operation : NEXT

pthighs
Jun 21, 2013

Pillbug
Holy crap I had this book as a kid and spent hours with it on my Dad's IBM PC.

ManxomeBromide
Jan 29, 2009

old school

Carbon dioxide posted:

Is it legal to do a complete for loop in a single line? so FOR blabla : some operation : NEXT

Yes. Just, as noted in the "Animals" commentary, make sure that the NEXT always executes, and you really should make sure that there is exactly one NEXT for every for. It is theoretically possible to write code that handles this correctly, but seriously, just don't. Put the NEXT at the very end and conditionally jump to it as needed. NEXT is not C's "continue".

Some operations (IF, for instance) track by line number, where failing a test skips the whole rest of the line. They're the unusual cases. Normally, things like GOSUB or NEXT or FOR or whatever track at the individual statement level, not the line level.

loaf
Jan 25, 2004



Our BASIC codebase was pretty clean and organized, but this was a common pattern and source of weird bugs

code:
IF Loopify THEN
  FOR I=0 to 10
END IF

REM do stuff that hopefully doesn't modify Loopify

IF Loopify THEN
  NEXT I
END IF

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

loaf posted:

Our BASIC codebase was pretty clean and organized, but this was a common pattern and source of weird bugs

code:
IF Loopify THEN
  FOR I=0 to 10
END IF

REM do stuff that hopefully doesn't modify Loopify

IF Loopify THEN
  NEXT I
END IF

Eugh. I'd set Loopify to whatever number of loops you wanted and have I go from 1 to that, get rid of the IF - THENs altogether.

And speaking of 'eugh'...



Nnnnnnnnnnnngh.

NNNNNNNNNNNNNNNNNNGH.

Okay, I get it. Guess-the-number games have probably been around since there were numbers. For computers especially, they're super easy to code, and they don't require any particular genius to learn to play. But they're so boring, and there are so many of them. In this book alone, there are eight games that involve you trying to figure out a random number or other pattern - and that's not counting things like 'guess the correct set of coordinates' or 'guess the word'.

Still. I choose to look at the bright side. And the bright side, in this case, is that this program does what most of the programs in this collection don't: bulletproof, to an almost excessive degree, its input. So I'm going to focus on that part of the program.

code:
220 FOR I=1 TO 20
230 PRINT "GUESS #";I,
240 INPUT A$
Note the use of the comma at the end of line 230. This makes sure that the question mark that INPUT prints remains lined up even if the player needs to make a tenth guess.

So the player has typed in something, and we've stored it in A$. Now what?

code:
245 IF LEN(A$)<>3 THEN 630

...

630 PRINT "TRY GUESSING A THREE-DIGIT NUMBER.":GOTO 230
So now we know that the input has exactly three symbols in it. That eliminates "YOUR MOM" or "23", but it doesn't eliminate "2.3" or "ABC".

code:
250 FOR Z=1 TO 3:A1(Z)=ASC(MID$(A$,Z,1)):NEXT Z
260 FOR J=1 TO 3
270 IF A1(J)<48 THEN 300
280 IF A1(J)>57 THEN 300
285 B(J)=A1(J)-48
290 NEXT J
295 GOTO 320
300 PRINT "WHAT?"
310 GOTO 230
There are three problems with this code. The first is that we're, once again, jumping out of a FOR - NEXT loop. The second is that the error message we show is unhelpful; I'd instead re-use the error at line 630. The third is that the process is inefficient; we store the ASCII codes for the input characters in an array, test to see if they represent digits, then store the digits in a second array. This wastes memory, albeit not much memory. Here's how I would rewrite the code.

code:
250 ZF=0
260 FOR J=1 TO 3
270 B(J)=ASC(MID$(A$,J,1))-48
280 IF B(J)<0 OR B(J)>9 THEN ZF=1
290 NEXT J
300 IF ZF=1 THEN 630
At this point, we know the player has entered a three-digit number, and we have that number stored in the B() array. Earlier the program generated a three-digit number and stored it in the A() array. At this point, you'd be forgiven for thinking the input checking is done... but there's one more thing to check.

code:
320 IF B(1)=B(2) THEN 650
330 IF B(2)=B(3) THEN 650
340 IF B(3)=B(1) THEN 650

...

650 PRINT "OH, I FORGOT TO TELL YOU THAT THE NUMBER I HAVE IN MIND"
660 PRINT "HAS NO TWO DIGITS THE SAME.":GOTO 230
This is actually kind of a cute bit of code. The instructions at the start of the program do, indeed, omit this bit of information. This check prevents the player from wasting a turn making a guess that can't possibly be correct and gives the program the tiniest bit of flavor.

So now we definitely have a valid guess. (If you wanted to get fancy, you could have the program keep track of previous guesses in, say, C((guess number), (digit)), and check to see if the player's repeated themselves, but that's more effort than I'm prepared to expend.) Now it's time to figure out what clues we're going to give, which I suppose is interesting enough to look at.

code:
350 C=0:D=0
360 FOR J=1 TO 2
370 IF A(J)<>B(J+1) THEN 390
380 C=C+1
390 IF A(J+1)<>B(J) THEN 410
400 C=C+1
410 NEXT J
420 IF A(1)<>B(3) THEN 440
430 C=C+1
440 IF A(3)<>B(1) THEN 460
450 C=C+1
460 FOR J=1 TO 3
470 IF A(J)<>B(J) THEN 490
480 D=D+1
490 NEXT J
C is the number of times the program should say "pico" (a digit is correct but in the wrong position), while D is the number of times the program should say "fermi" (a digit is correct but in the right position). The way this is coded uses way too many lines, though. I'd rewrite it like this.

code:
350 C=0:D=0
360 FOR J=1 TO 3
370 J1=J-1:IF J1=0 THEN J1=3
380 J2=J+1:IF J2=4 THEN J2=1
390 IF A(J1)=B(J2) THEN C=C+1
400 IF A(J2)=B(J1) THEN C=C+1
410 IF A(J)=B(J) THEN D=D+1
420 NEXT J
Here's a question for the class: how would this code fail if the computer could use any three-digit number? Also, what's the one thing the input bulletproofing misses?

And that's all the time I'm prepared to spend on this program. Let's squeeze a second one in.





Obviously the utility of this program was somewhat limited even back in the day, but hey, who doesn't like printing huge obnoxious banners? Let's get started with the analysis.

code:
10 INPUT "HORIZONTAL";X
20 INPUT "VERTICAL";Y
21 INPUT "CENTERED";L$
22 G1=0: IF L$>"P" THEN G1=1
23 INPUT "CHARACTER (TYPE 'ALL' IF YOU WANT CHARACTER BEING PRINTED)";M$
29 PRINT "STATEMENT";
30 INPUT A$
35 INPUT "SET PAGE";O$
I fixed the typo in line 21. Note the weirdness in line 22. As I said earlier, you can compare strings based on their internal character codes. I presume the intent is that G1 is set to 1 if the user types "Y", but the way it does it is just odd.

Anyway. X is the width of the output 'pixels', while Y is the height. G1 is whether the output should be centered on the page or not. M$ is what symbol to use for the output; if the user types, for instance, "*", then it'll use asterisks for everything, but if they type "ALL", it'll make the letters out of smaller versions of the same letters. (See the sample output, but note that the paper was turned sideways.) A$ is the actual message.

But what is O$, you might ask? It isn't anything, as it turns out. What it's asking you to do when it says "SET PAGE" is to advance the paper in the teleprinter to the top of the next page, presuming you're using fanfold paper and not just a continuous roll. The user just hits Enter, and the process starts.

code:
40 A=ASC(LEFT$(A$,1))
50 REM
60 REM
The program never uses A, so I don't know what this code is here for. Probably a remnant of something that was later changed.

code:
70 FOR T=1 TO LEN(A$)
80 P$=MID$(A$,T,1)
90 FOR O=1 TO 50
95 READ S$,S(1),S(2),S(3),S(4),S(5),S(6),S(7)
96 IF P$=" " THEN 812
100 IF P$=S$ THEN 200
120 NEXT O
200 RESTORE
Yet another program committing the cardinal sin of jumping out of a FOR - NEXT loop. Still, by looking at the DATA statements later in the program, you can see what this is doing. One line at a time, it reads the information that will be needed to print a letter, number, or symbol in, checks to see if that information is needed for this symbol, and keeps going until it gets what it wants. Line 96 is special-case handling for a space that jumps to a dedicated space-printing routine later; it should really be moved to (say) line 85, so we don't bother reading data at all. And the program doesn't properly handle the user typing a symbol that doesn't appear in the data. Still, at least in theory, when the program hits line 200, it has the information it needs stored in the S() array (note that we never explicitly DIMmed it, but that's fine; the first time we use it, it's given the elements 0-11, which is more than we need). We then use RESTORE to put the data pointer back at the beginning for the next time around.

code:
201 X$=M$
202 IF M$="ALL" THEN X$=S$
205 FOR U=1 TO 7
210 FOR K=8 TO 0 STEP -1
230 IF 2^K<S(U) THEN 270
240 J(9-K)=0
250 GOTO 280
270 J(9-K)=1: S(U)=S(U)-2^K
272 IF S(U)=1 THEN 815
280 NEXT K

...

815 F(U)=9-K: GOTO 445
A few things to say about this code. Line 202 could use P$ instead of S$; indeed, once we've compared the two in line 100, S$ has outlived its usefulness.

Line 205 starts a loop that will print the seven columns of each character (once we've turned the paper sideways, that is). Line 210 starts the process of converting the data for that column into pixels. Take a look at line 900.

code:
900 DATA "A",505,37,35,34,35,37,505
We previously set S(1) to 505. The first time through the loop at 205, U=1; the first time through the loop at 210, K=8. 2^K is 28, or 256. (As a side note, I know all the powers of two up to 65,536 off the top of my head. They're quite useful for purposes like this.)

Since 256 is less than 505, we jump to line 270. J(1) is set to 1, and S(1) is set to S(1)-2^K, or 249. Since this is not 1 (as seen in line 272), we go to the next loop of K.

K is now 7. 2^K is now 128, which is less than 249. J(2) is set to 1, and S(1) is set to 121. We loop again.

K is now 6. 2^K is now 64, which is less than 121. J(3) is set to 1, and S(1) is set to 57. We loop again.

K is now 5. 2^K is now 32, which is less than 57. J(4) is set to 1, and S(1) is set to 25. We loop again.

K is now 4. 2^K is now 16, which is less than 25. J(5) is set to 1, and S(1) is set to 9. We loop again.

K is now 4. 2^K is now 8, which is less than 9. J(6) is set to 1, and S(1) is set to 1. At this point, line 272 goes off; we go to line 815, which sets F(U) to 9-K, or 5. Then we go to line 445.

But what if S(1) started as an even number, you ask? Well, S(4) was set to 34. When U is 4, we start the K look again. 256 is not less than 34, so we set J(1) to 0; similarly, we set J(2) and J(3) to 0, since 128 and 64 are also not less than 34. 32 is, so we set J(4) to 1 and S(4) to 2 and proceed. 16 isn't less than 2; nor are 8, 4, or 2, so S(5) through S(8) are also set to 0. Which brings us at last to K=0; 1 is less than 2, so we set J(9) to 1 and - well, look at that! We set S(4) to 1. So we wind up visiting 815 after all.

Therefore, we almost always jump out of the loop, except under one circumstance: if S(U) is 1, as it is in, say, the data for "!". In that case, none of the nine values for K produce 2^K that is less than one, so all nine of the J() elements get set to 0, and the loop ends, falling through... to line 445, as it happens. And F(U) never gets set.

Let's talk about what this code actually does. As you may already have realized, it decodes a number from the data set for a given symbol into a column of pixels of that symbol. J(1) is the bottom-most pixel, while J(9) is the topmost. Each symbol has seven numbers that define it, so each character has a 7x9 pixel grid. We only need to look at one column of a letter at a time, though, because we print each one before moving on to the next. (Since the final output will be turned sideways, we're printing by column, not by row.)

So how do we get that column on paper?

code:
445 FOR T1=1 TO X
447 PRINT TAB((63-4.5*Y)*G1/(LEN(X$))+1);
Line 445 starts a loop that'll run for X iterations. (Each column is X 'columns' wide, as we decided earlier.) We then hit the mess of code on line 447. Let's take a closer look.

First off, if G1 is 0 - that is, if we chose not to center the printout - the whole equation inside TAB() resolves to 1. Columns are numbered starting at 1, so this statement only actually does anything if G1 is 1. If it is, the equation simplifies to (63-4.5*Y)/(LEN(X$))+1. This gives the number of spaces we need to start the line with in order to center the text. (If you're not sure how that works, plug a few values in for Y and LEN(X$) and see what comes out.)

There are two things to take away from this: first, this program centers the characters on column 64, so it's expecting the paper to be pretty wide. (Teleprinters often supported 132-column printout.) Second, it actually accounts for the length of X$, which means it can handle the user specifying, say, "NOM" as a symbol to use in the printing. (Three, there's no safety net for out-of-bounds inputs, but we're used to that by now.)

code:
450 FOR B=1 TO F(U)
Line 450 starts a loop from 1 to F(U). As you may recall, line 815 set that to 9-K at the point where we exited the loop, so we're going to be looking at J(1) through J(5). But what about J(6), which we also set? Well, if you take a look at the first column of the letter A in the sample output, you'll note that it's only five pixels high. So the last pixel set wasn't a real pixel.

In other words, for those of you more familiar with bitwise operations, the numbers in the data set aren't strict bitwise representations of the letters; a bit is turned on after the last bit that's actually supposed to be turned on, unless - well, take a look at the numbers yourselves. I don't want to confuse the folks who aren't familiar with bitwise operators. The important takeaway is that the 1 stored in the highest position of J() isn't a 'real' pixel unless it's in J(9).

Oh, and remember how, if the data number was 1, F(U) never gets set? Turns out it doesn't matter; however many times we loop through B, we're not actually printing anything, so whatever number is left in there is fine. (Though we really ought to special-case an empty column to save time.)

Anyway.

code:
460 IF J(B)=0 THEN 500
465 FOR I=1 TO Y: PRINT X$;: NEXT I
470 GOTO 600
500 FOR I=1 TO Y
510 FOR I1=1 TO LEN(X$)
520 PRINT " ";: NEXT I1
530 NEXT I
So if J(B) is 0 - i.e., if there's a blank pixel here - we jump to line 500, where we print Y*LEN(X$) spaces, accounting for the pixel height and the symbol width both. (We could simplify that with a single loop from 1 to Y*LEN(X$). Some BASIC varieties even supply functions to take that number directly and move the cursor as desired.) If J(B) is 1, however, we print Y copies of X$, drawing the pixel. In either case, we wind up at line 600.

code:
600 NEXT B
620 PRINT
630 NEXT T1
700 NEXT U
After we've printed, or not printed, each pixel, we move to the next line with line 620. Finishing the T1 loop prints that line X times total, making the column sufficiently wide; finishing the U loop moves through the remaining columns of the symbol, printing them in turn.

code:
750 FOR H=1 TO 2*X: PRINT: NEXT H
800 NEXT T
806 FOR H=1 TO 75: PRINT: NEXT H
810 END
812 FOR H=1 TO 7*X: PRINT: NEXT H
813 GOTO 800
And this completes the puzzle. After each character, we print 2*X blank lines before moving to the next. After the last character, we print 75 blank lines, the better to be able to tear the result out of the teleprinter. And that code on lines 812-813 is called if the symbol we're printing is " "; it just prints 7*X blank lines. (It should probably print 9*X blank lines, since it's skipping over the bit that prints spaces between symbols, but the programmer may have thought the spaces looked too wide that way.)

So! What're the major problems with this program? Well, there are jumping out of FOR - NEXT loops and a lack of input checking, which are nothing new - though really, it should at least check to see if it's going to overflow a single line before the user goes through a box of paper printing "HAPPY BIRTHDAY PRINCESS CELEST" or something. I'm also obscurely disappointed that you can't use "ALL" to print pixels when you could use "LOVE" or something, but hey.

There's also no error handling for typing a symbol that isn't in the program's set of known symbols. Incidentally, I'd read all of that data into a large array or two instead of having to jump back and forth through it, but if memory's a concern, this approach works. (I note that you could order the DATA lines in descending frequency of symbol use, which would at least speed up the 'start from the beginning and go until you find the symbol you want' approach. It looks like the author has made a vague effort in that direction.)

Other than that, though, it does what it says on the tin. It just isn't worth using for anything if you're not on a teleprinter. There are varieties of BASIC that let you send things to the printer, but they are (naturally) interpreter-dependent, and also hardware-dependent - early varieties of BASIC make assumptions about what a printer will do that may not be supported by modern printers, or by modern operating systems. For instance, you can print text from within the VICE emulator - but it gets stored in a text file, and you have to choose between 'contains text, but doesn't support the Commodore's special characters' or 'contains a pixel-by-pixel representation of the output with plus symbols for black pixels'. And even if you could run QBASIC under Windows 10, I dunno what'd happen if you tried to LPRINT.

So. One program that isn't useful anymore and one that wasn't really useful then. I'm looking forward to the next program, which is actually kind of interesting-looking.

FredMSloniker fucked around with this message at 03:07 on Jun 27, 2020

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
Bonus content! There are a number of amusing doodles in the book. Here are some from pages I didn't share for one reason or another.



(On the page with the 'Awari' code.)



(On a page of its own.)

Carbon dioxide
Oct 9, 2012

You know, I was thinking and the best BASIC games I've ever seen are probably GORILLA.BAS and NIBBLES.BAS that came with MS-DOS QBasic.

Those are way more complex than anything in this book and you'd probably wouldn't want to copy them from paper.

Anyway, I guess the stuff they do where they have a sort of full screen graphical game where the whole screen updates instead of the terminal just scrolling as it prints things is something for which you need a much more modern computer than anything this book was aiming for.

Adbot
ADBOT LOVES YOU

nielsm
Jun 1, 2009



It's more that doing full screen graphics or pseudo-graphics typically needs some kind of hardware specific knowledge. Every system tends to have its own way of setting graphics modes, colors, or moving the text cursor around the screen. It wouldn't be easy to make those portable between different systems.

One way might be to leave gaps in the program for the system specific things, and then have a separate section in the text for "on system A enter these additional lines, on system B enter these instead, on other systems you're on your own".

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply