10 PRINT"LET'S READ BASIC COMPUTER GAMES":GOTO 10

Put your Let's Plays in here.
Post Reply
User avatar
Simulcast from the Something Awful forums, for those of you who don't feel like hanging out there anymore, IT'S

Image

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.”

Image

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!

User avatar
Image

Image

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

Code: Select all

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.

Code: Select all

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.)

Code: Select all

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.

Code: Select all

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.

Code: Select all

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.

Code: Select all

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.

Code: Select all

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.

Code: Select all

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.

Code: Select all

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.

Code: Select all

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.

Code: Select all

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.)

Code: Select all

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.

Code: Select all

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.

Code: Select all

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.)

Code: Select all

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.

Code: Select all

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.

Code: Select all

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...

Code: Select all

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...

Code: Select all

970 PRINT"SORRY, YOU LOSE"
980 IF N<Q THEN 240
...and unless they've bet their last dollar...

Code: Select all

240 Q=Q-N
250 GOTO 120
...we go there. But what if they have?

Code: Select all

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.

Code: Select all

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.

Code: Select all

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?

User avatar
I'm going to be updating roughly once a day until I get caught up with the SA thread, at which point I'll simulcast them. That said, you don't have to wait until then to start posting! I'm eager to hear what you have to say.

User avatar
Site Admin
Oh man, I'm glad you're doing this, Fred. I tried to run a thread with the USBORNE books, but... Well, things happened.

Anyway, improvements! Let's see, there are, on many systems, the suits of cards as ASCII, and even if there weren't, we could use the custom ASCII slots for them. Unfortunately, the way of creating them varies from BASIC to BASIC (I'm used to my childhood computer, the BBC Micro, which uses... VDU 223, if I'm remembering right. Anyway, from there, we'd define the character (usually 8x8) using the numbers from 2's compliment counting of binary.

Soooo a spade would be...

Code: Select all

24  (00011000)
64  (00111100)
64  (00111100)
126 (01111110)
126 (01111110)
92  (01011010)
24  (00011000)
64  (00111100)
Errrr... My math may be wrong, but that's the general idea. And we'd print those alongside the numbers. Hell, we could even get fancy with it, make 2 symbols for each suit, 1 upside down, DRAW boxes, bam, cards.

User avatar
As someone whose middle school attempted to teach our class how to make screensaver-looking things in BASIC long, long ago, I look forward to this thread.

User avatar
JamieTheD wrote:
Sun Jun 28, 2020 7:29 pm

Code: Select all

24  (00011000)
64  (00111100)
64  (00111100)
126 (01111110)
126 (01111110)
92  (01011010)
24  (00011000)
64  (00111100)
Errrr... My math may be wrong,
Afraid so. I see what you're doing, but from left to right the bits are numbered 128, 64, 32, 16, 8, 4, 2, and 1. (I memorized the powers of two up to 65,536 as a child, for obvious reasons.) So the lines you've numbered 64 should be 32 + 16 + 8 + 4, or 60, and the line you've numbered 92 should be 64 + 16 + 8 + 2, or 90.

You also haven't taken into account that there's no separation between 8x8 characters in those BASIC varieties that use them, which is why the Commodore spade symbol looks like:

Code: Select all

016 (00001000)
056 (00011100)
062 (00111110)
127 (01111111)
127 (01111111)
056 (00011100)
062 (00111110)
000 (00000000)
I'm not sure why the blank lines are the bottom row and the left column, but I assume it's so the symbol doesn't bleed into the left border.

That said, you could certainly take advantage of reprogrammable character sets if your flavor of BASIC has them! Maybe you'd like to grab some characters you aren't using and turn them into pixel-art card faces. That's a bit out of the scope of my project, but don't let that stop you!

User avatar
As I'm catching up with SA posts, I had to decide on how to handle people there commenting on posts. For now, I'm going to re-edit the posts to be useful for here; we'll see what I do when I catch up.

Tiggum re-coded Acey Ducey, and in the process used a subroutine. Let's cover what that is.

Subroutines

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.)


Carbon dioxide wrote: 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.



I then said I'd look into an improved version of my own, but I realized I should check what the rules of the game actually were. Wikipedia told me that the game is actually called Acey Deucey, note the spelling, and further said this:
Wikipedia wrote: 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.
Given that, I decided to write a version of the program that followed those rules instead. Which I did, but I decided not to share it because it was fairly lengthy (190 lines, mainly because as a self-imposed challenge I stuck with the 'one line per statement' rule of the original program) and didn't do anything particularly special. I did, however, share a subroutine I worked up for handling drawing a card from the deck, using an actual (software implementation of a) deck instead of just RNGing up a deck. I realized as I was posting it, though, that it was a mis-implementation. Here's a corrected version, ditching the one-statement-per-line rule and using arbitrary line numbers (so it's not a patch to the original game or anything).

First, you need to initialize the deck. Since we don't care about suits in this game, only ranks, this process is straightforward.

Code: Select all

10 DIM DK(12), DS(12)
20 TC=52:FOR ZZ=0 TO 12:DK(ZZ)=4:NEXT
DK is the deck, while DS is the discard pile. TC is the number of total cards in the deck; we start with four of each rank. (Note that they're numbered here from 0 to 12. More on that later.) The discard pile initially has zero of each rank, as that's the default value of a numeric variable.

The subroutine proper is as follows.

Code: Select all

3000 IF TC>0 THEN 3050
3010 PRINT "I NEED TO SHUFFLE THE DECK."
3020 FOR FZ = 0 TO 12
3030 DK(FZ)=DS(FZ):TC=TC+DS(FZ):DS(FZ)=0
3040 NEXT FZ
3050 FA=INT(RND(1)*TC):FZ=0
3060 IF FA>=DK(FZ) THEN FA=FA-DK(FZ):FZ=FZ+1:GOTO 3060
3070 DK(FZ)=DK(FZ)-1:TC=TC-1:FZ=FZ+2:RETURN
Line 3000 checks to see if there are any cards left in the deck. If not, lines 3020-3040 shuffle the deck by putting the cards in the discard pile into the deck and updating the total card count. Why use a discard pile at all, you might ask? To keep the player from drawing two copies of the same card. This would be especially important in a game with multiple players having hands of cards.

As a side note, I reserve variables starting with F for use inside subroutines. Remember that scoped variables aren't a thing in BASIC.

Line 3050 chooses a number from 0 to TC-1. (Visualize it as counting down from the top of the deck to a random card and pulling it out rather than shuffling the deck and drawing the top card.) Line 3060 then handles figuring out what rank that card is. If we have enough cards of rank FZ, we stop there and go to line 3070, which removes that card from the deck, adds 2 (to put the return value from 2 to 14), and RETURNs. (The program calling the subroutine knows to look in FZ for the result.) Otherwise, we subtract the number of cards in rank FZ from FA, add 1 to FZ, and keep trying until we get it right. (If you have trouble following that, just ask, and I'll break it down some more.)

At this point, ManxomeBromide, who'd taken a look at the book before, chipped in that he wanted to be the one to cover the next program in the book, Amazing.

Image

I'll cover his coverage, and my response, next time! In the meantime, can you figure out how this code works?

User avatar
ManxomeBromide wrote: 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: Select all

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: Select all

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: Select all

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:

Code: Select all

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: Select all

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: Select all

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: Select all

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!

Code: Select all

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: Select all

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:

Code: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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:

Code: Select all

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:

Code: Select all

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?

Code: Select all

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.
Last edited by FredMSloniker on Tue Jun 30, 2020 4:29 pm, edited 1 time in total.

User avatar
ManxomeBromide wrote: 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: Select all

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.
Image

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: Select all

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: Select all

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.

User avatar
Image

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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

Manipulating strings

Code: Select all

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.

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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?

User avatar
This thread has been awfully quiet, and I'd like to know why. Do you not know how to contribute? Just ask. Are you waiting for me to get caught up with the SA thread? I can dump the remaining posts here if no one wants to comment on them. Or are there just not very many people following the thread? Lemme know, so I know what to do!

User avatar
I'm here, just don't have a lot of time to mess around with BASIC until the weekend. :P The last time I did programming of any sort was in Python so this is going to be a huge step backwards in terms of user-friendliness, to say the least.

User avatar
I'm going to start this post by covering the 'exercises' I mentioned regarding Animal. Here's a simple fix for the issue of jumping out of the X and Y loops:

Code: Select all

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!)

Let's move on to a game that's actually halfway worth playing!

Image

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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 6[super]5[/super]. 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 6[super]8[/super]-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: Select all

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.

User avatar
Site Admin
FredMSloniker wrote:
Wed Jul 01, 2020 10:59 pm
This thread has been awfully quiet, and I'd like to know why. Do you not know how to contribute? Just ask. Are you waiting for me to get caught up with the SA thread? I can dump the remaining posts here if no one wants to comment on them. Or are there just not very many people following the thread? Lemme know, so I know what to do!
I think it's because the forum still has that new forum smell, and so there's a fraction of people, quite a few of whom don't quite know what to do with a thread about BASIC.

Still, I will say it's very nice to see Awari, a variant of games like Mancala, in this book. It's nice to see something out of left field like that.

EDIT: Apologies if I'm not braining about BASIC, which I am pretty damn familiar with. Just having a rough week when it comes to braining in general.

User avatar
Personally I'm waiting for the thread to catch up.

Another reason could be that this kind of thread is of more interest to programmers than to game enthusiasts. I happen to be both, but the coders all were in SA's Cavern of Cobol and probably have no idea of this place, and even if they did, they wouldn't be very interesting in joining a Let's Play forum.

User avatar
I'm also waiting to catch up, but I've also just simply had less time for recreational coding these past few weeks, and this thread counts.

EDIT: That said, I've found a bug in your patched version! Your end-of-game check in lines 250-270 does not actually match the game's rules. It will not report the game as over until neither player can move. You need a check for whether either player is stuck, not both.

User avatar
ManxomeBromide wrote:
Thu Jul 02, 2020 3:12 pm
EDIT: That said, I've found a bug in your patched version! Your end-of-game check in lines 250-270 does not actually match the game's rules. It will not report the game as over until neither player can move. You need a check for whether either player is stuck, not both.
You're quite right! I can think of a few possible ways to fix this; what would you go with?

User avatar
Site Admin
Well, let's see, pseudocode wise, it would be something along the lines of:

> End state set to [Not ended]
---> If [Player] cannot move, [Endstate true]
---> If [Computer] cannot move, [Endstate true]
---> If [Neither], RETURN.

Gah, brain still not working to put that into BASIC code, but at least I've got the cases and basic idea. Either IF will end the game, but if nothing triggers, the end check fails, game continues.

User avatar
Image

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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.

Image

Image

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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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 2[super]8[/super], 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: Select all

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: Select all

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: Select all

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: Select all

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: Select all

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.



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.

Image

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

Image

(On a page of its own.)

User avatar
Image

Image

Hoo boy. This program. This is what detractors of BASIC point to when they complain about line numbers and GOTOs. This program was repeatedly hacked on, stuffing lines in willy-nilly and, when the line numbers ran out, actually using GOTO to get to unused line numbers just long enough to do a few things before GOTOing right back! When a program gets this bad, there's only one thing to do: print it out, work through its flow, and type it in from scratch.

At least, that's the way it used to work. These days we have, if nothing else, a text editor that we can flop code around in.

Here's what the program does in broad strokes. After getting the player's starting defense - but wait, there's already a bug! If the player types in an incorrect defense number in line 76, it heads off to line 2010, which is used elsewhere in the program to ask for a new defense number. However, that code assumes a game is already in play, so, among other things, it doesn't actually ask for the opposing team's name.

Anyway. Assuming we avoid that bug, play starts in line 370. Our first visit there is at the start of the game (execution falling through from line 80), but we also go back to this line at the end of the first half and at the start of overtime. The center jump, interestingly, is weighted 60-40 in the opponent's favor; if they get the ball, we head to line 3000, but if the player gets it, we drop through to line 425.

Line 425 is called from various places in the program to print a blank line, before dropping to line 430 - which is also called from various places in the program, this time to ask what shot the player wants to make next. (Some parts of the program want to add something to the beginning of the input line, most obviously line 455, hence the skip.) Line 440 sets P to 0; P is the variable for whose turn it is, and it's 0 for the player and 1 for the computer. We then check that the player entered an integer from 1 to 4 inclusive before going to line 460.

Lines 460 and 480 perform an oddly placed check. T is the number of turns that have passed during the game. The game is supposed to end at turn 100, but to add a touch of uncertainly, we also flip a coin and don't end the game if it comes up heads. Placing the check here, however, seems bad timing - especially since it means the game can only actually end (a) if the player has the ball and (b) after they've tried to make a move. The first may be intended behavior, but the second seems like a bad idea.

We'll come back to line 490 later. Line 1000 is where we start actually processing the player's move. The ON - GOTO structure on that line, as well as the GOTO on line 1030, is an odd way of saying 'if the player made either kind of jump shot, go to 1040; otherwise, go to line 1300'. (If Z, the player's offensive play, isn't 1 or 2, execution falls through the ON - GOTO to the next line.)

Lines 1040 and 1300 are identical, because a bit of code is unnecessarily duplicated here. Lines 1040-1046 advance the game's clock; if T=50, we jump to line 8000, which prints the score at the end of the first half and heads back to line 370 for the second half's center jump. If T=92, we jump to line 1046, which calls the subroutine at 600, which prints the two-minute warning; then we drop to line 1050. If neither are true, line 1043 sends us to line 1050 anyway. If we change line 1042 to just GOSUB 600, we can omit lines 1043 and 1046 entirely. The code over at 1300-1304 can have the exact same improvement applied - or, better yet, we can move the time-advancing code somewhere else that will be called regardless of what play the player calls.

Line 1305, which we reach if the chosen play is not 1 or 2, checks if it's 0, in which case we go to the aforementioned line 2010 (change defense), which heads back to 425 to ask for an actual offensive play. Otherwise, it prints the line of text at 1320 (play 3, lay up) or 1700 (play 4, set shot) - but either way, it winds up at 1330. Plays 1 and 2 don't even get that much difference, showing the line 'jump shot' at line 1050 before heading to line 1060. As it turns out, there are only two actual plays in the player's handbook: 'jump shot' and 'not jump shot'.

From here, the code gets distinctly wiggly. There are five possible results of a shot: the shot is good (all shots score two points), the shot is missed, the shot is blocked, the shooter is fouled, or the shooter is assessed a charging foul. The order in which these possibilities are checked, the odds of success on a given check, and the odds of one side or the other recovering the ball, depend on whether or not the shot was a jump shot and, in some but only some cases, on the active defense.

One way or another, we call the subroutine at 6000 if the player scores, call the subroutine at 4000 if the player is fouled (it handles the two free throws they get), and head to either line 430 (if the player still has the ball) or line 3000 (if the computer gets it).

Over at line 3000, we set P to 1 - it's now the computer's turn - then advance the clock with similar code to that at 1040 - but not identical, because we've hit on another bug. The game doesn't check for the two-minute warning if the computer has the ball - line 3015 exists, to give said warning, but line 3012 jumps over it, and no other line in the code goes there. (It's not the only code that never gets called; line 1180, which is supposed to handle the player being fouled on a jump shot, can't be reached.)

Line 3020 picks a play for the computer in a strange way. Instead of returning an integer in the 1-4 range, it returns a number from 1 to 3.5. As this number is handled in much the same way as the player's numbers, this means that the computer is half as likely to make set shots as it is to make any other play - but, like the player, only 'jump shot' and 'not jump shot' actually matter, so it winds up being an 8:7 bias toward jump shots. Again, there's a complicated wodge of code to determine the result of the given play, sending execution to 425 or 3000, depending. Amazingly, it does reuse the subroutine at 4000 (the only place P is actually checked), but scoring is handled at line 7000.

Lines 5000-5030 and 5100-5140 are most likely a late addition to the code, as they're actually part of the code blocks, such as they are, for lines 3000 and 425, respectively - if a press defense is in play, there's a chance that either team can steal the ball from the other team. The code starting at 8000 is called at the end of the first half, while the code starting at line 491 is called when the game should be over; if the game's drawn, however, it falls through to line 492 which announces overtime, puts some time back on the clock (on line 499), and heads back to the center jump (on line 370).

Line 520, which is where the game is supposed to end, uses a STOP, which shows an error. Line 9999, the last line in the program, contains a doubly-unnecessary END - since you can't actually reach that line.

This is a program that could use some serious optimization. If you stored the opposing team's name in a string array, storing "DARTMOUTH" (or your team name of choice) in the other element of the array, you could consolidate a bunch of '(this team) has the ball'-type text. (We've already got P! We're barely using it!) We could also work on improving the flow of all those random checks - eliminate redundancies, store values that change by play in an array, that sort of thing. While we're at it, we could improve the basic way the game works - create a matrix of values for offensive play vs. defensive play? And let the computer choose its own defense as well as its own offense? Maybe even have specific teams with specific stats? But just tidying up the code would be a huge improvement.

Before I go, let me share just

One Weird Trick

Let's take a look at a piece of code that's representative of something that happens a lot in this program.

Code: Select all

445 IF Z<>INT(Z) THEN 455
446 IF Z<0 OR Z>4 THEN 455
447 GOTO 460
455 PRINT "INCORRECT ANSWER.  RETYPE IT. ";:GOTO 430
460 IF RND(1)<.5 THEN 1000
Now let's look at an abstract version of this code:

Code: Select all

	IF (this thing is true) THEN (go to label a)
	GOTO (label b)
(label a)
	(do stuff): GOTO (somewhere else)
(label b)
	(do other stuff)
Why do we have a block of branching code where one of the branches jumps immediately after the other when the other does not fall through to that code? That's just untidy. Let's swap the branches.

Code: Select all

	IF (this thing is not true) THEN (go to label)
	(do stuff): GOTO (somewhere else)
(label)
	(do other stuff)
See how much simpler the code is when we take advantage of the natural flow? It's not a huge savings in code length -

Code: Select all

445 IF Z=INT(Z) AND Z>=0 AND Z<=4 THEN 460
455 PRINT "INCORRECT ANSWER.  RETYPE IT. ";:GOTO 430
460 IF RND(1)<.5 THEN 1000
- but it's easier to understand, and it's a savings that can be applied throughout the program. Whenever possible, work with the flow in BASIC!

User avatar
FredMSloniker wrote:
Fri Jul 03, 2020 7:24 pm
See how much simpler the code is when we take advantage of the natural flow? It's not a huge savings in code length -

Code: Select all

445 IF Z=INT(Z) AND Z>=0 AND Z<=4 THEN 460
455 PRINT "INCORRECT ANSWER.  RETYPE IT. ";:GOTO 430
460 IF RND(1)<.5 THEN 1000
- but it's easier to understand, and it's a savings that can be applied throughout the program. Whenever possible, work with the flow in BASIC!
It's easier to understand, but it's treacherous. This burned me in AMAZING a bit. :)

In the modern world we generally think of AND and OR and like it terms that ultimately come from LISP: there's a collection of values, and we want to know of all (AND) or any (OR) of them are true. We are used to a world where the computer goes through the alternatives in order and sees if they are true (or "truthy" - many languages don't explicitly have 'true/false' as separate values and instead have some way of assigning truth or falsity to arbitrary values.) Once it finds something that lets it know the answer right away, we expect the calculation to stop.

BASIC doesn't do this, and that means that if you need to need to check something like
IF (it is safe to even check the value) AND (the value checked has some property) THEN (do the thing)
BASIC will go right ahead and do the dangerous check, and very likely end up crashing at some point with the BASIC equivalents of a NullPointerException or an IndexOutOfBoundsException. In these cases you have to do the chained IF statements.

The formal term for this is that in BASIC, the AND/OR/NOT operators do not short-circuit. The reason they don't, in many but not all BASICs, is kind of interesting: it's because they aren't actually logical operators.

Instead, these are operations that combine the bits in their arguments; AND returns the number that has a 1 bit in every slot that both operands had a 1; OR returns a number with a 1 bit in every slot that either operand had a 1 in; NOT flips all the bits. In a Microsoft BASIC, 10 AND 12 is 8; 10 OR 12 is 14; NOT 10 is - due to the way integer math works on the computers Microsoft BASIC uses - negative 11.

So why does this work at all? Well, it's not really consistent what you'll get out if you print out a statement like PRINT 3>2. But on MS BASICs, it will print out -1. This is the integer with all 1 bits set.

And that is what makes this work. Generally speaking, 0 is false, and all non-zero values are truthy. The problem with this from a logical standpoint is that if you'd like NOT to be negation, now all numbers save -1 are falsy, which means that this program prints out both lines on a C64:

Code: Select all

10 IF 3 THEN PRINT "YAY!"
20 IF NOT 3 THEN PRINT "WAIT, WHAT?"
But as long as the only values you are working with are 0 and -1, then there is no difference between bit-level and logic-level operations for determining truth and falsehood.

(This is also why languages in the C tradition use && and || in their logical tests; & and | are the bitwise checks and would suffer from the problems we see here.)

User avatar
It can get more degenerate than that. Consider the humble code:

Code: Select all

IF A=3 THEN X=X+2 ELSE X=X+5
This code works exactly the same way:

Code: Select all

X=X+5+(A=3)*3

User avatar
Now that I've thought on your post, ManxomeBromide, I have a few more things to add.

You're right that AND, OR, and NOT are bitwise operators, not logical operators. Any comparison function returns not a boolean 'true' or 'false' but a number representing 'true' or 'false', and those numbers are 'all bits on' (-1 in all BASIC implementations I'm aware of) or 'all bits off' (0, likewise). That said, I have two nits with your nits. One, you should be aware of this and not be using 3 to mean 'true' in the first place, and if you want to know if 3 is or is not 0, you should be making that comparison explicitly. Two, when it comes to having multiple comparisons on one line, the original program started it. :V (Line 446.)

You're also right that BASIC doesn't short-circuit logical operators (on account of not having them). The most common way this will cause problems is attempting to access an array element that doesn't exist; for instance, if you want to check the four neighbors of an element in a two-dimensional grid, you're going to run into trouble if you try to check A(5,-1). Specifically, you're going to get an ILLEGAL QUANTITY ERROR if your array index is less than zero and a BAD SUBSCRIPT ERROR if your array index is too large. (At least in Commodore V2 BASIC.) That's your IndexOutOfBoundsException. (BASIC doesn't have pointers, so it can't throw a NullPointerException; the closest I can think of is trying to access memory that doesn't exist with PEEK() or POKE, which also gives an ILLEGAL QUANTITY ERROR.)

Anyway. The more you know.

Image

Hey, remember that thing I said about guess-the-number games being as prolific as they are boring? So are remove-the-object games. BATNUM gets credit for exactly two things. One, it was written by John Kemeny, who, as previously mentioned, was one of the two people who created BASIC. (Which means that, if I find any coding sins, I'll be Quite Cross.) Two, it can play a lot of different versions, which means you don't need programs like... er... Twenty-Three Matches. Which is also in this book. As noted above.

Yay.

Looking at the flow, I see a puzzling decision in lines 350-380; if the player enters zero for the number of objects in the pile, the program jumps to line 330 and asks for a different number right away, but if they enter an invalid number of objects, it goes to line 220 and prints a bunch of blank lines first. The error-checking looks pretty solid, though. (It doesn't check if A=B, which is a legal game, but one that only allows one move.)

Line 530 is part of the computer's strategy. In a game where it wants to avoid taking the last object, it wants to force the player to have only one object on their turn. To do this, it needs to force the player to have C+1 objects on their previous turn; that way, no matter how many objects the player takes (from A to B, as established in line 430), the computer can take C-P objects, which will also be in the A to B range, and leave just one. Similarly, the computer wants the player to have C*2+1 objects on the turn before that, C*3+1 objects on the turn before that, and so on. If, on the other hand, the computer wants to take the last object, it wants the number of objects on the player's turn to be C*X, where X is some integer, so that on the player's last turn they can't prevent the computer from taking the last object.

The core game loop is from lines 550-590. Line 550 calls the subroutine at 600, which makes the computer's move; line 560 checks W, which is set to 1 if the game is over, and jumps to line 220 to start a new game if so. Line 570 calls the subroutine at line 810, which makes the player's move (which is why line 540 jumps here if the player makes the first move); line 580 checks again if the game is over. Then line 590 starts the process again until someone wins.

Line 600 starts the subroutine that makes the computer's move. Q is set to N, the number of objects remaining, if M, the flag for whether the computer wants to take the last object or not, is 1 (meaning it wants to); if M isn't 1, Q is set to N-1. Lines 600-620 are kind of a stiff way to do this, and after examining the code, I've realized why: not only is there only one statement per line, but IF - THEN statements are only used for implicit GOTOs! Was that a limitation of the earliest BASICs?

Lines 630-670 determines if the computer has a forced loss, which can only happen if it's trying to avoid taking the last object (M isn't 1) and if N<=A (there are at most as many objects in the pile as the computer is obligated to take). If so, the code prints a loss message, sets the 'the game is over' flag, and returns. Similarly, lines 680-710 determine if the computer can win this turn, which can only happen if it's trying to take the last object (we only get here if M is 1) and N<=B (there are at most as many objects in the pile as the computer is able to take).

Line 720 figures out how many objects the computer wants to take. If it's trying to avoid taking the last object, as we discussed earlier, Q=N-1. The computer wants to take some number of objects, P, so that, at the end of this turn, there will be C*X+1 objects remaining, where X is some integer. That number of objects will also be N-P. Therefore:

C*X+1=N-P
C*X+1+P=N
C*X+P=N-1
C*X+P=Q (since Q=N-1)
P=Q-C*X
P=Q-C*INT(Q/C) (since we can freely choose an integer value for X)

Which is the statement at line 720.

If the computer is trying to take the last object, Q=N; since we want to have C*X objects at the end of the turn, we start with C*X=N-P, or C*X=Q-P, or C*X+P=Q, and proceed from there as above. Either way, line 720 gives the number of pieces the computer wants to take.

But why that X in particular? The answer lies in modulo math. Q-C*INT(Q/C) is another way of writing Q modulo C, that is, the remainder if you divide Q by C. Since the computer wants to reduce the number of objects in the pile by C at a time, and since it would like to take Q objects if only it could, by repeatedly reducing Q by C it will eventually reach a valid move (in theory - see below) - and that repeated subtraction of C from Q is the same as dividing Q by C and taking the remainder.

If the computer has a winning strategy, though, then so does the player. In the game Twenty-Three Matches (A=1, B=3, N=23, M=2), whoever goes first always wins with perfect play. Solving the equation on line 720 gives us that move: two matches. But if we let the player go first, and they take that move, when we come to line 720, N=21, which gives a P of... 0. Which is to say, it'd really much rather it be the player's turn, thanks.

Unfortunately for it, that's not an option, and lines 730-760 enforce that. (I don't think P will ever be greater than B to begin with. I'd also consider changing this code so that, if the computer can't make the move it wants, it makes a random move in hopes of confusing the player.) Line 770 applies the move, line 780 describes the move, and line 790 confirms that the game is not over. (This means we don't have to explicitly reset W at the start of a new game, since this line and its twin, line 1060, in the player code take care of that before the win condition is first checked.) Last but not least, line 800 returns control to the main loop.

Lines 810-820 starts the player's turn handling by asking for a move. Note the cheeky bit of code at lines 830-860. Lines 880-890 handle the player taking fewer objects than they're required to; this is only legal if there are fewer objects remaining than that, and then only if they take all of those objects. If that's the case, we go to line 960 to show if they won or lost; otherwise, it's an illegal move.

If we get to line 910, it's because the player has taken at least as many objects as they're required to. Here we make sure they've taken at most as many objects as they're allowed to, then head to line 940, where we take away that many objects, then check to see if there are now zero objects remaining, in which case we fall through to 960's end-of-game code. Otherwise, we head to 1030, which does a sanity check on the number of items remaining; if we now have a negative number, we put the objects back and send up an illegal move error. Otherwise, we mark the game as not over and return.

The code here is a tiny bit tangled. If we put a check in earlier for if P>N, we wouldn't need to put any objects back in the first place. Adapting the code within the rather draconian restrictions of this flavor of BASIC is left as an exercise for the reader.

That said, this is a perfectly serviceable BASIC game, as should be expected from one of the people that created the language. It's just a shame I've already explained to you how to beat it.

User avatar
The weird thing in lines 330-380 is that they first do a not equal to 0 check, and then a separate less than one check to deal with negatives. There's no point to that.

User avatar
FredMSloniker wrote:
Sat Jul 04, 2020 7:12 pm
You're right that AND, OR, and NOT are bitwise operators, not logical operators. Any comparison function returns not a boolean 'true' or 'false' but a number representing 'true' or 'false', and those numbers are 'all bits on' (-1 in all BASIC implementations I'm aware of) or 'all bits off' (0, likewise).
Apple, IBM, Commodore, and even some less famous machines like the TRS-80 and the Exidy Sorcerer all ran descendants of Microsoft BASIC. I had to do some digging to find some examples of other kinds, and it turns out they don't all have the same behavior:
  • Sinclair BASIC, which ran on the ZX line of machines (most famously the Spectrum), has an AND and OR that return one of the arguments inconsistently, instead of being a bit operation. It uses 1 instead of -1 for a true value from a boolean comparison.
  • Atari BASIC (the original one, not the Microsoft BASIC port it got later) on the Atari 800 uses 1 for true, and doesn't short circuit, but AND, OR, and NOT are logical operators--in particular, NOT 3 is 0.
  • BBC BASIC is not Microsoft BASIC either, and has some unusual QBASIC-like features that do not match any other dialect. (It also has a totally kickass integrated assembler that makes mixing BASIC and machine code sufficiently trivial that I bet it kept a lot of people from moving on to C for years.) Despite not being an MS BASIC dialect, though, its behavior here completely matches it. (I did this test with BBC BASIC V from 1989, running on a simulated ARM610 Risc PC.)
FWIW. Our esteemed host JamieTheD will know more about Sinclair and BBC than I will, I imagine.

User avatar
Site Admin
BBC BASIC did indeed keep some people from moving to C for a bit, although a lot of the good stuff on the BBC was compiled from PASCAL. Still, part of the reason why? PROCEDURES and FUNCTIONS.

Yes, BBC BASIC had functions. We'll get into that in a moment. But it was also heavily optimised, running well designed programs a little faster than Microsoft BASIC contemporaries of the time.

It's a shame that the BBC Micro's DRAW functions were, to put it bluntly, shit. The C64 most definitely outperformed it graphically, much as it outperformed a lot of the 8 bits at the time. BBC BASIC also had REPEAT UNTIL loops (saving a little time for the programmer), and IF-THEN-ELSE (which not all contemporaries had.)

Still... PROCedures and FuNctions. The major difference being that PROC doesn't return anything, and FN does. However, both can have values passed into them, and both need to be DEFined, then ENDed. An example given in the BBC BASIC manual goes like so.

Code: Select all

 10 MODE 12
 20 PRINT TAB(0,10)"Countdown commencing ";
 30 FOR N% = 30 TO 1 STEP -1
 40   PRINT TAB(22,10) "  " TAB(22,10);N%;
 50   PROCwait_1_second
 60 NEXT
 70 PRINT TAB(0,10) "BLAST OFF";STRING$(14," ")
 80 END
 90
100 DEF PROCwait_1_second
110 TIME = 0
120 REPEAT
130 UNTIL TIME >= 100
140 ENDPROC
There's examples of functions in some of the code in the USBORNE BBC BASIC books, and in the BBC BASIC manual (I've linked directly to it, to save time.) Still, a PROC is a good way to, say, automate a DRAW call (of which there were three types, SET, DRAW, and VDU (Which could also change the display mode, but is mostly used for printing special characters)), while FN is used for calculations (as it ENDs on an =(Return value) statement) PROCs can be recursive, although this isn't really that efficient in terms of, say, loops, and you can build a LIBRARY of functions and procedures, which can then be stored in memory!

Now, keep in mind that this is the RISC OS version of the manual, which was the last version of BBC BASIC, on the Archimedes computer (or Archie, as it's affectionately known.) But PROCs and FuNctions remained pretty much the same throughout. The DRAW commands, however...

There's a reason I said the BBC Micro had shit drawing commands. You cannot draw a circle with any sort of CIRCLE command, like you can in RISC OS BASIC. You cannot, in fact, DRAW a box without either five lines (four LINE commands, and one GCOL (Graphics COLour) command), or two PLOT commands (which can draw, er... Points, lines, dotted lines, and, most importantly of all, filled triangles.) And it'll not look great, or update very well, unless you use PROCs. The segment on Advanced Graphics can be found here in the original BBC BASIC manual.

So, yes, procedures, functions, and draw commands that make me want to tear my hair out. Apart from that, there's really only slight differences, such as the RNG. Fun fact: BBC Micro games with random factors are incredibly speedrun friendly, because the RNG is, in fact, a stored sequence from which it takes sequentially. So even if you have a time based randomiser (as you should in all BBC BASIC programs), so long as you have the timing down, you can get the result you want.

This is perhaps why there aren't that many BBC Micro games with random factors, except boardgames, or at the very beginning when generating things.

As to the Sinclair line, I'm afraid I can't help too much. And I've probably made a pig's ear of things, but the BBC BASIC manual is a fun read for BASIC nerds. 90% of the USBORNE BASIC books are similar programs to this, with the odd graphical element mostly done with VDU and PLOT commands, and one notable exception: The Fantasy Adventure book. Which was basically a small, simple RPG with a level editor.

I love that book. Actually, I love all the books, but especially the weird text adventure duo, and Fantasy Adventure.

User avatar
Image

Image

Image
BASIC Computer Games wrote: BATTLE is based on the popular game Battleship which is primarily played to familiarize people with the location and designation of points on a coordinate plane.
I'm pretty sure that's not why people play Battleship. At any rate, this program is only loosely based on Battleship; for one thing, the computer doesn't actually shoot back. For another thing -
BASIC Computer Games wrote: The main objective and primary educational benefit of BATTLE comes from attempting to decode the bad guys' fleet disposition code. To do this, you must make a comparison between the coded matrix and the actual matrix which you construct as you play the game.
- there's this. The only real 'gameplay' is trying to crack the coded matrix. The problem is that said 'code' is the same every time, so once you've solved it, there's nothing else to do with the program. With that in mind, I'm going to focus on how the coded matrix is made, then move on.

Code: Select all

1180 INPUT X,Y

...

1230 R=7-Y
1240 C=X
2150 IF F(R,C)>0 THEN 1290
F() is a two-dimensional array that holds the starting state of the fleet. The first dimension is R, the row, and the second C, the column. We can use whatever variable names we want, of course, but that's what line 2150, which checks for a hit, uses.

A 6x6 grid of row and column values looks like this. (If you use spreadsheets, this should look slightly familiar.)

Illustration 1
(R=1,C=1) (R=1,C=2) (R=1,C=3) (R=1,C=4) (R=1,C=5) (R=1,C=6)
(R=2,C=1) (R=2,C=2) (R=2,C=3) (R=2,C=4) (R=2,C=5) (R=2,C=6)
(R=3,C=1) (R=3,C=2) (R=3,C=3) (R=3,C=4) (R=3,C=5) (R=3,C=6)
(R=4,C=1) (R=4,C=2) (R=4,C=3) (R=4,C=4) (R=4,C=5) (R=4,C=6)
(R=5,C=1) (R=5,C=2) (R=5,C=3) (R=5,C=4) (R=5,C=5) (R=5,C=6)
(R=6,C=1) (R=6,C=2) (R=6,C=3) (R=6,C=4) (R=6,C=5) (R=6,C=6)

Line 1180 gets the coordinates the player wants to shoot. Note that we can ask for more than one variable's worth of input at a time; the values must be separated by commas, and if the player doesn't give enough, the program will prompt for more input. (In Commodore BASIC V2, it does so by printing ??.) It will keep the input already given, though, so if the player only types a value for X, they then type only a value for Y.

The player's input is in Cartesian coordinates. A 6x6 grid of Cartesian coordinates would look like this.

Illustration 2
(X=1,Y=6) (X=2,Y=6) (X=3,Y=6) (X=4,Y=6) (X=5,Y=6) (X=6,Y=6)
(X=1,Y=5) (X=2,Y=5) (X=3,Y=5) (X=4,Y=5) (X=5,Y=5) (X=6,Y=5)
(X=1,Y=4) (X=2,Y=4) (X=3,Y=4) (X=4,Y=4) (X=5,Y=4) (X=6,Y=4)
(X=1,Y=3) (X=2,Y=3) (X=3,Y=3) (X=4,Y=3) (X=5,Y=3) (X=6,Y=3)
(X=1,Y=2) (X=2,Y=2) (X=3,Y=2) (X=4,Y=2) (X=5,Y=2) (X=6,Y=2)
(X=1,Y=1) (X=2,Y=1) (X=3,Y=1) (X=4,Y=1) (X=5,Y=1) (X=6,Y=1)

In Cartesian coordinates, therefore, x increases as you go to the right, and y increases as you go up. However, in a spreadsheet, while the column increases as you go to the right, the row increases as you go down. Lines 1230 and 1240 convert X and Y to R and C.

But that's only half of the picture. The other half is the coded matrix. How does that get printed out?

Code: Select all

1050 FOR I=1 TO 6
1051 FOR J=1 TO 6
1052 H(I,J)=F(J,I)
1053 NEXT J
1054 NEXT I
1060 FOR I=1 TO 6
1061 FOR J=1 TO 6
1062 PRINT H(I,J);
1063 NEXT J
1064 PRINT
1065 NEXT I
H() is a two-dimensional array that holds the coded matrix. The loop from line 1060 to line 1065 prints it out row by row, while the loop from line 1061 to line 1063 prints an individual row. If line 1062 printed the values of I and J for a given cell, the output might look something like this:

Illustration 3
(I=1,J=1) (I=1,J=2) (I=1,J=3) (I=1,J=4) (I=1,J=5) (I=1,J=6)
(I=2,J=1) (I=2,J=2) (I=2,J=3) (I=2,J=4) (I=2,J=5) (I=2,J=6)
(I=3,J=1) (I=3,J=2) (I=3,J=3) (I=3,J=4) (I=3,J=5) (I=3,J=6)
(I=4,J=1) (I=4,J=2) (I=4,J=3) (I=4,J=4) (I=4,J=5) (I=4,J=6)
(I=5,J=1) (I=5,J=2) (I=5,J=3) (I=5,J=4) (I=5,J=5) (I=5,J=6)
(I=6,J=1) (I=6,J=2) (I=6,J=3) (I=6,J=4) (I=6,J=5) (I=6,J=6)

Which means that, for H(), I is the row and J is the column (compare to Illustration 1).

The loop from line 1050 to line 1054, meanwhile, is what sets up the coded matrix in the first place. Line 1052 in particular transposes the two dimensions of F() into H(); rows become columns and columns become rows.

So. The player looks at the coded matrix and picks a row and column they want to shoot. They then transpose the row and column, convert them into x and y Cartesian coordinates, and enter them into the game, which converts them back into a row and column and checks them against the fleet grid. Which means that the sample code sheet on page 15 decodes to this:

Illustration 4
0 0 5 5 5 5
0 4 0 0 1 0
0 4 0 6 0 1
2 4 6 0 0 0
2 6 0 0 0 0
6 0 0 3 3 3

You can compare this to the sample output to confirm this.

If there's anything else you want to know about this program, let me know. Otherwise, I'm going to move on to the next program, which is actually fairly interesting-looking - and also nearly two pages of code, so it'll take me a bit to break it down.

User avatar
Not Sploosh Kaboom, rated 1/10.

User avatar
JamieTheD wrote:
Mon Jul 06, 2020 3:23 pm
There's examples of functions in some of the code in the USBORNE BBC BASIC books, and in the BBC BASIC manual (I've linked directly to it, to save time.) Still, a PROC is a good way to, say, automate a DRAW call (of which there were three types, SET, DRAW, and VDU (Which could also change the display mode, but is mostly used for printing special characters)), while FN is used for calculations (as it ENDs on an =(Return value) statement) PROCs can be recursive, although this isn't really that efficient in terms of, say, loops, and you can build a LIBRARY of functions and procedures, which can then be stored in memory!

Now, keep in mind that this is the RISC OS version of the manual, which was the last version of BBC BASIC, on the Archimedes computer (or Archie, as it's affectionately known.) But PROCs and FuNctions remained pretty much the same throughout. The DRAW commands, however...

There's a reason I said the BBC Micro had shit drawing commands. You cannot draw a circle with any sort of CIRCLE command, like you can in RISC OS BASIC. You cannot, in fact, DRAW a box without either five lines (four LINE commands, and one GCOL (Graphics COLour) command), or two PLOT commands (which can draw, er... Points, lines, dotted lines, and, most importantly of all, filled triangles.) And it'll not look great, or update very well, unless you use PROCs. The segment on Advanced Graphics can be found here in the original BBC BASIC manual.

90% of the USBORNE BASIC books are similar programs to this, with the odd graphical element mostly done with VDU and PLOT commands, and one notable exception: The Fantasy Adventure book. Which was basically a small, simple RPG with a level editor.

I love that book. Actually, I love all the books, but especially the weird text adventure duo, and Fantasy Adventure.
I've actually gotten RISC OS 5 running on my Raspberry Pi 3, and it too runs BBC BASIC like a champ:



I'm guessing that this is one of the "weird text adventure duo" there :)
JamieTheD wrote:
Mon Jul 06, 2020 3:23 pm
It's a shame that the BBC Micro's DRAW functions were, to put it bluntly, shit. The C64 most definitely outperformed it graphically, much as it outperformed a lot of the 8 bits at the time.
That, alas, was no credit to BASIC. The C64 BASIC was decidedly lacking, and in fact had no language support for graphics of any kind beyond that you could type out semigraphics and color and cursor control commands. Doing any kind of sprite or bitmapped graphics (or sound or joystick I/O, for that matter) required you to directly comunicate with the other chips via POKE and PEEK commands, and there was a very sharp limit on how fast you could make that go. Generally you had to create a BASIC/Machine Language hybrid program, and the C64 didn't really have much intentional support for that either. This is why so many C64 type-ins were either hexdumps or 75% DATA statements by weight and had 3-minute startup times as it slowly parsed it all through and loaded it into some unused memory.

User avatar
Site Admin
ManxomeBromide wrote:
Tue Jul 07, 2020 3:13 am


I'm guessing that this is one of the "weird text adventure duo" there :)
Yup! That one is the okay one. The other (Island of Mystery) takes... An interesting turn late in the game. I won't spoil it, even if the book sort of does before you even get to the listing... Fairly good art in those two text adventure books, though... Actually, fairly good art throughout the USBORNE books. They make the games seem a lot more dramatic than they actually are. Example: Secret Weapon (Battle Games, p. 16)

Illustration: Blue spaceship, obviously a bomber from its wide base and two B-52 like cockpits! Also it has a BIG LASER!

Story: This bombing run on the Robot Spare Parts Store must succeed to end the war against the Robots! You are the only hope, and all you can do is fire and pray!


The listing: It's a hot and cold game with number input and vague text output, Difficulty+5 guesses as to the exact X/Y of the Robot Spare Parts Store, from a grid of Difficulty x Difficulty.

User avatar
JamieTheD wrote:
Tue Jul 07, 2020 8:04 am
Actually, fairly good art throughout the USBORNE books. They make the games seem a lot more dramatic than they actually are.
This is how I imagine FredMSloniker typing in these programs:

Image

I think my favorite part is the Iron-Man-style arc reactor of GOTOs on the middle of his shirt.

(Image credit: AtariAge)

EDIT: I suppose I should say something about Battle, but it's hard to when it's less a game than a homework assignment that only has one problem and isn't even procgen. Um. Well.

"Commander, we have deciphered the enemy's secret naval code?"
"Yes?"
"It turns out we were holding the paper sideways."

User avatar
I've been reading classic robot stories by Asimov and these images quite remind me of that.

User avatar
ManxomeBromide wrote:
Tue Jul 07, 2020 6:55 pm
I suppose I should say something about Battle, but it's hard to when it's less a game than a homework assignment that only has one problem and isn't even procgen. Um. Well.

"Commander, we have deciphered the enemy's secret naval code?"
"Yes?"
"It turns out we were holding the paper sideways."
Yeah, the philosophy I've been building as I read through this is finding something worth talking about, no matter how banal the program. Acey Ducey got a line-by-line analysis, since it was the first program; Amazing got a guest breakdown by ManxomeBromide; Animal had an interesting way to build a database; Awari had a learning AI; Bagels actually error-checked its input, unlike most of the programs in here; Banner used the binary representation of numbers to store pixel data; Basketball served as an example of what not to do; and Batnum showed how to solve an entire class of taking-object puzzles. Battle was the first one that stumped me in terms of interesting things to say, and there at least I managed to scratch something out.

Let's take a brief look at a long program.

Image

Image

Image

This is an impressive program, with an accordingly long listing. It's well-commented, makes good use of subroutines, and supports a surprisingly large number of features. That said, I'm not going to look at it for long. Why?
BASIC Computer Games wrote: Cards are automatically reshuffled as the 51st card is reached. For greater realism, you may wish to change this to the 41st card in Line 110. Actually, fanatical purists will want to modify the program so it uses three decks of cards instead of just one.
You know what? Challenge accepted.

C64List

C64List is a program that does a number of useful things. Most prominently, it converts Commodore BASIC V2 programs to and from a variety of formats, including PRG (the way the C64 stores BASIC programs internally; it can be loaded into an emulator or transferred to a real C64) and TXT (what you'd expect: a plain-text version of a program). It also, however, has an LBL format, which removes the line numbers and inserts labels as necessary; I typed in Basketball and converted it from TXT to LBL to get a better view of the program flow (and to confirm that certain lines of code were never visited). As a bonus feature, it supports long variable names, which will get converted into the two-character names that the C64 actually uses.

Here's what ManxomeBromide's rewrite of Amazing looks like when I convert it from TXT to LBL and use the -extractvariables option. (Apologies for the scrolling; that's how this board handles the code tag.)

Code: Select all

{var:SuperVariable0000=XM}
{var:SuperVariable0001=YM}
{var:SuperVariable0002=W}
{var:SuperVariable0003=V}
{var:SuperVariable0004=VD}
{var:SuperVariable0005=R}
{var:SuperVariable0006=TI}
{var:SuperVariable0007=S}
{var:SuperVariable0008=C}
{var:SuperVariable0009=Z}
{var:SuperVariable0010=N}
{var:SuperVariable0011=X}
{var:SuperVariable0012=Y}
{var:SuperVariable0013=L$}
{var:SuperVariable0014=M$}
{var:SuperVariable0015=R$}
	PRINT TAB(12);"AMAZING PROGRAM"
	PRINT TAB(11);"CREATIVE COMPUTING":PRINT TAB(9);"MORRISTOWN, NEW JERSEY"
	PRINT:PRINT:PRINT:PRINT
{:40}
	INPUT "WHAT ARE YOUR WIDTH AND LENGTH";{var:SuperVariable0000},{var:SuperVariable0001}
	IF {var:SuperVariable0000}<2 OR {var:SuperVariable0001}<2 THEN PRINT "MEANINGLESS DIMENSIONS. TRY AGAIN.":GOTO {:40}
	DIM {var:SuperVariable0002}({var:SuperVariable0000}+1,{var:SuperVariable0001}+1),{var:SuperVariable0003}({var:SuperVariable0000},{var:SuperVariable0001}),{var:SuperVariable0004}(4)
	{var:SuperVariable0005}=RND(-{var:SuperVariable0006})
	{var:SuperVariable0005}=INT(RND(1)*{var:SuperVariable0000}+1):{var:SuperVariable0007}=1:{var:SuperVariable0002}({var:SuperVariable0005},{var:SuperVariable0007})=1:{var:SuperVariable0008}=2:{var:SuperVariable0009}=0
{:90}
	{var:SuperVariable0010}=0:{var:SuperVariable0011}=0
	IF {var:SuperVariable0005}<>1 AND {var:SuperVariable0002}({var:SuperVariable0005}-1,{var:SuperVariable0007})=0 THEN {var:SuperVariable0010}={var:SuperVariable0010}+1:{var:SuperVariable0004}({var:SuperVariable0010})=1
	IF {var:SuperVariable0007}<>1 AND {var:SuperVariable0002}({var:SuperVariable0005},{var:SuperVariable0007}-1)=0 THEN {var:SuperVariable0010}={var:SuperVariable0010}+1:{var:SuperVariable0004}({var:SuperVariable0010})=2
	IF {var:SuperVariable0005}<>{var:SuperVariable0000}AND {var:SuperVariable0002}({var:SuperVariable0005}+1,{var:SuperVariable0007})=0 THEN {var:SuperVariable0010}={var:SuperVariable0010}+1:{var:SuperVariable0004}({var:SuperVariable0010})=3
	IF ({var:SuperVariable0007}={var:SuperVariable0001}AND {var:SuperVariable0009}=0) OR ({var:SuperVariable0007}<>{var:SuperVariable0001}AND {var:SuperVariable0002}({var:SuperVariable0005},{var:SuperVariable0007}+1)=0) THEN {var:SuperVariable0010}={var:SuperVariable0010}+1:{var:SuperVariable0004}({var:SuperVariable0010})=4
	IF {var:SuperVariable0010}>0 THEN {var:SuperVariable0010}={var:SuperVariable0004}(INT(RND(1)*{var:SuperVariable0010})+1)
	ON {var:SuperVariable0010}+1 GOSUB {:390}, {:400}, {:410}, {:420}, {:430}
	IF {var:SuperVariable0011}=0 THEN {var:SuperVariable0008}={var:SuperVariable0008}+1:GOTO {:210}
{:170}
	IF {var:SuperVariable0005}<>{var:SuperVariable0000}THEN {var:SuperVariable0005}={var:SuperVariable0005}+1:GOTO {:200}
	IF {var:SuperVariable0007}<>{var:SuperVariable0001}THEN {var:SuperVariable0005}=1:{var:SuperVariable0007}={var:SuperVariable0007}+1:GOTO {:200}
	{var:SuperVariable0005}=1:{var:SuperVariable0007}=1
{:200}
	IF {var:SuperVariable0002}({var:SuperVariable0005},{var:SuperVariable0007})=0 THEN {:170}
{:210}
	IF {var:SuperVariable0009}=0 OR {var:SuperVariable0008}<{var:SuperVariable0000}*{var:SuperVariable0001}+1 THEN {:90}
	PRINT CHR$(176);:IF {var:SuperVariable0002}(1,1)=1 THEN PRINT "  ";:GOTO {:240}
	PRINT CHR$(195);CHR$(195);
{:240}
	FOR {var:SuperVariable0011}=2 TO {var:SuperVariable0000}:PRINT CHR$(178);:IF {var:SuperVariable0002}({var:SuperVariable0011},1)=1 THEN PRINT "  ";:GOTO {:260}
	PRINT CHR$(195);CHR$(195);
{:260}
	NEXT {var:SuperVariable0011}:PRINT CHR$(174)
	FOR {var:SuperVariable0012}=1 TO {var:SuperVariable0001}:PRINT CHR$(221);:FOR {var:SuperVariable0011}=1 TO {var:SuperVariable0000}
	IF {var:SuperVariable0003}({var:SuperVariable0011},{var:SuperVariable0012})<2 THEN PRINT "  ";CHR$(221);:GOTO {:300}
	PRINT "   ";
{:300}
	NEXT {var:SuperVariable0011}:PRINT:{var:SuperVariable0013}=CHR$(171):{var:SuperVariable0014}=CHR$(219):{var:SuperVariable0015}=CHR$(179)
	IF {var:SuperVariable0012}={var:SuperVariable0001}THEN {var:SuperVariable0013}=CHR$(173):{var:SuperVariable0014}=CHR$(177):{var:SuperVariable0015}=CHR$(189)
	PRINT {var:SuperVariable0013};:IF {var:SuperVariable0003}(1,{var:SuperVariable0012})=0 OR {var:SuperVariable0003}(1,{var:SuperVariable0012})=2 THEN PRINT CHR$(195);CHR$(195);:GOTO {:340}
	PRINT "  ";
{:340}
	FOR {var:SuperVariable0011}=2 TO {var:SuperVariable0000}:PRINT {var:SuperVariable0014};
	IF {var:SuperVariable0003}({var:SuperVariable0011},{var:SuperVariable0012})=0 OR {var:SuperVariable0003}({var:SuperVariable0011},{var:SuperVariable0012})=2 THEN PRINT CHR$(195);CHR$(195);:GOTO {:370}
	PRINT "  ";
{:370}
	NEXT {var:SuperVariable0011}:PRINT {var:SuperVariable0015}:NEXT {var:SuperVariable0012}
	END
{:390}
	{var:SuperVariable0011}=1:RETURN
{:400}
	{var:SuperVariable0002}({var:SuperVariable0005}-1,{var:SuperVariable0007})={var:SuperVariable0008}:{var:SuperVariable0003}({var:SuperVariable0005}-1,{var:SuperVariable0007})=2:{var:SuperVariable0005}={var:SuperVariable0005}-1:RETURN
{:410}
	{var:SuperVariable0002}({var:SuperVariable0005},{var:SuperVariable0007}-1)={var:SuperVariable0008}:{var:SuperVariable0003}({var:SuperVariable0005},{var:SuperVariable0007}-1)=1:{var:SuperVariable0007}={var:SuperVariable0007}-1:RETURN
{:420}
	{var:SuperVariable0002}({var:SuperVariable0005}+1,{var:SuperVariable0007})={var:SuperVariable0008}:{var:SuperVariable0003}({var:SuperVariable0005},{var:SuperVariable0007})={var:SuperVariable0003}({var:SuperVariable0005},{var:SuperVariable0007})+2:{var:SuperVariable0005}={var:SuperVariable0005}+1:RETURN
{:430}
	IF {var:SuperVariable0007}={var:SuperVariable0001}THEN {:450}
	{var:SuperVariable0002}({var:SuperVariable0005},{var:SuperVariable0007}+1)={var:SuperVariable0008}:{var:SuperVariable0003}({var:SuperVariable0005},{var:SuperVariable0007})={var:SuperVariable0003}({var:SuperVariable0005},{var:SuperVariable0007})+1:{var:SuperVariable0007}={var:SuperVariable0007}+1:RETURN
{:450}
	{var:SuperVariable0009}=1:IF {var:SuperVariable0003}({var:SuperVariable0005},{var:SuperVariable0007})<>0 THEN {var:SuperVariable0003}({var:SuperVariable0005},{var:SuperVariable0007})=3:RETURN
	{var:SuperVariable0003}({var:SuperVariable0005},{var:SuperVariable0007})=1:{var:SuperVariable0005}=1:{var:SuperVariable0007}=1:{var:SuperVariable0008}={var:SuperVariable0008}-1
	IF {var:SuperVariable0002}({var:SuperVariable0005},{var:SuperVariable0007})=0 THEN {var:SuperVariable0011}=1:{var:SuperVariable0008}={var:SuperVariable0008}+1
	RETURN
With a bit of search-and-replace, we can change {var:SuperVariable0000 (note the lack of closing brace) to, say, {var:mazewidth and have something easier to understand in the code. Similarly, we can replace {:40} with {:getwidthandlength} or something. Then we can make edits and convert back to TXT format, or into PRG format to run in VICE or somesuch.

But that's grabbing existing code and messing with it. Let's throw Blackjack out the window and start from scratch.

Blackjack, The Modern Way

I went through a brief gambling phase when I was younger; the local casino had a deal on Tuesdays where, if you bought $20 worth of chips, they'd give you $30, so long as you went through them at least once before cashing out. Blackjack was one of the two games I played (the other being craps), being a game with a relatively slim house advantage. But the house always wins, and when I realized, tracking my games, that I was $100 down from where I'd started, I stopped.

One thing I remember was the machines they had at every table that they would use for shuffling. There was a sort of hopper, called the shoe, in which they had eight decks' worth of shuffled cards, and they'd deposit the discards in another shoe. When they reached a card that had been randomly inserted near the bottom of the shoe, they'd place the remainder of the cards in the shoe and swap it with another shoe that was sitting in a shuffling machine. You could listen to it shuffling as you played the next several hands.

So we're going to need a shuffling machine. Let's make one.

Code: Select all

{:freshshoe}
	PRINT "GETTING A FRESH SHOE..."
	FOR {var:temp} = 0 TO 51: {var:shoe}({var:temp}) = {var:decks}: NEXT
	{var:lastcard} = INT(RND(.) * {var:lastcardrange}) + {var:lastcardmaximum}
	{var:totalcards} = 52 * {var:decks}: RETURN
This code will initialize the shoe, which is stored in {var:shoe}(). (Note that the parentheses are outside the curly braces; C64List basically does search-and-replace on variable names, and doesn't care about the difference between A and A(), so keep that in mind.) As I mentioned in my suggestion for improving Acey Deucey, I'm not actually storing the shoe in a shuffled form; instead, I'm storing the number of each card we have. (I'm using the zero index to save memory, which is why they're numbered 0-51.)

We'll set {var:decks} to 8 somewhere early in the program, so we can change it later if we want to change the number of decks used. We'll also DIM {var:shoe}(51).

While we're at it, we'll set {var:lastcardmaximum} to the earliest card we'll consider shuffling at (more precisely, the largest remaining shoe size) and {var:lastcardrange} to the number of cards after that we can possibly wait. (Technically, the number of cards after that plus one, since RND(.) * {var:lastcardrange} will return a number between 0 and not quite {var:lastcardrange}, and INT() rounds down.) In this case, we'll set {var:lastcardmaximum} to 104, or 2 * 52, and {var:lastcardrange} to 52, so the game will shuffle somewhere between six and seven decks in.

Incidentally, we could save ourselves a lot of trouble by going to video blackjack, i.e. having a single deck that got shuffled after every hand. It takes a while to initialize the shoe, which is why we have that PRINT statement.

Code: Select all

{:drawacard}
	{var:functionz} = INT(RND(.) * {var:totalcards}): {var:functiona} = 0
{:drawacardloop}
	IF {var:functionz} > {var:shoe}({var:functiona}) THEN {var:functionz} = {var:functionz} - {var:shoe}({var:functiona}): {var:functiona} = {var:functiona} + 1: GOTO {:drawacardloop}
	{var:shoe}({var:functiona}) = {var:shoe}({var:functiona}) - 1: {var:totalcards} = {var:totalcards} - 1: RETURN
Calling this function will return a card ID code in {var:functiona}. I'm setting any variable name starting with 'function' aside for subroutines to use. Similarly, any variable name starting with 'temp' will have a very limited lifespan.

When {var:totalcards} drops to or below {var:lastcard}, we'll call {:freshshoe} as soon as the current hand is done.

Code: Select all

	PRINT "PLEASE WAIT..."
	{var:tempstring} = "A23456789TJQK{$d3}{$da}{$d8}{$c1}"
	FOR {var:tempy} = 0 TO 3: FOR {var:tempx} = 0 TO 12
	{var:cardnames}({var:tempy} * 13 + {var:tempx}) = MID$({var:tempstring}, {var:tempx} + 1, 1) + MID$({var:tempstring}, {var:tempy} + 14, 1)
	NEXT: NEXT
This code will be called fairly early to initialize {var:cardnames}, an array containing the name of each card. The hex numbers at the end of {var:tempstring} are the Commodore 64's symbols for 'heart', 'diamond', 'club', and 'spade'; if you're using this program on a different system, you should know how to adapt them. We could generate a card name 'live', but doing this now saves time later, and we have the memory to spare.

I thought about adding color codes so that, for instance, spades would print in black and hearts in red - in case you don't know, 'turn the current text color red' is a symbol in the C64's character set and can be added to a string for printing - but the C64's red is rather dark, and the contrast between the two isn't great.

Let's put this together into a test program.

Code: Select all

{renumber}{number:10}{step:10}
' These commands tell C64List to start numbering the program at 10 and go up 10 for each line after that. (Renumber must be FIRST in a label file, which is why this comment is after it.)

' C64List has various ways to convert quoted text. In order to properly preserve the difference between uppercase and graphic glyphs in strings, we have to add this directive.
{alpha:off}

' We can save some memory by eliminating unnecessary spaces from the output.
{crunch:on}

' We have to supply the actual variable names that will be used for each long variable name. This is also where we set their type. If we use the same name twice, C64List will complain.
{var:cardnames=CN$}
{var:totalcards=TC}
{var:functiona=FA}
{var:functionz=FZ}
{var:shoe=S}
{var:decks=DN}
{var:lastcardrange=LR}
{var:lastcardmaximum=LM}
{var:lastcard=LC}
{var:temp=ZZ}
{var:tempstring=ZZ$}
{var:tempx=ZX}
{var:tempy=ZY}

' Dimensioning arrays, setting constants, and initializing the RNG.	
	DIM {var:cardnames}(51), {var:shoe}(51)
	{var:decks} = 8: {var:lastcardmaximum} = 104: {var:lastcardrange} = 52
	{var:temp} = RND(-TI)
	
' Initialize the {varcardnames}() array.
	PRINT "PLEASE WAIT..."
	{var:tempstring} = "A23456789TJQK{$d3}{$da}{$d8}{$c1}"
	FOR {var:tempy} = 0 TO 3: FOR {var:tempx} = 0 TO 12
	{var:cardnames}({var:tempy} * 13 + {var:tempx}) = MID$({var:tempstring}, {var:tempx} + 1, 1) + MID$({var:tempstring}, {var:tempy} + 14, 1)
	NEXT: NEXT

	GOSUB {:freshshoe}
	PRINT "DRAWING FIVE CARDS: ";
	FOR {var:tempx} = 1 TO 5: GOSUB {:drawacard}: PRINT {var:cardnames}({var:functiona}); " ";: NEXT
	PRINT
	
' Before the subroutines, we must...
	END
	
' Get a fresh shoe's worth of cards.
{:freshshoe}
	PRINT "GETTING A FRESH SHOE..."
	FOR {var:temp} = 0 TO 51: {var:shoe}({var:temp}) = {var:decks}: NEXT
	{var:lastcard} = INT(RND(.) * {var:lastcardrange}) + {var:lastcardmaximum}
	{var:totalcards} = 52 * {var:decks}: RETURN

' Draw a card. The card number is returned in {var:functiona}.
{:drawacard}
	{var:functionz} = INT(RND(.) * {var:totalcards}): {var:functiona} = 0
{:drawacardloop}
	IF {var:functionz} > {var:shoe}({var:functiona}) THEN {var:functionz} = {var:functionz} - {var:shoe}({var:functiona}): {var:functiona} = {var:functiona} + 1: GOTO {:drawacardloop}
	{var:shoe}({var:functiona}) = {var:shoe}({var:functiona}) - 1: {var:totalcards} = {var:totalcards} - 1: RETURN
Which, when we feed it into C64List, returns this text file:

Code: Select all

10 DIM CN$(51),S(51)
20 DN=8:LM=104:LR=52
30 ZZ=RND(-TI)
40 PRINT"PLEASE WAIT..."
50 ZZ$="A23456789TJQK{$d3}{$da}{$d8}{$c1}"
60 FOR ZY=0 TO 3:FOR ZX=0 TO 12
70 CN$(ZY*13+ZX)=MID$(ZZ$,ZX+1,1)+MID$(ZZ$,ZY+14,1)
80 NEXT:NEXT
90 GOSUB 140
100 PRINT"DRAWING FIVE CARDS: ";
110 FOR ZX=1 TO 5:GOSUB 180:PRINT CN$(FA);" ";:NEXT
120 PRINT
130 END
140 PRINT"GETTING A FRESH SHOE..."
150 FOR ZZ=0 TO 51:S(ZZ)=DN:NEXT
160 LC=INT(RND(.)*LR)+LM
170 TC=52*DN:RETURN
180 FZ=INT(RND(.)*TC):FA=0
190 IF FZ>S(FA)THEN FZ=FZ-S(FA):FA=FA+1:GOTO 190
200 S(FA)=S(FA)-1:TC=TC-1:RETURN
And a PRG that gives us this result when run a few times:

Image

Whew! This post is getting pretty long, and we've barely done anything yet. Read this through, let me know if there's anything that isn't crystal clear, and we'll pick up from here next time!

Post Reply